diff --git a/404.html b/404.html index fe3fe41..bebb62d 100644 --- a/404.html +++ b/404.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Error 404 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Jsem · Nudista · Online"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main page page--error"><div class="hero"><div class="main__inner"><h1><svg class="fi fi-bell" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></g></svg> Jsem · Nudista · Online</h1><p>Jsem Jan Rippl - nudista online! Vítejte! Tato část mého webu slouží jako moje pracovní nástěnka a zároveň jako má vlatní "sociální sít", jenž je umístěna na službě NoLog (Gitea) co by statické HTML stránky, spravované za pomoci publikačního systému Publii.</p><fieldset><legend><svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewbox="0 0 24 24"><path fill="currentColor" d="M16.279 11.506c.132-.016.257-.018.373 0c.066-.154.078-.419.019-.708c-.09-.429-.211-.688-.461-.646c-.251.04-.261.35-.17.779c.05.24.14.446.239.575m-2.149.339c.18.078.29.129.331.086c.029-.028.021-.084-.022-.154a1.05 1.05 0 0 0-.464-.371a1.26 1.26 0 0 0-1.228.146c-.119.088-.232.209-.218.283c.007.023.023.042.065.05c.099.011.444-.164.843-.188c.282-.02.513.068.693.148m-.361.205c-.232.037-.361.113-.443.187c-.071.062-.113.128-.113.177l.018.042l.037.014c.053 0 .171-.046.171-.046c.324-.115.539-.102.752-.078c.117.014.172.02.198-.02c.007-.012.018-.035-.007-.074c-.056-.091-.291-.24-.613-.202m1.784.756c.159.078.333.046.39-.069c.059-.115-.024-.272-.183-.349c-.158-.079-.333-.049-.39.066c-.057.115.026.274.183.352m1.018-.891c-.128-.002-.234.138-.238.316c-.003.177.1.321.229.322c.129.002.235-.139.238-.315s-.099-.32-.229-.323m-8.644 3.183c-.032-.04-.085-.029-.136-.015c-.036.007-.076.017-.119.016a.265.265 0 0 1-.221-.111c-.059-.09-.056-.225.01-.378l.03-.069c.104-.231.275-.619.082-.988a.88.88 0 0 0-.671-.488a.861.861 0 0 0-.739.267c-.284.313-.327.741-.273.893c.021.056.053.071.075.074c.048.007.119-.029.164-.15l.014-.038c.02-.064.057-.184.118-.278a.518.518 0 0 1 .717-.15c.2.131.275.375.19.608c-.044.121-.115.351-.1.54c.032.383.27.537.48.556c.206.007.35-.108.387-.193c.021-.053.003-.084-.008-.096"></path><path fill="currentColor" d="M19.821 14.397c-.009-.029-.061-.216-.13-.44l-.144-.384c.281-.423.286-.799.249-1.013a1.284 1.284 0 0 0-.372-.724c-.222-.232-.677-.472-1.315-.651l-.335-.093c-.002-.015-.018-.79-.031-1.123c-.011-.24-.031-.616-.148-.986c-.14-.502-.381-.938-.684-1.221c.835-.864 1.355-1.817 1.354-2.634c-.003-1.571-1.933-2.049-4.312-1.063l-.503.214c-.002-.002-.911-.894-.924-.905c-2.714-2.366-11.192 7.06-8.48 9.349l.593.501a2.916 2.916 0 0 0-.166 1.345c.065.631.389 1.234.915 1.701c.5.442 1.159.724 1.796.723c1.055 2.432 3.465 3.922 6.291 4.007c3.032.09 5.576-1.333 6.644-3.889c.069-.179.365-.987.365-1.7c-.001-.718-.406-1.015-.663-1.014M7.416 16.309a1.38 1.38 0 0 1-.28.021c-.916-.026-1.905-.85-2.003-1.827c-.109-1.08.443-1.912 1.421-2.108c.116-.025.258-.038.41-.031c.548.032 1.354.452 1.539 1.645c.164 1.055-.096 2.132-1.087 2.3m-1.021-4.562a2.325 2.325 0 0 0-1.473.94c-.197-.164-.562-.48-.626-.604c-.524-.994.571-2.928 1.337-4.02c1.889-2.698 4.851-4.739 6.223-4.371c.222.064.96.921.96.921s-1.37.759-2.642 1.819c-1.711 1.32-3.006 3.236-3.779 5.315m9.611 4.158a.05.05 0 0 0 .03-.054a.05.05 0 0 0-.056-.045s-1.434.212-2.789-.283c.147-.479.541-.308 1.134-.259a8.287 8.287 0 0 0 2.735-.296c.613-.177 1.419-.524 2.045-1.018c.212.465.286.975.286.975s.163-.029.3.055c.13.08.224.245.16.671c-.133.798-.471 1.445-1.042 2.041a4.259 4.259 0 0 1-1.249.934a5.337 5.337 0 0 1-.814.346c-2.149.701-4.349-.07-5.058-1.727a2.761 2.761 0 0 1-.142-.392c-.302-1.092-.046-2.4.755-3.226v-.001c.051-.052.102-.113.102-.191c0-.064-.042-.133-.077-.183c-.28-.406-1.253-1.099-1.057-2.44c.139-.964.982-1.642 1.768-1.602l.2.012c.34.02.637.063.917.076c.47.019.891-.049 1.391-.465c.169-.142.304-.263.532-.301c.024-.006.084-.025.203-.021a.681.681 0 0 1 .343.109c.4.266.457.912.479 1.385c.012.269.045.922.055 1.108c.026.428.139.489.365.563c.129.044.248.074.423.125c.529.147.845.3 1.043.493a.637.637 0 0 1 .188.372c.065.457-.353 1.021-1.455 1.533c-1.206.559-2.669.701-3.679.588l-.354-.04c-.81-.108-1.269.936-.784 1.651c.313.461 1.164.761 2.017.761c1.953.002 3.455-.832 4.015-1.554l.044-.063c.026-.042.005-.063-.03-.041c-.455.312-2.483 1.552-4.651 1.18c0 0-.264-.044-.504-.138c-.19-.072-.591-.258-.639-.668c1.747.543 2.85.031 2.85.03m-6.12-7.852c.672-.776 1.499-1.452 2.241-1.83c.025-.014.052.015.038.038a2.125 2.125 0 0 0-.208.508c-.006.027.023.049.046.032c.462-.314 1.264-.651 1.968-.693a.03.03 0 0 1 .021.055a1.66 1.66 0 0 0-.31.311c-.014.02-.001.049.024.049c.494.003 1.191.175 1.644.43c.03.018.008.077-.025.069c-.688-.157-1.811-.277-2.979.008c-1.044.254-1.84.646-2.419 1.069c-.03.02-.065-.019-.041-.046"></path></svg> Služba Mailchimp</legend><form id="form" class="validate" action="https://online.us2.list-manage.com/subscribe/post?u=2ac5382353059a705b15ea6ab&id=9189628091&f_id=00924ae0f0" method="post" target="_self"><label>E-mail pro zasílání novinek:</label><br><input id="mce-EMAIL" name="EMAIL" required="" size="40%" type="email" placeholder="Váš e-mail"><div style="position: absolute; left: -5000px;" aria-hidden="true"><input tabindex="-1" name="b_2ac5382353059a705b15ea6ab_9189628091" type="text"></div> <input id="mc-embedded-subscribe" name="subscribe" type="submit" value="Přihlásit"></form><p><br><a href="/ochrana-soukromi/">· Soukromí</a> <a href="#">· Kontakt</a> <a href="/rubriky/politiky/">· Politiky</a></p></fieldset><fieldset><legend><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Veřejné kalendáře (Google)</legend><small>Všechny mé kalendáře se dělí na veřejné / neveřejné a externí / interní.</small><div id="calendar"></div></fieldset></div></div><div class="main__inner"><h1>404 Error</h1><p>The page you were looking for appears to have been moved, deleted or does not exist. You could go back to where you were or head straight to our home page.</p><p><a href="https://jsem.nudista.online/" class="btn">Jít zpět na úvod!</a></p></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main page page--error"><div class="hero"><div class="main__inner"><h1><svg class="fi fi-bell" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></g></svg> Jsem · Nudista · Online</h1><p>Jsem Jan Rippl - nudista online! Vítejte! Tato část mého webu slouží jako moje pracovní nástěnka a zároveň jako má vlatní "sociální sít", jenž je umístěna na službě NoLog (Gitea) co by statické HTML stránky, spravované za pomoci publikačního systému Publii.</p><fieldset><legend><svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewbox="0 0 24 24"><path fill="currentColor" d="M16.279 11.506c.132-.016.257-.018.373 0c.066-.154.078-.419.019-.708c-.09-.429-.211-.688-.461-.646c-.251.04-.261.35-.17.779c.05.24.14.446.239.575m-2.149.339c.18.078.29.129.331.086c.029-.028.021-.084-.022-.154a1.05 1.05 0 0 0-.464-.371a1.26 1.26 0 0 0-1.228.146c-.119.088-.232.209-.218.283c.007.023.023.042.065.05c.099.011.444-.164.843-.188c.282-.02.513.068.693.148m-.361.205c-.232.037-.361.113-.443.187c-.071.062-.113.128-.113.177l.018.042l.037.014c.053 0 .171-.046.171-.046c.324-.115.539-.102.752-.078c.117.014.172.02.198-.02c.007-.012.018-.035-.007-.074c-.056-.091-.291-.24-.613-.202m1.784.756c.159.078.333.046.39-.069c.059-.115-.024-.272-.183-.349c-.158-.079-.333-.049-.39.066c-.057.115.026.274.183.352m1.018-.891c-.128-.002-.234.138-.238.316c-.003.177.1.321.229.322c.129.002.235-.139.238-.315s-.099-.32-.229-.323m-8.644 3.183c-.032-.04-.085-.029-.136-.015c-.036.007-.076.017-.119.016a.265.265 0 0 1-.221-.111c-.059-.09-.056-.225.01-.378l.03-.069c.104-.231.275-.619.082-.988a.88.88 0 0 0-.671-.488a.861.861 0 0 0-.739.267c-.284.313-.327.741-.273.893c.021.056.053.071.075.074c.048.007.119-.029.164-.15l.014-.038c.02-.064.057-.184.118-.278a.518.518 0 0 1 .717-.15c.2.131.275.375.19.608c-.044.121-.115.351-.1.54c.032.383.27.537.48.556c.206.007.35-.108.387-.193c.021-.053.003-.084-.008-.096"></path><path fill="currentColor" d="M19.821 14.397c-.009-.029-.061-.216-.13-.44l-.144-.384c.281-.423.286-.799.249-1.013a1.284 1.284 0 0 0-.372-.724c-.222-.232-.677-.472-1.315-.651l-.335-.093c-.002-.015-.018-.79-.031-1.123c-.011-.24-.031-.616-.148-.986c-.14-.502-.381-.938-.684-1.221c.835-.864 1.355-1.817 1.354-2.634c-.003-1.571-1.933-2.049-4.312-1.063l-.503.214c-.002-.002-.911-.894-.924-.905c-2.714-2.366-11.192 7.06-8.48 9.349l.593.501a2.916 2.916 0 0 0-.166 1.345c.065.631.389 1.234.915 1.701c.5.442 1.159.724 1.796.723c1.055 2.432 3.465 3.922 6.291 4.007c3.032.09 5.576-1.333 6.644-3.889c.069-.179.365-.987.365-1.7c-.001-.718-.406-1.015-.663-1.014M7.416 16.309a1.38 1.38 0 0 1-.28.021c-.916-.026-1.905-.85-2.003-1.827c-.109-1.08.443-1.912 1.421-2.108c.116-.025.258-.038.41-.031c.548.032 1.354.452 1.539 1.645c.164 1.055-.096 2.132-1.087 2.3m-1.021-4.562a2.325 2.325 0 0 0-1.473.94c-.197-.164-.562-.48-.626-.604c-.524-.994.571-2.928 1.337-4.02c1.889-2.698 4.851-4.739 6.223-4.371c.222.064.96.921.96.921s-1.37.759-2.642 1.819c-1.711 1.32-3.006 3.236-3.779 5.315m9.611 4.158a.05.05 0 0 0 .03-.054a.05.05 0 0 0-.056-.045s-1.434.212-2.789-.283c.147-.479.541-.308 1.134-.259a8.287 8.287 0 0 0 2.735-.296c.613-.177 1.419-.524 2.045-1.018c.212.465.286.975.286.975s.163-.029.3.055c.13.08.224.245.16.671c-.133.798-.471 1.445-1.042 2.041a4.259 4.259 0 0 1-1.249.934a5.337 5.337 0 0 1-.814.346c-2.149.701-4.349-.07-5.058-1.727a2.761 2.761 0 0 1-.142-.392c-.302-1.092-.046-2.4.755-3.226v-.001c.051-.052.102-.113.102-.191c0-.064-.042-.133-.077-.183c-.28-.406-1.253-1.099-1.057-2.44c.139-.964.982-1.642 1.768-1.602l.2.012c.34.02.637.063.917.076c.47.019.891-.049 1.391-.465c.169-.142.304-.263.532-.301c.024-.006.084-.025.203-.021a.681.681 0 0 1 .343.109c.4.266.457.912.479 1.385c.012.269.045.922.055 1.108c.026.428.139.489.365.563c.129.044.248.074.423.125c.529.147.845.3 1.043.493a.637.637 0 0 1 .188.372c.065.457-.353 1.021-1.455 1.533c-1.206.559-2.669.701-3.679.588l-.354-.04c-.81-.108-1.269.936-.784 1.651c.313.461 1.164.761 2.017.761c1.953.002 3.455-.832 4.015-1.554l.044-.063c.026-.042.005-.063-.03-.041c-.455.312-2.483 1.552-4.651 1.18c0 0-.264-.044-.504-.138c-.19-.072-.591-.258-.639-.668c1.747.543 2.85.031 2.85.03m-6.12-7.852c.672-.776 1.499-1.452 2.241-1.83c.025-.014.052.015.038.038a2.125 2.125 0 0 0-.208.508c-.006.027.023.049.046.032c.462-.314 1.264-.651 1.968-.693a.03.03 0 0 1 .021.055a1.66 1.66 0 0 0-.31.311c-.014.02-.001.049.024.049c.494.003 1.191.175 1.644.43c.03.018.008.077-.025.069c-.688-.157-1.811-.277-2.979.008c-1.044.254-1.84.646-2.419 1.069c-.03.02-.065-.019-.041-.046"></path></svg> Služba Mailchimp</legend><form id="form" class="validate" action="https://online.us2.list-manage.com/subscribe/post?u=2ac5382353059a705b15ea6ab&id=9189628091&f_id=00924ae0f0" method="post" target="_self"><label>E-mail pro zasílání novinek:</label><br><input id="mce-EMAIL" name="EMAIL" required="" size="40%" type="email" placeholder="Váš e-mail"><div style="position: absolute; left: -5000px;" aria-hidden="true"><input tabindex="-1" name="b_2ac5382353059a705b15ea6ab_9189628091" type="text"></div> <input id="mc-embedded-subscribe" name="subscribe" type="submit" value="Přihlásit"></form><p><br><a href="/ochrana-soukromi/">· Soukromí</a> <a href="#">· Kontakt</a> <a href="/rubriky/politiky/">· Politiky</a></p></fieldset><fieldset><legend><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Veřejné kalendáře (Google)</legend><small>Všechny mé kalendáře se dělí na veřejné / neveřejné a externí / interní.</small><div id="calendar"></div></fieldset></div></div><div class="main__inner"><h1>404 Error</h1><p>The page you were looking for appears to have been moved, deleted or does not exist. You could go back to where you were or head straight to our home page.</p><p><a href="https://jsem.nudista.online/" class="btn">Jít zpět na úvod!</a></p></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/autor/jan-rippl-cz/index.html b/autor/jan-rippl-cz/index.html index 678cc3d..e5e9798 100644 --- a/autor/jan-rippl-cz/index.html +++ b/autor/jan-rippl-cz/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Author: CZ 🇨🇿 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="CZ 🇨🇿"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/autor/jan-rippl-cz/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li class="active"><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="eager" height="240" width="240" alt="CZ 🇨🇿" class="page--author__avatar"><div class="page--author__content"><h1>CZ 🇨🇿 <sup>(2)</sup></h1><p>Aktivity související s Českou republikou, Karlovarským krajem, Chebským okresem, a regionem Ašska (Město Aš a okolí).</p><p class="page--author__website"><a href="https://janrippl.cz" class="tltp" target="_blank" rel="nofollow noreferrer noopener"><svg height="20" width="20" aria-hidden="true" aria-hidden="true"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#website"/></svg> <span>Navštivte můj web!</span></a></p></div></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-29T12:32">úno 29, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/projekt/">Projekt</a></h2><p>Nějaký perex</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/akce/" class="c-card-tag">🗓️ Akce</a></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li class="active"><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="eager" height="240" width="240" alt="CZ 🇨🇿" class="page--author__avatar"><div class="page--author__content"><h1>CZ 🇨🇿 <sup>(3)</sup></h1><p>Aktivity související s Českou republikou, Karlovarským krajem, Chebským okresem, a regionem Ašska (Město Aš a okolí).</p><p class="page--author__website"><a href="https://janrippl.cz" class="tltp" target="_blank" rel="nofollow noreferrer noopener"><svg height="20" width="20" aria-hidden="true" aria-hidden="true"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#website"/></svg> <span>Navštivte můj web!</span></a></p></div></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-03-05T22:13">bře 5, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/pixelfedcz/">Pixelfed.cz</a></h2><p>Registrace dnes 5. března 2024 ve 20:59...</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-29T12:32">úno 29, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/projekt/">Projekt</a></h2><p>Nějaký perex</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/akce/" class="c-card-tag">🗓️ Akce</a></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/autor/jan-rippl-de/index.html b/autor/jan-rippl-de/index.html index 9f17090..5ae5079 100644 --- a/autor/jan-rippl-de/index.html +++ b/autor/jan-rippl-de/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Author: SRN 🇩🇪 - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="SRN 🇩🇪"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/autor/jan-rippl-de/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="eager" height="240" width="240" alt="SRN 🇩🇪" class="page--author__avatar"><div class="page--author__content"><h1>SRN 🇩🇪 <sup>(2)</sup></h1><p class="page--author__website"><a href="https://janrippl.cz" class="tltp" target="_blank" rel="nofollow noreferrer noopener"><svg height="20" width="20" aria-hidden="true" aria-hidden="true"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#website"/></svg> <span>Navštivte můj web!</span></a></p></div></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="eager" height="240" width="240" alt="SRN 🇩🇪" class="page--author__avatar"><div class="page--author__content"><h1>SRN 🇩🇪 <sup>(2)</sup></h1><p class="page--author__website"><a href="https://janrippl.cz" class="tltp" target="_blank" rel="nofollow noreferrer noopener"><svg height="20" width="20" aria-hidden="true" aria-hidden="true"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#website"/></svg> <span>Navštivte můj web!</span></a></p></div></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/feed.json b/feed.json index d9f9f99..26c673c 100644 --- a/feed.json +++ b/feed.json @@ -10,6 +10,20 @@ "name": "CZ 🇨🇿" }, "items": [ + { + "id": "https://jsem.nudista.online/pixelfedcz/", + "url": "https://jsem.nudista.online/pixelfedcz/", + "title": "Pixelfed.cz", + "summary": "<p>Registrace dnes 5. března 2024 ve 20:59...</p>\n", + "content_html": "<p>Registrace dnes 5. března 2024 ve 20:59...</p>\n\n<p> </p>", + "author": { + "name": "CZ 🇨🇿" + }, + "tags": [ + ], + "date_published": "2024-03-05T22:13:11+01:00", + "date_modified": "2024-03-05T22:13:11+01:00" + }, { "id": "https://jsem.nudista.online/projekt/", "url": "https://jsem.nudista.online/projekt/", diff --git a/feed.xml b/feed.xml index 0a35c74..21acc94 100644 --- a/feed.xml +++ b/feed.xml @@ -3,12 +3,35 @@ <title>NoLogWeb</title> <link href="https://jsem.nudista.online/feed.xml" rel="self" /> <link href="https://jsem.nudista.online" /> - <updated>2024-03-02T15:25:44+01:00</updated> + <updated>2024-03-05T22:13:11+01:00</updated> <author> <name>CZ 🇨🇿</name> </author> <id>https://jsem.nudista.online</id> + <entry> + <title>Pixelfed.cz</title> + <author> + <name>CZ 🇨🇿</name> + </author> + <link href="https://jsem.nudista.online/pixelfedcz/"/> + <id>https://jsem.nudista.online/pixelfedcz/</id> + + <updated>2024-03-05T22:13:11+01:00</updated> + <summary> + <![CDATA[ + <p>Registrace dnes 5. března 2024 ve 20:59...</p> + + ]]> + </summary> + <content type="html"> + <![CDATA[ + <p>Registrace dnes 5. března 2024 ve 20:59...</p> + +<p> </p> + ]]> + </content> + </entry> <entry> <title>Projekt</title> <author> diff --git a/fullcalendar-main/.editorconfig b/fullcalendar-main/.editorconfig new file mode 100644 index 0000000..7c33ec6 --- /dev/null +++ b/fullcalendar-main/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +quote_type = single diff --git a/fullcalendar-main/.github/ISSUE_TEMPLATE/bug-report.yml b/fullcalendar-main/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..bb4c17c --- /dev/null +++ b/fullcalendar-main/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,49 @@ +name: Bug Report +description: Report something that doesn't work correctly +body: + - type: input + id: reduced-test-case + attributes: + label: Reduced Test Case + description: > + A [reduced test case](https://css-tricks.com/reduced-test-cases/) is required. + A [debugging template](https://fullcalendar.io/reduced-test-cases) will help you get started. + placeholder: URL of reduced test case + validations: + required: true + - type: checkboxes + id: reduced-test-case-confirmation + attributes: + label: > + Do you understand that if a reduced test case is not provided, + we will intentionally delay triaging of your ticket? + options: + - label: I understand + required: true + - type: dropdown + id: connector + attributes: + label: Which connector are you using (React/Angular/etc)? + options: + - No connector (vanilla JS) + - React + - Angular + - Vue + validations: + required: true + - type: textarea + id: description + attributes: + label: Bug Description + description: > + Describe how to recreate the bug. + What do you expect to happen? + What happens instead? + validations: + required: true + - type: markdown + id: screenshot + attributes: + value: > + **Screenshot:** + If the bug is visual, drag a screenshot into the description above. diff --git a/fullcalendar-main/.github/ISSUE_TEMPLATE/config.yml b/fullcalendar-main/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..58521fd --- /dev/null +++ b/fullcalendar-main/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help + url: https://stackoverflow.com/questions/tagged/fullcalendar + about: Stuck on a tough problem? Visit the StackOverflow fullcalendar tags diff --git a/fullcalendar-main/.github/ISSUE_TEMPLATE/feature-request.yml b/fullcalendar-main/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..250ce10 --- /dev/null +++ b/fullcalendar-main/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,37 @@ +name: Feature Request +description: Suggest an idea you want implemented +body: + - type: checkboxes + id: confirmations + attributes: + label: Checklist + options: + - label: I've already searched through [existing tickets](https://github.com/fullcalendar/fullcalendar/issues) + required: true + - label: Other people will find this feature useful + required: true + - type: dropdown + id: connector + attributes: + label: Is this feature for a specific connector (React/Angular/etc)? + options: + - No connector in particular + - React + - Angular + - Vue + - Other + validations: + required: true + - type: textarea + id: description + attributes: + label: Feature Description + description: Please describe what this feature will do. + validations: + required: true + - type: markdown + id: mockup + attributes: + value: > + **Visual Mockup:** + If you are requesting a new UI, drag some sort of mockup or screenshot into the area above. diff --git a/fullcalendar-main/.github/workflows/ci.yml b/fullcalendar-main/.github/workflows/ci.yml new file mode 100644 index 0000000..c47d7c4 --- /dev/null +++ b/fullcalendar-main/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main +env: + TZ: "America/New_York" + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PNPM + uses: pnpm/action-setup@v2.2.4 + with: + version: 8.6.3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.13.0 + cache: 'pnpm' + + - name: Setup Turbo cache + uses: actions/cache@v3 + with: + path: node_modules/.cache/turbo + key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: turbo-${{ github.job }}-${{ github.ref_name }}- + + - name: Install dependencies + run: pnpm install + + - name: Lint + run: pnpm run lint + + - name: Build + run: pnpm run build + + # - name: Test + # run: pnpm run test diff --git a/fullcalendar-main/.gitignore b/fullcalendar-main/.gitignore new file mode 100644 index 0000000..16c98ac --- /dev/null +++ b/fullcalendar-main/.gitignore @@ -0,0 +1,11 @@ + +# Package manager +node_modules + +# Generated +tsconfig.json +tsconfig.tsbuildinfo +dist + +# Monorepo +.turbo diff --git a/fullcalendar-main/.npmrc b/fullcalendar-main/.npmrc new file mode 100644 index 0000000..2161781 --- /dev/null +++ b/fullcalendar-main/.npmrc @@ -0,0 +1,7 @@ +engine-strict = true +use-node-version = 18.13.0 +prefer-workspace-packages = true + +# disable so that filtered install can be used. bug: +# https://github.com/pnpm/pnpm/issues/6300 +dedupe-peer-dependents = false diff --git a/fullcalendar-main/.vscode/extensions.json b/fullcalendar-main/.vscode/extensions.json new file mode 100644 index 0000000..b308e58 --- /dev/null +++ b/fullcalendar-main/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} diff --git a/fullcalendar-main/.vscode/settings.json b/fullcalendar-main/.vscode/settings.json new file mode 100644 index 0000000..246e9c6 --- /dev/null +++ b/fullcalendar-main/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/fullcalendar-main/CHANGELOG.md b/fullcalendar-main/CHANGELOG.md new file mode 100644 index 0000000..a4f8a0c --- /dev/null +++ b/fullcalendar-main/CHANGELOG.md @@ -0,0 +1,2004 @@ +## 6.1.11 (2024-02-19) + +### General + +- fix: fc-event-past className not attached to events that end midnight before today (#6120, #6486) +- fix: aria-labelledby on view container should not exist when headerToolbar:false (#6884) + +### DayGrid and MultiMonth + +- fix: possible infinite recursion with dayGrid, dayMaxEventRows, and many hidden event rows (#7462) +- fix: incorrectly put events under +more link (#7002, #6608, #6900) + +### React + +- fix: events incorrectly positioned w/ React strict-mode (#7574, #7389, #7400) + +### Angular + +- fix: Angular 17 SRR error with el.getRootNode (#7550) + +### React/Vue/Angular Connectors and Adaptive plugin + +- fix: eventContent custom content not rendering within print-mode (#7419) + +## 6.1.10 (2023-11-28) + +- feature: Angular version 17 support (#7525) +- fix: Vue 3 background event with custom rendering, not receiving el in eventDidMount (#7524) +- fix: font-icon elements should have role="img" (#7501) +- locale: fix bg (#7493) +- locale: fix ca (#7394) +- locale: fix nl (#7471) + +## 6.1.9 (2023-09-21) + +- fix: Table selection is not prevented when long pressing to drag events in Safari (#7441) +- fix: Custom event rendering with `white-space:normal` can causes infinite loop (#7447) +- fix: `eventClick` does not fire for allDay events with async provided resources (#7365) +- fix: `eventContent` with Preact nodes (via `createElement`) not rendering (#7342) +- fix: React 16: calling calendarApi methods within useEffect causes fatal error (#7448) +- fix: Angular/Vue2: dot-event element from `eventDidMount` does not exists in the DOM (#7191) +- fix: Angular Universal: `document` is not defined error (#7352) + +## 6.1.8 (2023-05-24) + +- feature: Luxon 3 plugin (#6957) +- feature: Angular 16 support (#7312) +- fix: React warning with JSX "flushSync was called from inside a lifecycle method" (#7334) +- fix: styling sometimes broken in production Next.js (#7284) +- fix: styling broken in Remix, use official workaround (#7261) +- fix: for React custom views, alias `component` setting to `content` (#7207) +- locale fix: he (#7124) +- locale fix: zh-tw (#7289) + +## 6.1.7 (2023-05-08) + +- fix: React 18 flickering while rendering event-mirror during drag/resize/select (#7165, #7234) +- fix: React & Vue3: unnecessary calls to `eventContent` for event-mirror during drag/resize/select + +## 6.1.6 (2023-04-23) + +- fix: timeZone change (w/ tz plugin) not updating recurring event times (#5273) +- fix: timeZone change (w/ tz plugin) not rerendering timed events +- fix: rrule package breaks when imported via cjs, like in Next.js (#7260) + +## 6.1.5 (2023-03-21) + +- fix: inject static runtime stylesheets near top of head, avoid CSS precedence problems (#7220) +- fix: prevent unnecessary reflows during clicking (potentially solves #7209) +- fix: RRule events w/ wrong dates after being dynamically updated (#7230) +- fix: incorrect calendar dimensions when first rendered in Ionic-Angular (#4976) +- fix: dayGrid timed events w/ custom eventContent fire eventDidMount w/ stale element (#7191) +- fix: resource-timeline crashes when resourceGroupLaneContent is set (#7203) +- fix: buggy dragging of timed event from timegrid more-popover to all-day slot (#7222) +- fix: timeline slots do not fill print version if initially scrolled (#6859) +- fix: Resource::getParent() returns undefined (#7023) +- fix: Preact breaking .d.ts changes, using more specific semver range (#7225) +- fix: broken daygrid-related styles in timegrid/multimonth if daygrid not loaded (#7238) +- fix: support Vue dash-name slot names, for use with script tags (#7078) +- fix: Nuxt 2 error: Cannot read properties of undefined (reading 'isHiddenDay') (#7217) +- fix: Vue 2 SSR broken + +## 6.1.4 (2023-02-07) + +- fix: bug introduced in v6.1.3 where month-start-text appears within day cells of multimonth view + +## 6.1.3 (2023-02-07) + +- fix: React: finally fix root cause of state issues (#7066, #7067, #7071) +- fix: Angular: NgClass can only toggle CSS classes expressed as strings (#7182) +- fix: Angular/Vue: accept content-injection function w/ { html } or { domNodes } (#7188) +- fix: monthStartFormat not working with dayGrid views having a custom duration (#7197) + +## 6.1.2 (2023-01-31) + +Apply v6.1.1's new CJS/ESM/nested-import interop strategy to React/Vue connectors. Details: + +- For maximum compatibility with legacy build systems like create-react-app +- Only affects React/Vue2/Vue3 connectors. Assets for standard/premium not generated + +## 6.1.1 (2023-01-30) + +- fix: Multi-Month not included in fullcalendar-scheduler (#7177) +- fix: Multi-Month has nonexistent 'internal' entrypoint, causing error for skypack (#7176) +- fix: Vue connector should not error-out when given content-injector functions (#7175) +- fix: continued CJS/ESM confusion with certain build tools (#7170, #7113, #7143) + +## 6.1.0 (2023-01-29) + +- feature: multimonth view (#470, #1140) + - provides `multiMonthYear` view, which displays 3x4 small months when space permits + - can extend `multiMonth` view with custom durations + - can specify `multiMonthMinWidth`, which will force wrapping if months are too small + - can specify `multiMonthMaxColumns: 1` to guarantee one column of months + - can specify `multiMonthTitleFormat` to customize text above each month +- feature: improved daygrid behavior when multiple weeks/months + - when many rows, instead of condensing rows, guaranteed min-height, for forcing scrollbars + - displaying month names for month switchovers, controlled by `monthStartFormat` + - a new stock `dayGridYear` view +- feature: in daygrid/multimonth, +more link has hover effect with grey background color +- feature: `@fullcalendar/web-component` has `shadow` option +- feature/fix: from content-injection hooks, returning `true` does default rendering (#7110) +- feature/fix: CSP nonce value passed to dynamically-injected stylesheets (#7104) +- fix: React event duplication while DnD and updating redux store (#7066, #7067, #7071) +- fix: styles are not applied correctly for elements using shadow DOM (#7118) +- fix: custom view `content` with JSX doesn't render (#7160) +- fix: `resourceAreaHeaderContent` overrides `headerContent` in resource columns (#7153) +- fix: update moment-timezone for latest tz data (#6987) +- fix: dayGrid w/ `showNonCurrentDates: false` can have final squished row (#7162) +- fix: root-level repo with git submodules shouldn't force ssh protocol (#6714) +- fix: in `fullcalendar` bundle, `FullCalendar.Preact` exposed for interop with plugins +- fix: EventApi::toPlainObject now returns `allDay` property +- locale: add weekTextLong for French locale (#6731, #7144) +- locale: replace "évènement" in French locales (#7108) +- locale: add Uzbek cyrillic translation (#6853) +- locale: update Galician locale (#7103) +- locale: update pt-br locale (#7106) + +## 6.0.4 (2023-01-13) + +React: + +- FIX: Remove need to import `react-dom/test-utils` for `act()` (#7140, #7141) + +## 6.0.3 (2023-01-11) + +Standard/premium + +- FIX: Time grid and timeline more-events link positioned incorrectly (#7134, #7115) +- FIX: file extensions of CJS/ESM dist files changed to support Jest (#7113) + +React: + +- FIX: Maximum update depth exceeded w/ eventContent & dayMaxEvents (#7116) +- FIX: Certain cases of broken rendering w/ React 17 and content-injection (#7127, #7131) +- FIX: Content-injection not using updated function closures for rendering (#7119) + +Vue 3: + +- FIX: With Webpack, fullySpecified:false workaround no longer needed (#7125, #7114) + +## 6.0.2 (2022-12-27) + +Standard/premium: + +- FIX: unable to resize an event smaller after initial resize (#7099) + +React: + +- FIX: re-rendering loop error with navLink and dayCellContent (#7107) + +Angular: + +- FIX: resource content-injection, when resource element destroyed, throws JS error (#7105) + +Vue 2: + +- FIX: resource content-injection, when resource element destroyed, throws JS error + +## 6.0.1 (2022-12-20) + +Standard/premium: + +- FIX: Property `type` does not exist on type `ViewApi` (#7056) +- FIX: Expose `globalLocales` publicly for importing on-demand (#7057) + +React: + +- FIX: multi-day events rendered by eventContent are overlapping each other (#7089) + +Angular: + +- FIX: error with eventContent & list view (#7058) + +## 6.0.0 (2022-12-13) + +[V6 Release Notes and Upgrade Guide](https://fullcalendar.io/docs/v6/upgrading-from-v5) + +Changes since final beta: + +- FIX: certain ng-template names don't work ([angular-426]) +- FIX: minify CSS that is embedded into JS files +- FIX: more informational README files in published packages +- FIX: daygrid events sometimes not correctly positioned with Vue connectors +- BREAKING: @fullcalendar/icalendar now has ical.js peer dependency + +[angular-426]: https://github.com/fullcalendar/fullcalendar-angular/issues/426 + +## 6.0.0-beta.4 (2022-12-07) + +Standard/Premium: + +- FIX: jsDelivr default URLs have wrong mime type (#7045) +- FIX: Unmet peer dependency "moment" warning from moment-timezone (#6839) +- FIX: fullcalendar and fullcalendar-scheduler packages accidentally include sourcemaps + +Angular: + +- FIX: BrowserModule incompatible with lazy-loaded module ([angular-423]) +- FIX: Inputs should accept undefined/null for compatibility with async ([angular-424]) +- FIX: content-injections bugs with drag-n-drop and rerendering + +Vue: + +- FIX: Remove global js 'default' from export maps (#7047) +- FIX: content-injections bugs with drag-n-drop and rerendering + +React: + +- FIX: Remove global js 'default' from export maps (#7047) + +[angular-423]: https://github.com/fullcalendar/fullcalendar-angular/issues/423 +[angular-424]: https://github.com/fullcalendar/fullcalendar-angular/issues/424 + +## 6.0.0-beta.3 (2022-12-01) + +Bugfixes: + +- Wrong typing for events function and errorCallback (#7039) +- Error with global bundle and individual global locales (#7033) +- Fix package.json lint warnings (#7038) +- Fixes in React/Angular connectors (see individual changelogs) + +## 6.0.0-beta.2 (2022-11-22) + +See https://fullcalendar.io/docs/v6/upgrading-from-v5 + +## 5.11.3 (2022-08-23) + +- fixed: timeline view (without resources) problem with expanding height (#5792) +- fixed: locales not working in IE11 (#6790) + +## 6.0.0-beta.1 (2022-08-03) + +FullCalendar no longer attempts to import .css files. Instead, FullCalendar's JS is responsible for +injecting its own CSS. This solves many issues with third party libraries: + +- _Webpack_: no longer necessary to use css-loader + (see [example project][webpack-css-hack]) +- _Rollup_: no longer necessary to use a css-processing plugin (like postcss) + (see [example project][rollup-css-hack]) +- _NextJS_: no longer necessary to ignore and manually import .css files + (see [example project][next-css-hack], #6674) +- _Angular 14_ is incompatible with FullCalendar v5 ([see ticket][angular-css-bug]). FullCalendar v6 + restores support for Angular 14 and above, but does so via a completely different package. Please + use the new FullCalendar Web Component package (`@fullcalendar/web-component`), which can + integrate with Angular via the [method described here][angular-web-components]. + +[webpack-css-hack]: https://github.com/fullcalendar/fullcalendar-examples/blob/10fe58abfc94457c7582af3948b3764cd17e7960/webpack/webpack.config.js +[rollup-css-hack]: https://github.com/fullcalendar/fullcalendar-examples/blob/10fe58abfc94457c7582af3948b3764cd17e7960/rollup/rollup.config.js +[next-css-hack]: https://github.com/fullcalendar/fullcalendar-examples/tree/10fe58abfc94457c7582af3948b3764cd17e7960/next +[angular-css-bug]: https://github.com/fullcalendar/fullcalendar-angular/issues/403 +[angular-web-components]: https://coryrylan.com/blog/using-web-components-in-angular + +## 5.11.2 (2022-07-26) + +- fixed: React Strict Mode, dateSet, and "Maximum update depth exceeded error" (#5935, [react-185]) +- fixed: React Strict Mode, timeline scrolling not synced ([react-192]) +- fixed: React, datesSet with object-like dateIncrement, "Maximum update depth..." ([react-131]) + +[react-185]: https://github.com/fullcalendar/fullcalendar-react/issues/185 +[react-192]: https://github.com/fullcalendar/fullcalendar-react/issues/192 +[react-131]: https://github.com/fullcalendar/fullcalendar-react/issues/131 + +## 5.11.1 (_see dates in tickets_) + +- react fix: restore accidentally-removed support for React 17 ([react-182]) +- vue3 fix: Cannot target calendar api with several instances ([vue-155]) + +[react-182]: https://github.com/fullcalendar/fullcalendar-react/issues/182 +[vue-155]: https://github.com/fullcalendar/fullcalendar-vue/issues/155 + +## 5.11.0 (2022-04-08) + +- internal changes for compatibility with React 18 + +## 5.10.2 (2022-02-09) + +- bootstrap 5 support, via `@fullcalendar/bootstrap5` package (#6299) +- luxon 2 support, via `@fullcalendar/luxon2` package (#6502) +- angular 13 support ([ang-387][ang-387]) + +[ang-387]: https://github.com/fullcalendar/fullcalendar-angular/issues/387 + +## 5.10.1 (2021-11-02) + +- locale strings for the recent WAI-ARIA improvements: + - nb (#6610) + - de (#6597) + - sv (#6592) + +## 5.10.0 (2021-10-13) + +- feature: WAI-ARIA improvements: + - toolbar (#6521) + - human-readable `title` attributes on all buttons. new options: + - `buttonHints` + - `customButtons.hint` + - `viewHint` (ex: `$0 view` -> `"month view"`) + - `aria-labelledby` attribute connecting view-title with view-container + - event elements (#3364) + - previously, only events with an `event.url` property were tabbable by the end-user. + now, events _without_ urls can be made tabbable by enabling `event.interactive` or by + enabling the calendar-wide `eventInteractive` option. + - when focused, pressing enter/spacebar will trigger an `eventClick` + - more-links and popover (#6523) + - human-readable `title` attributes on "+more" links via new option `moreLinkHint` + - when focused, pressing enter/spacebar will open popover + - `aria-controls`/`aria-expanded` attributes connecting link to popover + - `aria-labelledby` attribute connecting popover-title to popover + - `aria-label` attribute describing "X" close icon via new option `closeHint` + - pressing escape key closes popover + - nav-links (#6524) + - human-readable `title` attributes on all navLinks via new option `navLinkHint` + - when focused, pressing enter/spacebar will trigger `navLinkClick` + - table-based views (#6526) + - all cells within thead elements have been made into `<th>` tags + - retrofit the necessarily non-ARIA-friendly table markup with `role` tags. the root table is a + `grid`, children have been given `rowgroup`/`row`/`columnheader`/`rowheader`/`cell`, and + non-functional table elements have been given `presentation`. + - in timegrid views, the time-axis axis has been removed from the accessibility tree + - list-view (#6525) + - introduced a table-header specifically for screen readers. header cells label the time/event + columns using the following new options: `timeHint` and `eventHint` + - removed the "dot" column from the accessibility tree +- feature: date formatting option `week` now accepts `'long'` if locale defines `weekTextLong` +- bugfix: timeline-view events hidden by `eventMaxStack` sometimes appear over other events (#6543) +- bugfix: daygrid event rendering with `dayMaxEventRows` and custom `eventOrder` can cause infinite loop (#6573) +- bugfix: content-injected html/domNodes as view-specific options don't clear when switching views (#6079, #6555) +- bugfix: more compliant CSS with Sass processors (#6564) +- locale: added si-lk (#6553) + +HELP WANTED populating new options in locales (examples: [es][es-aria-example], [en-GB][en-aria-example]) + +- `buttonHints` +- `viewHint` +- `weekTextLong` +- `moreLinkHint` +- `navLinkHint` +- `closeHint` +- `timeHint` +- `eventHint` + +[es-aria-example]: https://github.com/fullcalendar/fullcalendar/commit/63cd61bd89ae56642e76e3ea8b3a44cbd3fe2555 +[en-aria-example]: https://github.com/fullcalendar/fullcalendar/commit/d8e33a04ecc9bd8dd54f1d2c39aaa7ed919f896c + +## 5.9.0 (2021-07-28) + +- fix: dayGrid events sometimes overlap when eventOrderStrict:true (#6393) +- fix: timeline events incorrectly positioned when uneven heights (#6395) +- fix: dayGrid events snap to top of cell while resizing (#6308) +- fix: duplicate events in dayGrid popover (#6397) +- fix: sticky elements within header of timeline views not sticking +- fix: resource-timeline views with sticky elements not working within shadow DOM (#5888) +- fix: event dragging auto-scroll does not work within shadow DOM (#6428) +- fix: cannot resize timeline events via touch within shadow DOM (#6429, #6449) +- fix: error with eventContent, domNodes, and view-specific options (#6079) +- fix: times events do not get printed in Firefox using adaptive plugin (#6438) +- fix: icalendar events with RECURRENCE-ID are displayed twice (#6451) +- fix: typing of Event::setProp does not allow boolean (#6445) +- fix: typing fix rrule's freq property (#6235) +- locale: added Samoan (#6368) +- locale: added Central Kurdish (#6400) +- locale: added Khmer (#6416) +- locale: fixed Hungarian (#6229) + +## 5.8.0 (2021-06-15) + +- fix: events not rendering in Jest environment (#6377) +- fix: prev button sometimes ineffective when dateIncrement < view's duration (#5319, #4678) +- fix: changeDate ineffective when date already in view (#4929) +- fix: upgrade tslib to guarantee \_\_spreadArray (#6376) +- fix: eventOrderStrict positioning problems (#5767) + +## 5.7.2 (2021-06-03) + +- fixed table-related Chrome 91 bug causing timegrid view with allDaySlot:false and certain + custom CSS to appear broken (#6338, #6343) + +## 5.7.1 (2021-06-02) + +- updated Angular connector to support Angular 12 ([angular-369](https://github.com/fullcalendar/fullcalendar-angular/issues/369)) +- new Vue 3 connector ([vue-131](https://github.com/fullcalendar/fullcalendar-vue/issues/131)) + +## 5.7.0 (2021-05-11) + +- feature: +more popover for timegrid (#4218) +- feature: +more popover for timeline (#4827) +- feature: eventShortHeight for timegrid +- feature: eventMinHeight for timegrid (#961) +- feature: eventMinWidth for timeline (#4823) +- feature: eventOrderStrict flag to ensure strict event ordering (#5766, #5767) +- feature: scrollTimeReset flag to not reset scroll state across dates (#6178) +- fix: events can be completely hidden behind others with custom eventOrder (#6019) +- fix: less homogeneous event widths in timegrid (#5004) +- fix: +more shows on days with less events than dayMaxEvents (#6187) +- fix: +more popover can be scrolled down with page scroll (#5532) +- fix: +more popover falls behind the sticky dates header (#5782) +- fix: all-day events are displayed in front of the sticky header (#5596) +- fix: respect duration in eventOrder as highest precedence (#5481) +- fix: refetching events should keep event popover open (#3958) +- fix: accidental +more popover close with shadow dom (#6205) +- fix: dayGrid events stretched out of cells in print media (#6300) +- dev: when attempting `npm install` in the dev repo, will throw an error saying to use yarn (#5504) +- dev: ensure building on windows works (#5366) + obscure breaking changes: +- renamed fc-timegrid-event-condensed className to fc-timegrid-event-short +- removed config.timeGridEventCondensedHeight + +## 5.6.0 (2021-03-28) + +- feature: icalendar events receive URL (#6173) +- feature: icalendar events receive location, organizer, description in extendedProps (#6097) +- fix: resizing resource column larger does not always update column widths (#6140) +- fix: print view cut off for wide liquid-width calendar (#5707) +- fix: event start time is limited by what is visible by slotMinTime (#6162) +- fix: Event::setProp can't change the id (#4730) +- fix: icalendar event source does not update on refreshEvents (#6194) +- fix: business hours per resource do not fill row height with expandRows (#6134) +- fix: icalendar recurring events ignoring count rule (#6190) +- fix: icalendar recurring timed-events with wrong times (#6139, #6106) +- fix: removed accidental ical.js dependency in common's package.json (#6171) +- fix: for gcal events, restore extendedProperties (#5083) +- fix: for gcal events, make attachments available (#5024) +- fix: can't parse rrule strings with newlines after UNTIL statements (#6126) +- locale: fixed typos in Tamil (#6115) +- locale: added Bengali (#6096) +- breaking-change: for icalendar recurring event that don't specify dtend/duration, + the resulting Event object's end is now determined by forceEventDuration, defaultTimedEventDuration, + and defaultAllDayEventDuration, whereas previously it was _sometimes_ null. + +## 5.5.1 (2021-01-16) + +- view styles lost after changing to view with allDaySlot:false, view-specific dayHeaders (#6069) +- type error when slotDuration is in whole days (#5952) +- rrule byweekday property not working (#6059) +- support for recurring events in iCalendar feed (#6068) +- add Indian/Tamil language support (#6061) +- error in @fullcalendar/scrollgrid with NextJS (SSR) (#6037) +- removed unnecessary use of Promise in icalendar package. restores IE11 compatibility + +## 5.5.0 (2020-12-19) + +- icalendar support (#1580) +- support exrule and exdate for rrule plugin (#4439) +- support for Angular 11 +- fix: recurring events missing with dtstart in UTC and timeZone not UTC (#5993) +- fix: events can have a gap between and take more rows than dayMaxEventRows when using eventOrder (#5883) +- fix: events dragged from the More popup to another resource drop on the wrong resource (#5593) +- fix: week number rendered twice in ResourceTimeGridView (#5890) +- fix: nowIndicator not positioned correctly for resourceTimelineYear view with slot duration 1 month (#5999) +- fix: oldResource and newResource missing from EventDropArg typescript definition (#6010) +- fix: loading callback fires before resources are done loading and again after (#5896) +- fix: locales are not compatible with IE 11 (#6014) +- fix: IE11 freezes trying to display dayGrid with dayMinWidth (#5971) +- fix: calling revert func within eventChange would erase affected event +- locale: add Armenian +- locale: add Austrian +- locale: add Welsh +- locale: add Esperanto +- locale: improve Dutch +- breaking-change: EventDropArg typescript type moved from interaction package to core + +## 5.4.0 (2020-11-11) + +- new fixedMirrorParent settings for drag-n-drop. workaround for #4673 +- rrule exclusion doesn't work while adding the 'Z' char for RRule datetimes (#5726) +- fix JS error when using dayMaxEventRows on small screens (#5850, #5863) +- export types for ResourceFunc and ResourceInput (#5797) +- more descriptive license key warning (#5910) +- better compatibility with Webpack 5, deeming `resolve.fullySpecified` unnecessary (#5822) +- dist files now include a CJS file. ESM is still used by default in most environments (#5929) + +## 5.3.2 (2020-09-06) + +fix: more-link sometimes incorrectly positioned behind events (#5790) + +## 5.3.1 (2020-09-04) + +bugfixes: + +- error with stickyScrollings.updateSize in certain 3rd-party environments (#5601) +- rrule exclusion doesn't work while adding the 'Z' char for RRule datetimes (#5726) +- more links sometimes hidden behind events with dayMaxEventRows (#5771) +- wrong version text in dist js files (#5778) + +## 5.3.0 (2020-08-12) + +bugfixes: + +- timelineDay with maxTime after 24:00, drag-n-drop behavior (#3900) +- Resizing on touch devices loses selection (#5706) +- Alignment of events in dayGridWeek when weekNumbers:true (#5708) +- Events are not printed in order according to their start time (#5709) +- scrollTime does not always work on prev/next (#5351) +- render method not rerendering resourceLabelContent (#5586) +- timeGrid with dayMinWidth, weekNumber cell collapses (#5684) +- fix luxon connector browser-global JS file including actual luxon lib + +## 5.2.1 (2020-07-30) + +Fixed misconfigured bundledDependencies. Only affected @fullcalendar/core NPM package. + +## 5.2.0 (2020-07-30) + +features: + +- provide browser-global JS file for all packages (#5617) +- indicate which row (or format) is being rendered in slotLabelContent (#5516) +- nepali locale (#5574) + +bugfixes: + +- compatible with server-side rendering (SSR) (#4784) +- background events don't fire eventClick in daygrid when clicked on day header (#5560) +- background events don't fire eventClick in timegrid (#5579) +- bigger touch hit area for selected list-item events in daygrid (#5635) +- CustomButtonInput click argument type is incorrect (#5432) +- parse rrule strings the same as objects (#5326) +- navLinks are not clickable if slotLabelFormat is a moment format (#5317) +- unswitch CSS variables in v-event.css (#5552) +- time slots not aligned to labels with dayMinWidth and Bootstrap theme (#5600) +- expandRows broken for time slat labels when horizontal scrolling (#5674) +- render method not rerendering resourceLabelContent (#5586) +- eventReceive/eventLeave is missing revert and relatedEvents (#5610) +- daygrid event changes between list-item and block, depending on start date (#5634) +- default scrollTime is not appropriate for month/year view (#5645) +- naturalBound is null with CSP (#5556) +- does not support Content Security Policy (CSP) nonce, only unsafe-inline css (#4317) +- RTL timeline scrolling messed up with nowIndicator (#5632) +- scrollTime does not always work when changing views (#5351) +- (p)react maximum recursion with specific resize/scrollbars (#5558, #5606) +- dayGridMonth overflows in Firefox (#5524) + +## 5.1.0 (2020-06-29) + +- fix: css variables for default event border and bg color switched (#5551) +- fix: eventContent moves arrow event length indicators (#5547) +- fix: wrong ts types for bootstrapFontAwesome settings (#5548) +- fix: Dash between event start and end times is "undefined" with + eventTimeFormat and moment plugin (#5493) +- fix: Events get displaced due to incorrect collisions detected depending on + browser, zoom level (#5549) +- fix: Resource rows are initially squished in Chome in timeline view with + contentHeight: "auto" and JSON resources (#5545) +- fix: unwanted text selection while dragging in Safari +- fix: reintroduce list-view color-change on event-row hover + +## 5.0.1 (2020-06-23) + +- fix: give type attribute to buttons in header to prevent form submit (#5529) +- fix: time axis customization via slotLabelContent causes ugly spacing (#5526) +- fix: export EventSourceFunc in type definitions (#5530) +- fix: prevent timed background events from appearing in daygrid +- fix: change CSS for when 'today' background color is applied + - fixes bootstrap-themed popover incorrectly being colored semi-transparent + - removes yellow color from date headers in timegrid view, which looks better + +## 5.0.0 (2020-06-21) + +Changes since RC: + +- CSS fix for timegrid events. overflow hidden on time text +- fix where dayMaxEvents would not readjust when increasing height of calendar +- don't set custom text colors on list-view events or list-item events (#5518) +- fix event dot color not being customizable (#5522) +- fix for calendar updating when no options were reset (#5519) +- fix typescript def omission of eventSource 'method' prop (#5505) +- fix typescript def problem with schedulerLicenseKey again (#5462) + +## 5.0.0-rc (2020-06-15) + +Changes since beta.4: + +- breaking changes: + - renamed `datesDidUpdate` to `datesSet` and added more props to the arg + - for `eventResize` callback arg, renamed `prevEvent` to `oldEvent` + - resources are ordered by ID by default. no longer sort by natural order +- new features: + - system for overriding CSS variables + - timegrid event titles are sticky-positioned while scrolling + - eventDrop now receives relatedEvents prop + - eventResize now receives relatedEvents prop + - eventReceive now receives relatedEvents prop and a revert function + - eventLeave now receives relatedEvents prop and a revert function + - eventAdd + - eventChange + - eventRemove + - eventsSet + - initialEvents + - Event::toPlainObject, Event::toJSON + - Event::startStr, Event::endStr + - Calendar::addEvent accept `true` for source + - resourceAdd + - resourceChange + - resourceRemove + - resourcesSet + - initialResources + - Resource::setProp + - Resource::setExtendedProp + - Resource::toPlainObject, Resource::toJSON + - View::calendar + - TypeScript definitions for Vue connector +- bugfixes: + - Event popover display issues with many events (#5471) + - Jest test runner cannot find fullcalendar modules (#5467) + - Incorrect version of tslib required (#5479) + - License key option unknown, error in console (#5462) + - @fullcalendar/common has no exported member ScrollGridChunkConfig (#5459) + - event title should display on same line as time for 30 minute events in grid views (#5447) + +## 5.0.0-beta.4 (2020-05-26) + +Changes since beta.3: + +- features: + - improved printing for timeline view (#4813) +- fixes: + - `eventDisplay` not working (#5434) + - typescript errors when compiling with tsc (#5446) + - `slotLabelFormat` as array not working (#5450) + - more exports of typescript interfaces (#5452) + +## 5.0.0-beta.3 (2020-05-20) + +Changes since beta.2: + +- features: + - the `@fullcalendar/react` plugin now uses React's real virtual DOM engine + - typescript definitions for every part of the API. baked in, so won't fall out of date. + - console warnings when given unknown options/props/listeners +- minor API changes: + - `windowResize` and `datesDidUpdate` now receive an arg with a ViewApi object + - `eventSourceSuccess`, `eventSourceFailure`, and `moreLinkClick` can't be attached using .on() +- distribution: + - working sourcemaps in all packages (#4719) + - the `fullcalendar` and `fullcalendar-scheduler` bundles + - are published as browser-globals only. no longer published as UMDs + - now include the Google Calendar connector in their main files + - don't provide copies of the other non-bundled plugins anymore (like rrule, moment, moment-timezone) + - both receive locale/locale-all entrypoints that will be automatically connected when loaded +- fixes: + - Week numbers are not clickable as navLinks (#5427) + - Events of different heights in the same resource can be positioned incorrectly (#5413) + - Events displayed on wrong date when pushed down by previous events that span multiple days (#5408) + - `textColor` setting in Event Object not working anymore (#5355) + - `resourceAreaWidth` is not updated when changed with setOption (#5368) + - JS error when printing timeline view with expandRows (#5399) + - fixed Scheduler license keys not working with `fullcalendar-scheduler` bundle + +## 5.0.0-beta.2 (2020-04-14) + +Changes since beta.1: + +- feature: sticky header dates and footer scrollbar +- feature: daygrid events with times render differently by default, with a dot +- feature: a `datesDidUpdate` callback +- renamed options: + - `defaultView` -> `initialView` + - `defaultDate` -> `initialDate` + - `header` -> `headerToolbar` + - `footer` -> `footerToolbar` + - `allDayDefault` -> `defaultAllDay` + - `eventRendering` -> `eventDisplay` (and `display` in event objects) + - `dir` -> `direction` +- fix: sometimes event dragging and selecting broken after switching views (#5346) +- fix: most likely fixed problem with infinite loop (#5352) +- fix: timeline scrolling sometimes gets out of sync when using a scroll wheel (#4889) +- fix: many other little bugfixes + +View the [full changelog](https://fullcalendar.io/docs/v5/upgrading-from-v4) + +## 5.0.0-beta.1 (2020-04-06) + +Read the [blog post](https://fullcalendar.io/blog/2020/04/v5-beta-released) + +## 4.4.0 (2020-02-11) + +- configurable `googleCalendarApiBase` (#4974) +- fix: navigating prev/next quickly might miss an event-source fetch (#4975) +- new locales: ug (#180), uz (#3553) +- locale fixes: fr (#5236), az (#5185), th (#5069), el (#5010), pt-br (#3812) + +## 4.3.1 (2019-08-10) + +- `FullCalendar.version` had incorrect text +- scheduler's releaseDate not written correctly, + resulting in license key warning always showing. + +## 4.3.0 (2019-08-09) + +- HTML/CSS for timeline events has been refactored. BREAKING CHANGE if customized CSS. +- timeline event titles sometimes overflow outside of element when time (#4928) +- eventStartEditable false is not compatible with eventResourceEditable true (#4930) +- calling Calendar::render after initial render causes bad sizing (#4718, #4723) +- when list views destroyed, wouldn't call eventDestroy (#4727) +- solve JS errors when switching views and using showNonCurrentDates (#4677, #4767) +- prevent unnecessary scrollbars from appearing in daygrid views (4624, #4732) +- draggedEvent start time is null in eventAllow when switching resources (#4932) +- scrollToTime method honors a whole duration, not just a time (#4935) +- some background events wouldn't recieve eventClick or hovering (#3148, #4750) +- fix infinite recursion when custom view type is itself (#4198) +- respect firstDay setting when weekNumberCalculation set to ISO (#4734) +- fix typo in Danish (#4708) +- adjust typescript def for setExtendedProp (#4679) +- googleCalendarApiKey added to typescript options definition (#4772) +- moment/luxon formatting same-day range with dash (#4686) +- error importing moment plugin into typescript project (#4691, #4680, #4580) +- refs to sourcemaps removed from dist (accidentally included in previous version) +- distributing an ESM file, referenced by package.json's `module` +- using a more portable SASS (#4626, #4651, #4671) + +## 4.2.0 (2019-06-02) + +- fix recurring event expansion when event starts before view and has duration (#4617, #4635) +- simple event recurring now allows a duration property on the event object +- internal Calendar::setOptions method removed (never meant to be public) + +## 4.1.0 (2019-04-24) + +- scrollToTime method (#467) +- ISO8601 datetime strings with no 'T' not parsed in Safari (#4610) +- all-day dropped events after third not being draggable (#4616) +- dateClick/selecting sometime report wrong dates after calendar resize (#4608) +- js error when using navLinks with header=false (#4619) +- js error when more+ link and multiple async event sources (#4585) +- timeGridEventMinHeight is not defined in OptionsInput interface (#4605) +- Interdependent package semvers with carrot, use tilde (#4620) +- dayRender now called for day columns in timeGrid views + +## 4.0.2 (2019-04-03) + +Bugfixes: + +- eventAllow and constraints not respected when dragging event between calendars +- viewSkeletonRender now in typedefs (#4589) +- invalid draggedEvent properties in eventAllow for external dnd (#4575) +- forceEventDuration not working with external dnd (#4597) +- rrule displaying time when allDay is true (#4576) +- rrule events not displaying at interval start (#4596) +- prev button not initially working when starting on 31st of a month (#4595) +- clicking X in popover generating a dayClick (#4584) +- locale file used as single script tag not affecting calendar locale (#4581) +- header "today" button not translated for pt and pt-br (#4591) +- fa locale typo (#4582) + +## 4.0.1 (2019-03-18) + +Read about all the changes in v4: +https://fullcalendar.io/docs/upgrading-from-v3 + +Obscure breaking changes from v3->v4 not mentioned elsewhere: + +- `touchMouseIgnoreWait` moved to `(packageRoot).config.touchMouseIgnoreWait` +- `dataAttrPrefix` moved to `(packageRoot).config.dataAttrPrefix` + +Advancements since latest prerelease: + +- New styling for buttons and icons in header. New styling for events. +- Bugfixes: #4539, #4503, #4534, #4505, #4477, #4467, #4454, #4458, #4483, + #4517, #4506, #4435, #4498, #4497, #4446, #4432, #4530 + +NOTE: version "4.0.0" was skipped because of an NPM publishing error + +## 3.10.0 (2019-01-10) + +POTENTIALLY BREAKING CHANGE: +The jquery and moment packages have been moved to peerDependencies. If you are using +NPM to install fullcalendar, you'll need to explicitly add jquery and moment as +dependencies of your project. NPM will not install them automatically. (#4136, #4233) + +New Features: + +- events from a Google Calendar event source will receive extended props (#4123) +- export more classes and util functions (#4124) +- new locales: zh-hk (#4266), be (#4274) + +Bugfixes: + +- not accepting dayClicks/selects because of overflow-x:hidden on html/body (#3615) +- event end time not displayed when duration is one slot, in agenda view (#3049) +- switching views before event fetch resolves, JS error (#3689) +- single-day allDay event not showing when time is specified (#3854) +- prev button doesn't work when previous days are hidden by hiddenDays and dayCount + is greater than dateIncrement (#4202) +- calendar locale not used in all moments objects (#4174) +- background event background color does not completely fill cells in Chrome (#4145) +- provide a delta for eventResize when resizing from start (#4135) +- IE11 memory leak from not removing handler correctly (#4311) +- make touchstart handlers passive (#4087) +- fixed typescript definition for: eventAllow (#4243), selectAllow (#4319) +- fixed locales: de (#4197, #4371), hu (#4203), tr (#4312), ja (#4329) + +## 3.9.0 (2018-03-04) + +- Bootstrap 4 support (#4032, #4065, thx @GeekJosh) +- add OptionsInput to the fullcalendar.d.ts exports (#4040, #4006) +- columnHeaderFormat/columnHeaderHtml/columnHeaderText in .d.ts file (#4061, #4085) +- list-view auto-height not working (#3346, #4071, thx @WhatTheBuild) +- bump momentjs minimum version to 2.20.1, for locale fixes (#4014) +- swedish week header translation fix (#4082) +- dutch year translation (#4069) + +## 3.8.2 (2018-01-30) + +Bugfixes: + +- Fix TypeScript definitions file with strictNullChecks (#4035) + +## 3.8.1 (2018-01-28) + +Bugfixes: + +- TypeScript definition file not compatible with noImplicitAny (#4017) +- ES6 classes are not supported for grid class (#3437) +- day numbers in month view should be localized (#3339) +- select helper is resizable, causes js error (#3764) +- selecting over existing select helper causes js error (#4031) +- eventOrder doesn't work on custom fields (#3950) +- aria label on button icons (#4023) +- dynamic option changes to select/overlap/allow doesn't cause rerender + +Locales: + +- added Georgian (#3994) +- added Bosnian (#4029) + +## 3.8.0 (2017-12-18) + +- new settings for month/agenda/basic views (#3078): + - `columnHeaderFormat` (renamed from `columnFormat`) + - `columnHeaderText` + - `columnHeaderHtml` +- TypeScript definition file (fullcalendar.d.ts) included in npm package (#3889) +- codebase using SASS, though not taking advantage of it yet (#3463) +- codebase fully ported to TypeScript / Webpack +- Afrikaans locale fix (#3862) + +## 3.7.0 (2017-11-13) + +Bugfixes: + +- `render` method does not re-adjust calendar dimension (#3893) +- when custom view navigates completely into hidden weekends, JS error ([scheduler-375]) + +Other: + +- in themes.html demo, fixed broken Bootswatch themes (#3917) +- moved JavaScript codebase over to TypeScript + (same external API; embedded typedefs coming soon) + +[scheduler-375]: https://github.com/fullcalendar/fullcalendar-scheduler/issues/375 + +## 3.6.2 (2017-10-23) + +Bugfixes: + +- Google Calendar event sources not calling `loading` callback (#3884) +- `eventDataTransform` w/ eventConstraint shouldn't be called during event resizing (#3859) +- `navLinks` would go to the previously navigated date (#3869) +- `nowIndicator` arrow would repeatedly render (#3872) +- fc-content-skeleton DOM element would repeatedly render on navigation in agenda view + +## 3.6.1 (2017-10-11) + +Bugfixes: + +- JSON feed event sources always requesting current page (#3865) +- multi-day events appearing multiple times in more+ popover (#3856) + +## 3.6.0 (2017-10-10) + +Features: + +- `agendaEventMinHeight` for guaranteeing height (#961, #3788) thx @Stafie +- `columnHeader` can be set to `false` to hide headings (#3438, #3787) thx @caseyjhol +- export all View classes (#2851, #3831) +- `updateEvent`, update complex attributes (#2864) +- Albanian locale (#3847) thx @alensaqe + +Bugfixes: + +- objects used as non-standard Event properties ignored by `updateEvent` (#3839) +- listDay error if event goes over period (#3843) +- `validDays` with `hiddenDays`, js error when no days active (#3846) +- json feed Event Source object no longer has `url` property (#3845) +- `updateEvent`, allDay to timed, when no end, wrong end date (#3144) +- `removeEvents` by `_id` stopped working (#3828) +- correct `this` context in FuncEventSource (#3848) thx @declspec +- js event not received in unselect callback when selecting another cell (#3832) + +Incompatibilities: + +- The `viewRender` callback might now be fired AFTER events have been rendered + to the DOM. However, the eventRender/eventAfterRender/eventAfterAllRender callbacks + will always be fired after `viewRender`, just as before. +- The internal `Grid` class (accessed via `$.fullCalendar.Grid`) has been removed. + For monkeypatching, use DayGrid/TimeGrid directly. + +## 3.5.1 (2017-09-06) + +- fixed loading trigger not firing (#3810) +- fixed overaggressively fetching events, on option changes (#3820) +- fixed event object `date` property being discarded (tho still parsed) (#3819) +- fixed event object `_id` property being discarded (#3811) + +## 3.5.0 (2017-08-30) + +Features: + +- Bootstrap 3 theme support (#2334, #3566) + - via `themeSystem: 'bootstrap3'` (the `theme` option is deprecated) + - new `bootstrapGlyphicons` option + - jQuery UI "Cupertino" theme no longer included in zip archive + - improved theme switcher on demo page (#1436) + (big thanks to @joankaradimov) +- 25% event rendering performance improvement across the board (#2524) +- console message for unknown method/calendar (#3253) +- Serbian cyrilic/latin (#3656) +- available via Packagist (#2999, #3617) + +Bugfixes: + +- slot time label invisible when minTime starts out of alignment (#2786) +- bug with inverse-background event rendering when out of range (#3652) +- wrongly disabled prev/next when current date outside of validRange (#3686, #3651) +- updateEvent, error when changing allDay from false to true (#3518) +- updateEvent doesn't support ID changes (#2928) +- Promise then method doesn't forward result (#3744) +- Korean typo (#3693) +- fixed switching from any view to listview, eventAfterRender isn't called (#3751) + +Incompatibilities: + +- Event Objects obtained from clientEvents or various callbacks are no longer + references to internally used objects. Rather, they are static object copies. +- `clientEvents` method no longer returns events in same order as received. + Do not depend on order. + +## 3.4.0 (2017-04-27) + +- composer.json for Composer (PHP package manager) (#3617) +- fix toISOString for locales with non-trivial postformatting (#3619) +- fix for nested inverse-background events (#3609) +- Estonian locale (#3600) +- fixed Latvian localization (#3525) +- internal refactor of async systems + +## 3.3.1 (2017-04-01) + +Bugfixes: + +- stale calendar title when navigate away from then back to the a view (#3604) +- js error when gotoDate immediately after calendar initialization (#3598) +- agenda view scrollbars causes misalignment in jquery 3.2.1 (#3612) +- navigation bug when trying to navigate to a day of another week (#3610) +- dateIncrement not working when duration and dateIncrement have different units + +## 3.3.0 (2017-03-23) + +Features: + +- `visibleRange` - complete control over view's date range (#2847, #3105, #3245) +- `validRange` - restrict date range (#429) +- `changeView` - pass in a date or visibleRange as second param (#3366) +- `dateIncrement` - customize prev/next jump (#2710) +- `dateAlignment` - custom view alignment, like start-of-week (#3113) +- `dayCount` - force a fixed number-of-days, even with hiddenDays (#2753) +- `showNonCurrentDates` - option to hide day cells for prev/next months (#437) +- can define a defaultView with a duration/visibleRange/dayCount with needing + to create a custom view in the `views` object. Known as a "Generic View". + +Behavior Changes: + +- when custom view is specified with duration `{days:7}`, + it will no longer align with the start of the week. (#2847) +- when `gotoDate` is called on a custom view with a duration of multiple days, + the view will always shift to begin with the given date. (#3515) + +Bugfixes: + +- event rendering when excessive `minTime`/`maxTime` (#2530) +- event dragging not shown when excessive `minTime`/`maxTime` (#3055) +- excessive `minTime`/`maxTime` not reflected in event fetching (#3514) + - when minTime is negative, or maxTime beyond 24 hours, when event data is requested + via a function or a feed, the given data params will have time parts. +- external event dragging via touchpunch broken (#3544) +- can't make an immediate new selection after existing selection, with mouse. + introduced in v3.2.0 (#3558) + +## 3.2.0 (2017-02-14) + +Features: + +- `selectMinDistance`, threshold before a mouse selection begins (#2428) + +Bugfixes: + +- iOS 10, unwanted scrolling while dragging events/selection (#3403) +- dayClick triggered when swiping on touch devices (#3332) +- dayClick not functioning on Firefix mobile (#3450) +- title computed incorrectly for views with no weekends (#2884) +- unwanted scrollbars in month-view when non-integer width (#3453, #3444) +- incorrect date formatting for locales with non-standlone month/day names (#3478) +- date formatting, incorrect omission of trailing period for certain locales (#2504, #3486) +- formatRange should collapse same week numbers (#3467) +- Taiwanese locale updated (#3426) +- Finnish noEventsMessage updated (#3476) +- Croatian (hr) buttonText is blank (#3270) +- JSON feed PHP example, date range math bug (#3485) + +## 3.1.0 (2016-12-05) + +- experimental support for implicitly batched ("debounced") event rendering (#2938) + - `eventRenderWait` (off by default) +- new `footer` option, similar to header toolbar (#654, #3299) +- event rendering batch methods (#3351): + - `renderEvents` + - `updateEvents` +- more granular touch settings (#3377): + - `eventLongPressDelay` + - `selectLongPressDelay` +- eventDestroy not called when removing the popover (#3416, #3419) +- print stylesheet and gcal extension now offered as minified (#3415) +- fc-today in agenda header cells (#3361, #3365) +- height-related options in tandem with other options (#3327, #3384) +- Kazakh locale (#3394) +- Afrikaans locale (#3390) +- internal refactor related to timing of rendering and firing handlers. + calls to rerender the current date-range and events from within handlers + might not execute immediately. instead, will execute after handler finishes. + +## 3.0.1 (2016-09-26) + +Bugfixes: + +- list view rendering event times incorrectly (#3334) +- list view rendering events/days out of order (#3347) +- events with no title rendering as "undefined" +- add .fc scope to table print styles (#3343) +- "display no events" text fix for German (#3354) + +## 3.0.0 (2016-09-04) + +Features: + +- List View (#560) + - new views: `listDay`, `listWeek`, `listMonth`, `listYear`, and simply `list` + - `listDayFormat` + - `listDayAltFormat` + - `noEventsMessage` +- Clickable day/week numbers for easier navigation (#424) + - `navLinks` + - `navLinkDayClick` + - `navLinkWeekClick` +- Programmatically allow/disallow user interactions: + - `eventAllow` (#2740) + - `selectAllow` (#2511) +- Option to display week numbers in cells (#3024) + - `weekNumbersWithinDays` (set to `true` to activate) +- When week calc is ISO, default first day-of-week to Monday (#3255) +- Macedonian locale (#2739) +- Malay locale + +Breaking Changes: + +- IE8 support dropped +- jQuery: minimum support raised to v2.0.0 +- MomentJS: minimum support raised to v2.9.0 +- `lang` option renamed to `locale` +- dist files have been renamed to be more consistent with MomentJS: + - `lang/` -> `locale/` + - `lang-all.js` -> `locale-all.js` +- behavior of moment methods no longer affected by ambiguousness: + - `isSame` + - `isBefore` + - `isAfter` +- View-Option-Hashes no longer supported (deprecated in 2.2.4) +- removed `weekMode` setting +- removed `axisFormat` setting +- DOM structure of month/basic-view day cell numbers changed + +Bugfixes: + +- `$.fullCalendar.version` incorrect (#3292) + +Build System: + +- using gulp instead of grunt (faster) +- using npm internally for dependencies instead of bower +- changed repo directory structure + +## 2.9.1 (2016-07-31) + +- multiple definitions for businessHours (#2686) +- businessHours for single day doesn't display weekends (#2944) +- height/contentHeight can accept a function or 'parent' for dynamic value (#3271) +- fix +more popover clipped by overflow (#3232) +- fix +more popover positioned incorrectly when scrolled (#3137) +- Norwegian Nynorsk translation (#3246) +- fix isAnimating JS error (#3285) + +## 2.9.0 (2016-07-10) + +- Setters for (almost) all options (#564). + See [docs](https://fullcalendar.io/docs/utilities/dynamic_options/) for more info. +- Travis CI improvements (#3266) + +## 2.8.0 (2016-06-19) + +- getEventSources method (#3103, #2433) +- getEventSourceById method (#3223) +- refetchEventSources method (#3103, #1328, #254) +- removeEventSources method (#3165, #948) +- prevent flicker when refetchEvents is called (#3123, #2558) +- fix for removing event sources that share same URL (#3209) +- jQuery 3 support (#3197, #3124) +- Travis CI integration (#3218) +- EditorConfig for promoting consistent code style (#141) +- use en dash when formatting ranges (#3077) +- height:auto always shows scrollbars in month view on FF (#3202) +- new languages: + - Basque (#2992) + - Galician (#194) + - Luxembourgish (#2979) + +## 2.7.3 (2016-06-02) + +internal enhancements that plugins can benefit from: + +- EventEmitter not correctly working with stopListeningTo +- normalizeEvent hook for manipulating event data + +## 2.7.2 (2016-05-20) + +- fixed desktops/laptops with touch support not accepting mouse events for + dayClick/dragging/resizing (#3154, #3149) +- fixed dayClick incorrectly triggered on touch scroll (#3152) +- fixed touch event dragging wrongfully beginning upon scrolling document (#3160) +- fixed minified JS still contained comments +- UI change: mouse users must hover over an event to reveal its resizers + +## 2.7.1 (2016-05-01) + +- dayClick not firing on touch devices (#3138) +- icons for prev/next not working in MS Edge (#2852) +- fix bad languages troubles with firewalls (#3133, #3132) +- update all dev dependencies (#3145, #3010, #2901, #251) +- git-ignore npm debug logs (#3011) +- misc automated test updates (#3139, #3147) +- Google Calendar htmlLink not always defined (#2844) + +## 2.7.0 (2016-04-23) + +touch device support (#994): - smoother scrolling - interactions initiated via "long press": - event drag-n-drop - event resize - time-range selecting - `longPressDelay` + +## 2.6.1 (2016-02-17) + +- make `nowIndicator` positioning refresh on window resize + +## 2.6.0 (2016-01-07) + +- current time indicator (#414) +- bundled with most recent version of moment (2.11.0) +- UMD wrapper around lang files now handles commonjs (#2918) +- fix bug where external event dragging would not respect eventOverlap +- fix bug where external event dropping would not render the whole-day highlight + +## 2.5.0 (2015-11-30) + +- internal timezone refactor. fixes #2396, #2900, #2945, #2711 +- internal "grid" system refactor. improved API for plugins. + +## 2.4.0 (2015-08-16) + +- add new buttons to the header via `customButtons` ([225]) +- control stacking order of events via `eventOrder` ([364]) +- control frequency of slot text via `slotLabelInterval` ([946]) +- `displayEventTime` ([1904]) +- `on` and `off` methods ([1910]) +- renamed `axisFormat` to `slotLabelFormat` + +[225]: https://code.google.com/p/fullcalendar/issues/detail?id=225 +[364]: https://code.google.com/p/fullcalendar/issues/detail?id=364 +[946]: https://code.google.com/p/fullcalendar/issues/detail?id=946 +[1904]: https://code.google.com/p/fullcalendar/issues/detail?id=1904 +[1910]: https://code.google.com/p/fullcalendar/issues/detail?id=1910 + +## 2.3.2 (2015-06-14) + +- minor code adjustment in preparation for plugins + +## 2.3.1 (2015-03-08) + +- Fix week view column title for en-gb ([PR220]) +- Publish to NPM ([2447]) +- Detangle bower from npm package ([PR179]) + +[PR220]: https://github.com/arshaw/fullcalendar/pull/220 +[2447]: https://code.google.com/p/fullcalendar/issues/detail?id=2447 +[PR179]: https://github.com/arshaw/fullcalendar/pull/179 + +## 2.3.0 (2015-02-21) + +- internal refactoring in preparation for other views +- businessHours now renders on whole-days in addition to timed areas +- events in "more" popover not sorted by time ([2385]) +- avoid using moment's deprecated zone method ([2443]) +- destroying the calendar sometimes causes all window resize handlers to be unbound ([2432]) +- multiple calendars on one page, can't accept external elements after navigating ([2433]) +- accept external events from jqui sortable ([1698]) +- external jqui drop processed before reverting ([1661]) +- IE8 fix: month view renders incorrectly ([2428]) +- IE8 fix: eventLimit:true wouldn't activate "more" link ([2330]) +- IE8 fix: dragging an event with an href +- IE8 fix: invisible element while dragging agenda view events +- IE8 fix: erratic external element dragging + +[2385]: https://code.google.com/p/fullcalendar/issues/detail?id=2385 +[2443]: https://code.google.com/p/fullcalendar/issues/detail?id=2443 +[2432]: https://code.google.com/p/fullcalendar/issues/detail?id=2432 +[2433]: https://code.google.com/p/fullcalendar/issues/detail?id=2433 +[1698]: https://code.google.com/p/fullcalendar/issues/detail?id=1698 +[1661]: https://code.google.com/p/fullcalendar/issues/detail?id=1661 +[2428]: https://code.google.com/p/fullcalendar/issues/detail?id=2428 +[2330]: https://code.google.com/p/fullcalendar/issues/detail?id=2330 + +## 2.2.7 (2015-02-10) + +- view.title wasn't defined in viewRender callback ([2407]) +- FullCalendar versions >= 2.2.5 brokenness with Moment versions <= 2.8.3 ([2417]) +- Support Bokmal Norwegian language specifically ([2427]) + +[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407 +[2417]: https://code.google.com/p/fullcalendar/issues/detail?id=2417 +[2427]: https://code.google.com/p/fullcalendar/issues/detail?id=2427 + +## 2.2.6 (2015-01-11) + +- Compatibility with Moment v2.9. Was breaking GCal plugin ([2408]) +- View object's `title` property mistakenly omitted ([2407]) +- Single-day views with hiddens days could cause prev/next misbehavior ([2406]) +- Don't let the current date ever be a hidden day (solves [2395]) +- Hebrew locale ([2157]) + +[2408]: https://code.google.com/p/fullcalendar/issues/detail?id=2408 +[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407 +[2406]: https://code.google.com/p/fullcalendar/issues/detail?id=2406 +[2395]: https://code.google.com/p/fullcalendar/issues/detail?id=2395 +[2157]: https://code.google.com/p/fullcalendar/issues/detail?id=2157 + +## 2.2.5 (2014-12-30) + +- `buttonText` specified for custom views via the `views` option + - bugfix: wrong default value, couldn't override default + - feature: default value taken from locale + +## 2.2.4 (2014-12-29) + +- Arbitrary durations for basic/agenda views with the `views` option ([692]) +- Specify view-specific options using the `views` option. fixes [2283] +- Deprecate view-option-hashes +- Formalize and expose View API ([1055]) +- updateEvent method, more intuitive behavior. fixes [2194] + +[692]: https://code.google.com/p/fullcalendar/issues/detail?id=692 +[2283]: https://code.google.com/p/fullcalendar/issues/detail?id=2283 +[1055]: https://code.google.com/p/fullcalendar/issues/detail?id=1055 +[2194]: https://code.google.com/p/fullcalendar/issues/detail?id=2194 + +## 2.2.3 (2014-11-26) + +- removeEventSource with Google Calendar object source, would not remove ([2368]) +- Events with invalid end dates are still accepted and rendered ([2350], [2237], [2296]) +- Bug when rendering business hours and navigating away from original view ([2365]) +- Links to Google Calendar events will use current timezone ([2122]) +- Google Calendar plugin works with timezone names that have spaces +- Google Calendar plugin accepts person email addresses as calendar IDs +- Internally use numeric sort instead of alphanumeric sort ([2370]) + +[2368]: https://code.google.com/p/fullcalendar/issues/detail?id=2368 +[2350]: https://code.google.com/p/fullcalendar/issues/detail?id=2350 +[2237]: https://code.google.com/p/fullcalendar/issues/detail?id=2237 +[2296]: https://code.google.com/p/fullcalendar/issues/detail?id=2296 +[2365]: https://code.google.com/p/fullcalendar/issues/detail?id=2365 +[2122]: https://code.google.com/p/fullcalendar/issues/detail?id=2122 +[2370]: https://code.google.com/p/fullcalendar/issues/detail?id=2370 + +## 2.2.2 (2014-11-19) + +- Fixes to Google Calendar API V3 code + - wouldn't recognize a lone-string Google Calendar ID if periods before the @ symbol + - removeEventSource wouldn't work when given a Google Calendar ID + +## 2.2.1 (2014-11-19) + +- Migrate Google Calendar plugin to use V3 of the API ([1526]) + +[1526]: https://code.google.com/p/fullcalendar/issues/detail?id=1526 + +## 2.2.0 (2014-11-14) + +- Background events. Event object's `rendering` property ([144], [1286]) +- `businessHours` option ([144]) +- Controlling where events can be dragged/resized and selections can go ([396], [1286], [2253]) + - `eventOverlap`, `selectOverlap`, and similar + - `eventConstraint`, `selectConstraint`, and similar +- Improvements to dragging and dropping external events ([2004]) + - Associating with real event data. used with `eventReceive` + - Associating a `duration` +- Performance boost for moment creation + - Be aware, FullCalendar-specific methods now attached directly to global moment.fn + - Helps with [issue 2259][2259] +- Reintroduced forgotten `dropAccept` option ([2312]) + +[144]: https://code.google.com/p/fullcalendar/issues/detail?id=144 +[396]: https://code.google.com/p/fullcalendar/issues/detail?id=396 +[1286]: https://code.google.com/p/fullcalendar/issues/detail?id=1286 +[2004]: https://code.google.com/p/fullcalendar/issues/detail?id=2004 +[2253]: https://code.google.com/p/fullcalendar/issues/detail?id=2253 +[2259]: https://code.google.com/p/fullcalendar/issues/detail?id=2259 +[2312]: https://code.google.com/p/fullcalendar/issues/detail?id=2312 + +## 2.1.1 (2014-08-29) + +- removeEventSource not working with array ([2203]) +- mouseout not triggered after mouseover+updateEvent ([829]) +- agenda event's render with no <a> href, not clickable ([2263]) + +[2203]: https://code.google.com/p/fullcalendar/issues/detail?id=2203 +[829]: https://code.google.com/p/fullcalendar/issues/detail?id=829 +[2263]: https://code.google.com/p/fullcalendar/issues/detail?id=2263 + +## 2.1.0 (2014-08-25) + +Large code refactor with better OOP, better code reuse, and more comments. +**No more reliance on jQuery UI** for event dragging, resizing, or anything else. + +Significant changes to HTML/CSS skeleton: + +- Leverages tables for liquid rendering of days and events. No costly manual repositioning ([809]) +- **Backwards-incompatibilities**: + - **Many classNames have changed. Custom CSS will likely need to be adjusted.** + - IE7 definitely not supported anymore + - In `eventRender` callback, `element` will not be attached to DOM yet + - Events are styled to be one line by default ([1992]). Can be undone through custom CSS, + but not recommended (might get gaps [like this][111] in certain situations). + +A "more..." link when there are too many events on a day ([304]). Works with month and basic views +as well as the all-day section of the agenda views. New options: + +- `eventLimit`. a number or `true` +- `eventLimitClick`. the `"popover`" value will reveal all events in a raised panel (the default) +- `moreLinkText` +- `dayPopoverFormat` + +Changes related to height and scrollbars: + +- `aspectRatio`/`height`/`contentHeight` values will be honored _no matter what_ + - If too many events causing too much vertical space, scrollbars will be used ([728]). + This is default behavior for month view (**backwards-incompatibility**) + - If too few slots in agenda view, view will stretch to be the correct height ([2196]) +- `'auto'` value for `height`/`contentHeight` options. If content is too tall, the view will + vertically stretch to accomodate and no scrollbars will be used ([521]). +- Tall weeks in month view will borrow height from other weeks ([243]) +- Automatically scroll the view then dragging/resizing an event ([1025], [2078]) +- New `fixedWeekCount` option to determines the number of weeks in month view + - Supersedes `weekMode` (**deprecated**). Instead, use a combination of `fixedWeekCount` and + one of the height options, possibly with an `'auto'` value + +Much nicer, glitch-free rendering of calendar _for printers_ ([35]). Things you might not expect: + +- Buttons will become hidden +- Agenda views display a flat list of events where the time slots would be + +Other issues resolved along the way: + +- Space on right side of agenda events configurable through CSS ([204]) +- Problem with window resize ([259]) +- Events sorting stays consistent across weeks ([510]) +- Agenda's columns misaligned on wide screens ([511]) +- Run `selectHelper` through `eventRender` callbacks ([629]) +- Keyboard access, tabbing ([637]) +- Run resizing events through `eventRender` ([714]) +- Resize an event to a different day in agenda views ([736]) +- Allow selection across days in agenda views ([778]) +- Mouseenter delegated event not working on event elements ([936]) +- Agenda event dragging, snapping to different columns is erratic ([1101]) +- Android browser cuts off Day view at 8 PM with no scroll bar ([1203]) +- Don't fire `eventMouseover`/`eventMouseout` while dragging/resizing ([1297]) +- Customize the resize handle text ("=") ([1326]) +- If agenda event is too short, don't overwrite `.fc-event-time` ([1700]) +- Zooming calendar causes events to misalign ([1996]) +- Event destroy callback on event removal ([2017]) +- Agenda views, when RTL, should have axis on right ([2132]) +- Make header buttons more accessibile ([2151]) +- daySelectionMousedown should interpret OSX ctrl+click as a right mouse click ([2169]) +- Best way to display time text on multi-day events _with times_ ([2172]) +- Eliminate table use for header layout ([2186]) +- Event delegation used for event-related callbacks (like `eventClick`). Speedier. + +[35]: https://code.google.com/p/fullcalendar/issues/detail?id=35 +[204]: https://code.google.com/p/fullcalendar/issues/detail?id=204 +[243]: https://code.google.com/p/fullcalendar/issues/detail?id=243 +[259]: https://code.google.com/p/fullcalendar/issues/detail?id=259 +[304]: https://code.google.com/p/fullcalendar/issues/detail?id=304 +[510]: https://code.google.com/p/fullcalendar/issues/detail?id=510 +[511]: https://code.google.com/p/fullcalendar/issues/detail?id=511 +[521]: https://code.google.com/p/fullcalendar/issues/detail?id=521 +[629]: https://code.google.com/p/fullcalendar/issues/detail?id=629 +[637]: https://code.google.com/p/fullcalendar/issues/detail?id=637 +[714]: https://code.google.com/p/fullcalendar/issues/detail?id=714 +[728]: https://code.google.com/p/fullcalendar/issues/detail?id=728 +[736]: https://code.google.com/p/fullcalendar/issues/detail?id=736 +[778]: https://code.google.com/p/fullcalendar/issues/detail?id=778 +[809]: https://code.google.com/p/fullcalendar/issues/detail?id=809 +[936]: https://code.google.com/p/fullcalendar/issues/detail?id=936 +[1025]: https://code.google.com/p/fullcalendar/issues/detail?id=1025 +[1101]: https://code.google.com/p/fullcalendar/issues/detail?id=1101 +[1203]: https://code.google.com/p/fullcalendar/issues/detail?id=1203 +[1297]: https://code.google.com/p/fullcalendar/issues/detail?id=1297 +[1326]: https://code.google.com/p/fullcalendar/issues/detail?id=1326 +[1700]: https://code.google.com/p/fullcalendar/issues/detail?id=1700 +[1992]: https://code.google.com/p/fullcalendar/issues/detail?id=1992 +[1996]: https://code.google.com/p/fullcalendar/issues/detail?id=1996 +[2017]: https://code.google.com/p/fullcalendar/issues/detail?id=2017 +[2078]: https://code.google.com/p/fullcalendar/issues/detail?id=2078 +[2132]: https://code.google.com/p/fullcalendar/issues/detail?id=2132 +[2151]: https://code.google.com/p/fullcalendar/issues/detail?id=2151 +[2169]: https://code.google.com/p/fullcalendar/issues/detail?id=2169 +[2172]: https://code.google.com/p/fullcalendar/issues/detail?id=2172 +[2186]: https://code.google.com/p/fullcalendar/issues/detail?id=2186 +[2196]: https://code.google.com/p/fullcalendar/issues/detail?id=2196 +[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111 + +## 2.0.3 (2014-08-15) + +- moment-2.8.1 compatibility ([2221]) +- relative path in bower.json ([PR 117]) +- upgraded jquery-ui and misc dev dependencies + +[2221]: https://code.google.com/p/fullcalendar/issues/detail?id=2221 +[PR 117]: https://github.com/arshaw/fullcalendar/pull/177 + +## 2.0.2 (2014-06-24) + +- bug with persisting addEventSource calls ([2191]) +- bug with persisting removeEvents calls with an array source ([2187]) +- bug with removeEvents method when called with 0 removes all events ([2082]) + +[2191]: https://code.google.com/p/fullcalendar/issues/detail?id=2191 +[2187]: https://code.google.com/p/fullcalendar/issues/detail?id=2187 +[2082]: https://code.google.com/p/fullcalendar/issues/detail?id=2082 + +## 2.0.1 (2014-06-15) + +- `delta` parameters reintroduced in `eventDrop` and `eventResize` handlers ([2156]) + - **Note**: this changes the argument order for `revertFunc` +- wrongfully triggering a windowResize when resizing an agenda view event ([1116]) +- `this` values in event drag-n-drop/resize handlers consistently the DOM node ([1177]) +- `displayEventEnd` - v2 workaround to force display of an end time ([2090]) +- don't modify passed-in eventSource items ([954]) +- destroy method now removes fc-ltr class ([2033]) +- weeks of last/next month still visible when weekends are hidden ([2095]) +- fixed memory leak when destroying calendar with selectable/droppable ([2137]) +- Icelandic language ([2180]) +- Bahasa Indonesia language ([PR 172]) + +[1116]: https://code.google.com/p/fullcalendar/issues/detail?id=1116 +[1177]: https://code.google.com/p/fullcalendar/issues/detail?id=1177 +[2090]: https://code.google.com/p/fullcalendar/issues/detail?id=2090 +[954]: https://code.google.com/p/fullcalendar/issues/detail?id=954 +[2033]: https://code.google.com/p/fullcalendar/issues/detail?id=2033 +[2095]: https://code.google.com/p/fullcalendar/issues/detail?id=2095 +[2137]: https://code.google.com/p/fullcalendar/issues/detail?id=2137 +[2156]: https://code.google.com/p/fullcalendar/issues/detail?id=2156 +[2180]: https://code.google.com/p/fullcalendar/issues/detail?id=2180 +[PR 172]: https://github.com/arshaw/fullcalendar/pull/172 + +## 2.0.0 (2014-06-01) + +Internationalization support, timezone support, and [MomentJS] integration. Extensive changes, many +of which are backwards incompatible. + +[Full list of changes][Upgrading-to-v2] | [Affected Issues][Date-Milestone] + +An automated testing framework has been set up ([Karma] + [Jasmine]) and tests have been written +which cover about half of FullCalendar's functionality. Special thanks to @incre-d, @vidbina, and +@sirrocco for the help. + +In addition, the main development repo has been repurposed to also include the built distributable +JS/CSS for the project and will serve as the new [Bower] endpoint. + +[MomentJS]: http://momentjs.com/ +[Upgrading-to-v2]: http://arshaw.com/fullcalendar/wiki/Upgrading-to-v2/ +[Date-Milestone]: https://code.google.com/p/fullcalendar/issues/list?can=1&q=milestone%3Ddate +[Karma]: http://karma-runner.github.io/ +[Jasmine]: http://jasmine.github.io/ +[Bower]: http://bower.io/ + +## 1.6.4 (2013-09-01) + +- better algorithm for positioning timed agenda events ([1115]) +- `slotEventOverlap` option to tweak timed agenda event overlapping ([218]) +- selection bug when slot height is customized ([1035]) +- supply view argument in `loading` callback ([1018]) +- fixed week number not displaying in agenda views ([1951]) +- fixed fullCalendar not initializing with no options ([1356]) +- NPM's `package.json`, no more warnings or errors ([1762]) +- building the bower component should output `bower.json` instead of `component.json` ([PR 125]) +- use bower internally for fetching new versions of jQuery and jQuery UI + +[1115]: https://code.google.com/p/fullcalendar/issues/detail?id=1115 +[218]: https://code.google.com/p/fullcalendar/issues/detail?id=218 +[1035]: https://code.google.com/p/fullcalendar/issues/detail?id=1035 +[1018]: https://code.google.com/p/fullcalendar/issues/detail?id=1018 +[1951]: https://code.google.com/p/fullcalendar/issues/detail?id=1951 +[1356]: https://code.google.com/p/fullcalendar/issues/detail?id=1356 +[1762]: https://code.google.com/p/fullcalendar/issues/detail?id=1762 +[PR 125]: https://github.com/arshaw/fullcalendar/pull/125 + +## 1.6.3 (2013-08-10) + +- `viewRender` callback ([PR 15]) +- `viewDestroy` callback ([PR 15]) +- `eventDestroy` callback ([PR 111]) +- `handleWindowResize` option ([PR 54]) +- `eventStartEditable`/`startEditable` options ([PR 49]) +- `eventDurationEditable`/`durationEditable` options ([PR 49]) +- specify function for `$.ajax` `data` parameter for JSON event sources ([PR 59]) +- fixed bug with agenda event dropping in wrong column ([PR 55]) +- easier event element z-index customization ([PR 58]) +- classNames on past/future days ([PR 88]) +- allow `null`/`undefined` event titles ([PR 84]) +- small optimize for agenda event rendering ([PR 56]) +- deprecated: + - `viewDisplay` + - `disableDragging` + - `disableResizing` +- bundled with latest jQuery (1.10.2) and jQuery UI (1.10.3) + +[PR 15]: https://github.com/arshaw/fullcalendar/pull/15 +[PR 111]: https://github.com/arshaw/fullcalendar/pull/111 +[PR 54]: https://github.com/arshaw/fullcalendar/pull/54 +[PR 49]: https://github.com/arshaw/fullcalendar/pull/49 +[PR 59]: https://github.com/arshaw/fullcalendar/pull/59 +[PR 55]: https://github.com/arshaw/fullcalendar/pull/55 +[PR 58]: https://github.com/arshaw/fullcalendar/pull/58 +[PR 88]: https://github.com/arshaw/fullcalendar/pull/88 +[PR 84]: https://github.com/arshaw/fullcalendar/pull/84 +[PR 56]: https://github.com/arshaw/fullcalendar/pull/56 + +## 1.6.2 (2013-07-18) + +- `hiddenDays` option ([686]) +- bugfix: when `eventRender` returns `false`, incorrect stacking of events ([762]) +- bugfix: couldn't change `event.backgroundImage` when calling `updateEvent` (thx @stephenharris) + +[686]: https://code.google.com/p/fullcalendar/issues/detail?id=686 +[762]: https://code.google.com/p/fullcalendar/issues/detail?id=762 + +## 1.6.1 (2013-04-14) + +- fixed event inner content overflow bug ([1783]) +- fixed table header className bug [1772] +- removed text-shadow on events (better for general use, thx @tkrotoff) + +[1783]: https://code.google.com/p/fullcalendar/issues/detail?id=1783 +[1772]: https://code.google.com/p/fullcalendar/issues/detail?id=1772 + +## 1.6.0 (2013-03-18) + +- visual facelift, with bootstrap-inspired buttons and colors +- simplified HTML/CSS for events and buttons +- `dayRender`, for modifying a day cell ([191], thx @althaus) +- week numbers on side of calendar ([295]) + - `weekNumber` + - `weekNumberCalculation` + - `weekNumberTitle` + - `W` formatting variable +- finer snapping granularity for agenda view events ([495], thx @ms-doodle-com) +- `eventAfterAllRender` ([753], thx @pdrakeweb) +- `eventDataTransform` (thx @joeyspo) +- `data-date` attributes on cells (thx @Jae) +- expose `$.fullCalendar.dateFormatters` +- when clicking fast on buttons, prevent text selection +- bundled with latest jQuery (1.9.1) and jQuery UI (1.10.2) +- Grunt/Lumbar build system for internal development +- build for Bower package manager +- build for jQuery plugin site + +[191]: https://code.google.com/p/fullcalendar/issues/detail?id=191 +[295]: https://code.google.com/p/fullcalendar/issues/detail?id=295 +[495]: https://code.google.com/p/fullcalendar/issues/detail?id=495 +[753]: https://code.google.com/p/fullcalendar/issues/detail?id=753 + +## 1.5.4 (2012-09-05) + +- made compatible with jQuery 1.8.\* (thx @archaeron) +- bundled with jQuery 1.8.1 and jQuery UI 1.8.23 + +## 1.5.3 (2012-02-06) + +- fixed dragging issue with jQuery UI 1.8.16 ([1168]) +- bundled with jQuery 1.7.1 and jQuery UI 1.8.17 + +[1168]: https://code.google.com/p/fullcalendar/issues/detail?id=1168 + +## 1.5.2 (2011-08-21) + +- correctly process UTC "Z" ISO8601 date strings ([750]) + +[750]: https://code.google.com/p/fullcalendar/issues/detail?id=750 + +## 1.5.1 (2011-04-09) + +- more flexible ISO8601 date parsing ([814]) +- more flexible parsing of UNIX timestamps ([826]) +- FullCalendar now buildable from source on a Mac ([795]) +- FullCalendar QA'd in FF4 ([883]) +- upgraded to jQuery 1.5.2 (which supports IE9) and jQuery UI 1.8.11 + +[814]: https://code.google.com/p/fullcalendar/issues/detail?id=814 +[826]: https://code.google.com/p/fullcalendar/issues/detail?id=826 +[795]: https://code.google.com/p/fullcalendar/issues/detail?id=795 +[883]: https://code.google.com/p/fullcalendar/issues/detail?id=883 + +## 1.5 (2011-03-19) + +- slicker default styling for buttons +- reworked a lot of the calendar's HTML and accompanying CSS (solves [327] and [395]) +- more printer-friendly (fullcalendar-print.css) +- fullcalendar now inherits styles from jquery-ui themes differently. + styles for buttons are distinct from styles for calendar cells. + (solves [299]) +- can now color events through FullCalendar options and Event-Object properties ([117]) + THIS IS NOW THE PREFERRED METHOD OF COLORING EVENTS (as opposed to using className and CSS) + - FullCalendar options: + - eventColor (changes both background and border) + - eventBackgroundColor + - eventBorderColor + - eventTextColor + - Event-Object options: + - color (changes both background and border) + - backgroundColor + - borderColor + - textColor +- can now specify an event source as an _object_ with a `url` property (json feed) or + an `events` property (function or array) with additional properties that will + be applied to the entire event source: + - color (changes both background and border) + - backgroudColor + - borderColor + - textColor + - className + - editable + - allDayDefault + - ignoreTimezone + - startParam (for a feed) + - endParam (for a feed) + - ANY OF THE JQUERY $.ajax OPTIONS + allows for easily changing from GET to POST and sending additional parameters ([386]) + allows for easily attaching ajax handlers such as `error` ([754]) + allows for turning caching on ([355]) +- Google Calendar feeds are now specified differently: + - specify a simple string of your feed's URL + - specify an _object_ with a `url` property of your feed's URL. + you can include any of the new Event-Source options in this object. + - the old `$.fullCalendar.gcalFeed` method still works +- no more IE7 SSL popup ([504]) +- remove `cacheParam` - use json event source `cache` option instead +- latest jquery/jquery-ui + +[327]: https://code.google.com/p/fullcalendar/issues/detail?id=327 +[395]: https://code.google.com/p/fullcalendar/issues/detail?id=395 +[299]: https://code.google.com/p/fullcalendar/issues/detail?id=299 +[117]: https://code.google.com/p/fullcalendar/issues/detail?id=117 +[386]: https://code.google.com/p/fullcalendar/issues/detail?id=386 +[754]: https://code.google.com/p/fullcalendar/issues/detail?id=754 +[355]: https://code.google.com/p/fullcalendar/issues/detail?id=355 +[504]: https://code.google.com/p/fullcalendar/issues/detail?id=504 + +## 1.4.11 (2011-02-22) + +- fixed rerenderEvents bug ([790]) +- fixed bug with faulty dragging of events from all-day slot in agenda views +- bundled with jquery 1.5 and jquery-ui 1.8.9 + +[790]: https://code.google.com/p/fullcalendar/issues/detail?id=790 + +## 1.4.10 (2011-01-02) + +- fixed bug with resizing event to different week in 5-day month view ([740]) +- fixed bug with events not sticking after a removeEvents call ([757]) +- fixed bug with underlying parseTime method, and other uses of parseInt ([688]) + +[740]: https://code.google.com/p/fullcalendar/issues/detail?id=740 +[757]: https://code.google.com/p/fullcalendar/issues/detail?id=757 +[688]: https://code.google.com/p/fullcalendar/issues/detail?id=688 + +## 1.4.9 (2010-11-16) + +- new algorithm for vertically stacking events ([111]) +- resizing an event to a different week ([306]) +- bug: some events not rendered with consecutive calls to addEventSource ([679]) + +[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111 +[306]: https://code.google.com/p/fullcalendar/issues/detail?id=306 +[679]: https://code.google.com/p/fullcalendar/issues/detail?id=679 + +## 1.4.8 (2010-10-16) + +- ignoreTimezone option (set to `false` to process UTC offsets in ISO8601 dates) +- bugfixes + - event refetching not being called under certain conditions ([417], [554]) + - event refetching being called multiple times under certain conditions ([586], [616]) + - selection cannot be triggered by right mouse button ([558]) + - agenda view left axis sized incorrectly ([465]) + - IE js error when calendar is too narrow ([517]) + - agenda view looks strange when no scrollbars ([235]) + - improved parsing of ISO8601 dates with UTC offsets +- $.fullCalendar.version +- an internal refactor of the code, for easier future development and modularity + +[417]: https://code.google.com/p/fullcalendar/issues/detail?id=417 +[554]: https://code.google.com/p/fullcalendar/issues/detail?id=554 +[586]: https://code.google.com/p/fullcalendar/issues/detail?id=586 +[616]: https://code.google.com/p/fullcalendar/issues/detail?id=616 +[558]: https://code.google.com/p/fullcalendar/issues/detail?id=558 +[465]: https://code.google.com/p/fullcalendar/issues/detail?id=465 +[517]: https://code.google.com/p/fullcalendar/issues/detail?id=517 +[235]: https://code.google.com/p/fullcalendar/issues/detail?id=235 + +## 1.4.7 (2010-07-05) + +- "dropping" external objects onto the calendar + - droppable (boolean, to turn on/off) + - dropAccept (to filter which events the calendar will accept) + - drop (trigger) +- selectable options can now be specified with a View Option Hash +- bugfixes + - dragged & reverted events having wrong time text ([406]) + - bug rendering events that have an endtime with seconds, but no hours/minutes ([477]) + - gotoDate date overflow bug ([429]) + - wrong date reported when clicking on edge of last column in agenda views [412] +- support newlines in event titles +- select/unselect callbacks now passes native js event + +[406]: https://code.google.com/p/fullcalendar/issues/detail?id=406 +[477]: https://code.google.com/p/fullcalendar/issues/detail?id=477 +[429]: https://code.google.com/p/fullcalendar/issues/detail?id=429 +[412]: https://code.google.com/p/fullcalendar/issues/detail?id=412 + +## 1.4.6 (2010-05-31) + +- "selecting" days or timeslots + - options: selectable, selectHelper, unselectAuto, unselectCancel + - callbacks: select, unselect + - methods: select, unselect +- when dragging an event, the highlighting reflects the duration of the event +- code compressing by Google Closure Compiler +- bundled with jQuery 1.4.2 and jQuery UI 1.8.1 + +## 1.4.5 (2010-02-21) + +- lazyFetching option, which can force the calendar to fetch events on every view/date change +- scroll state of agenda views are preserved when switching back to view +- bugfixes + - calling methods on an uninitialized fullcalendar throws error + - IE6/7 bug where an entire view becomes invisible ([320]) + - error when rendering a hidden calendar (in jquery ui tabs for example) in IE ([340]) + - interconnected bugs related to calendar resizing and scrollbars + - when switching views or clicking prev/next, calendar would "blink" ([333]) + - liquid-width calendar's events shifted (depending on initial height of browser) ([341]) + - more robust underlying algorithm for calendar resizing + +[320]: https://code.google.com/p/fullcalendar/issues/detail?id=320 +[340]: https://code.google.com/p/fullcalendar/issues/detail?id=340 +[333]: https://code.google.com/p/fullcalendar/issues/detail?id=333 +[341]: https://code.google.com/p/fullcalendar/issues/detail?id=341 + +## 1.4.4 (2010-02-03) + +- optimized event rendering in all views (events render in 1/10 the time) +- gotoDate() does not force the calendar to unnecessarily rerender +- render() method now correctly readjusts height + +## 1.4.3 (2009-12-22) + +- added destroy method +- Google Calendar event pages respect currentTimezone +- caching now handled by jQuery's ajax +- protection from setting aspectRatio to zero +- bugfixes + - parseISO8601 and DST caused certain events to display day before + - button positioning problem in IE6 + - ajax event source removed after recently being added, events still displayed + - event not displayed when end is an empty string + - dynamically setting calendar height when no events have been fetched, throws error + +## 1.4.2 (2009-12-02) + +- eventAfterRender trigger +- getDate & getView methods +- height & contentHeight options (explicitly sets the pixel height) +- minTime & maxTime options (restricts shown hours in agenda view) +- getters [for all options] and setters [for height, contentHeight, and aspectRatio ONLY! stay tuned..] +- render method now readjusts calendar's size +- bugfixes + - lightbox scripts that use iframes (like fancybox) + - day-of-week classNames were off when firstDay=1 + - guaranteed space on right side of agenda events (even when stacked) + - accepts ISO8601 dates with a space (instead of 'T') + +## 1.4.1 (2009-10-31) + +- can exclude weekends with new 'weekends' option +- gcal feed 'currentTimezone' option +- bugfixes + - year/month/date option sometimes wouldn't set correctly (depending on current date) + - daylight savings issue caused agenda views to start at 1am (for BST users) +- cleanup of gcal.js code + +## 1.4 (2009-10-19) + +- agendaWeek and agendaDay views +- added some options for agenda views: + - allDaySlot + - allDayText + - firstHour + - slotMinutes + - defaultEventMinutes + - axisFormat +- modified some existing options/triggers to work with agenda views: + - dragOpacity and timeFormat can now accept a "View Hash" (a new concept) + - dayClick now has an allDay parameter + - eventDrop now has an an allDay parameter + (this will affect those who use revertFunc, adjust parameter list) +- added 'prevYear' and 'nextYear' for buttons in header +- minor change for theme users, ui-state-hover not applied to active/inactive buttons +- added event-color-changing example in docs +- better defaults for right-to-left themed button icons + +## 1.3.2 (2009-10-13) + +- Bugfixes (please upgrade from 1.3.1!) + - squashed potential infinite loop when addMonths and addDays + is called with an invalid date + - $.fullCalendar.parseDate() now correctly parses IETF format + - when switching views, the 'today' button sticks inactive, fixed +- gotoDate now can accept a single Date argument +- documentation for changes in 1.3.1 and 1.3.2 now on website + +## 1.3.1 (2009-09-30) + +- Important Bugfixes (please upgrade from 1.3!) + - When current date was late in the month, for long months, and prev/next buttons + were clicked in month-view, some months would be skipped/repeated + - In certain time zones, daylight savings time would cause certain days + to be misnumbered in month-view +- Subtle change in way week interval is chosen when switching from month to basicWeek/basicDay view +- Added 'allDayDefault' option +- Added 'changeView' and 'render' methods + +## 1.3 (2009-09-21) + +- different 'views': month/basicWeek/basicDay +- more flexible 'header' system for buttons +- themable by jQuery UI themes +- resizable events (require jQuery UI resizable plugin) +- rescoped & rewritten CSS, enhanced default look +- cleaner css & rendering techniques for right-to-left +- reworked options & API to support multiple views / be consistent with jQuery UI +- refactoring of entire codebase + - broken into different JS & CSS files, assembled w/ build scripts + - new test suite for new features, uses firebug-lite +- refactored docs +- Options + - - date + - - defaultView + - - aspectRatio + - - disableResizing + - - monthNames (use instead of $.fullCalendar.monthNames) + - - monthNamesShort (use instead of $.fullCalendar.monthAbbrevs) + - - dayNames (use instead of $.fullCalendar.dayNames) + - - dayNamesShort (use instead of $.fullCalendar.dayAbbrevs) + - - theme + - - buttonText + - - buttonIcons + - x draggable -> editable/disableDragging + - x fixedWeeks -> weekMode + - x abbrevDayHeadings -> columnFormat + - x buttons/title -> header + - x eventDragOpacity -> dragOpacity + - x eventRevertDuration -> dragRevertDuration + - x weekStart -> firstDay + - x rightToLeft -> isRTL + - x showTime (use 'allDay' CalEvent property instead) +- Triggered Actions + - - eventResizeStart + - - eventResizeStop + - - eventResize + - x monthDisplay -> viewDisplay + - x resize -> windowResize + - 'eventDrop' params changed, can revert if ajax cuts out +- CalEvent Properties + - x showTime -> allDay + - x draggable -> editable + - 'end' is now INCLUSIVE when allDay=true + - 'url' now produces a real <a> tag, more native clicking/tab behavior +- Methods: + - - renderEvent + - x prevMonth -> prev + - x nextMonth -> next + - x prevYear/nextYear -> moveDate + - x refresh -> rerenderEvents/refetchEvents + - x removeEvent -> removeEvents + - x getEventsByID -> clientEvents +- Utilities: + - 'formatDate' format string completely changed (inspired by jQuery UI datepicker + datejs) + - 'formatDates' added to support date-ranges +- Google Calendar Options: + - x draggable -> editable +- Bugfixes + - gcal extension fetched 25 results max, now fetches all + +## 1.2.1 (2009-06-29) + +- bugfixes + - allows and corrects invalid end dates for events + - doesn't throw an error in IE while rendering when display:none + - fixed 'loading' callback when used w/ multiple addEventSource calls + - gcal className can now be an array + +## 1.2 (2009-05-31) + +- expanded API + - 'className' CalEvent attribute + - 'source' CalEvent attribute + - dynamically get/add/remove/update events of current month + - locale improvements: change month/day name text + - better date formatting ($.fullCalendar.formatDate) + - multiple 'event sources' allowed + - dynamically add/remove event sources +- options for prevYear and nextYear buttons +- docs have been reworked (include addition of Google Calendar docs) +- changed behavior of parseDate for number strings + (now interpets as unix timestamp, not MS times) +- bugfixes + - rightToLeft month start bug + - off-by-one errors with month formatting commands + - events from previous months sticking when clicking prev/next quickly +- Google Calendar API changed to work w/ multiple event sources + - can also provide 'className' and 'draggable' options +- date utilties moved from $ to $.fullCalendar +- more documentation in source code +- minified version of fullcalendar.js +- test suit (available from svn) +- top buttons now use `<button>` w/ an inner `<span>` for better css cusomization + - thus CSS has changed. IF UPGRADING FROM PREVIOUS VERSIONS, + UPGRADE YOUR FULLCALENDAR.CSS FILE + +## 1.1 (2009-05-10) + +- Added the following options: + - weekStart + - rightToLeft + - titleFormat + - timeFormat + - cacheParam + - resize +- Fixed rendering bugs + - Opera 9.25 (events placement & window resizing) + - IE6 (window resizing) +- Optimized window resizing for ALL browsers +- Events on same day now sorted by start time (but first by timespan) +- Correct z-index when dragging +- Dragging contained in overflow DIV for IE6 +- Modified fullcalendar.css + - for right-to-left support + - for variable start-of-week + - for IE6 resizing bug + - for THEAD and TBODY (in 1.0, just used TBODY, restructured in 1.1) + - IF UPGRADING FROM FULLCALENDAR 1.0, YOU MUST UPGRADE FULLCALENDAR.CSS diff --git a/fullcalendar-main/CONTRIBUTING.md b/fullcalendar-main/CONTRIBUTING.md new file mode 100644 index 0000000..2c34898 --- /dev/null +++ b/fullcalendar-main/CONTRIBUTING.md @@ -0,0 +1,14 @@ + +## Contributing Features + +The FullCalendar project welcomes PRs for new features, but because there are so many feature requests, and because every new feature requires refinement and maintenance, each PR will be prioritized against the project's other demands and might take a while to make it to an official release. + +Furthermore, each new feature should be designed as robustly as possible and be useful beyond the immediate usecase it was initially designed for. Feel free to start a ticket discussing the feature's specs before coding. + +## Contributing Bugfixes + +Please link to a bug ticket in the description of your PR. If a ticket doesn't exist, please create one. The ticket must contain a reduced test case. + +## Contributing Locale Data + +Please edit the source files in the `packages/core/locales/` directory. diff --git a/fullcalendar-main/LICENSE.md b/fullcalendar-main/LICENSE.md new file mode 100644 index 0000000..18ac667 --- /dev/null +++ b/fullcalendar-main/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Adam Shaw + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/fullcalendar-main/README.md b/fullcalendar-main/README.md new file mode 100644 index 0000000..379e2c3 --- /dev/null +++ b/fullcalendar-main/README.md @@ -0,0 +1,73 @@ +# FullCalendar + +Full-sized drag & drop calendar in JavaScript + +- [Project Website](https://fullcalendar.io/) +- [Documentation](https://fullcalendar.io/docs) +- [Changelog](CHANGELOG.md) +- [Support](https://fullcalendar.io/support) +- [License](LICENSE.md) +- [Roadmap](https://fullcalendar.io/roadmap) + +Connectors: + +- [React](https://github.com/fullcalendar/fullcalendar-react) +- [Angular](https://github.com/fullcalendar/fullcalendar-angular) +- [Vue 3](https://github.com/fullcalendar/fullcalendar-vue) | + [2](https://github.com/fullcalendar/fullcalendar-vue2) + +## Bundle + +The [FullCalendar Standard Bundle](bundle) is easier to install than individual plugins, though filesize will be larger. It works well with a CDN. + +## Installation + +Install the FullCalendar core package and any plugins you plan to use: + +```sh +npm install @fullcalendar/core @fullcalendar/interaction @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + interactionPlugin, + dayGridPlugin + ], + initialView: 'timeGridWeek', + editable: true, + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` + +## Development + +You must install this repo with [PNPM](https://pnpm.io/): + +``` +pnpm install +``` + +Available scripts (via `pnpm run <script>`): + +- `build` - build production-ready dist files +- `dev` - build & watch development dist files +- `test` - test headlessly +- `test:dev` - test interactively +- `lint` +- `clean` + +[Info about contributing code »](CONTRIBUTING.md) diff --git a/fullcalendar-main/bundle/.eslintrc.cjs b/fullcalendar-main/bundle/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/bundle/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/bundle/README.md b/fullcalendar-main/bundle/README.md new file mode 100644 index 0000000..daefe94 --- /dev/null +++ b/fullcalendar-main/bundle/README.md @@ -0,0 +1,58 @@ + +# FullCalendar Standard Bundle + +Easily render a full-sized drag & drop calendar with a combination of standard plugins + +This `fullcalendar` package bundles these plugins: + +- [@fullcalendar/core](https://github.com/fullcalendar/fullcalendar/tree/main/packages/core) +- [@fullcalendar/interaction](https://github.com/fullcalendar/fullcalendar/tree/main/packages/interaction) +- [@fullcalendar/daygrid](https://github.com/fullcalendar/fullcalendar/tree/main/packages/daygrid) +- [@fullcalendar/timegrid](https://github.com/fullcalendar/fullcalendar/tree/main/packages/timegrid) +- [@fullcalendar/list](https://github.com/fullcalendar/fullcalendar/tree/main/packages/list) +- [@fullcalendar/multimonth](https://github.com/fullcalendar/fullcalendar/tree/main/packages/multimonth) + +## Usage with CDN or ZIP archive + +Load the `index.global.min.js` file and use the `FullCalendar` global namespace: + +```html +<!DOCTYPE html> +<html> + <head> + <script src='https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js'></script> + <script> + + document.addEventListener('DOMContentLoaded', function() { + const calendarEl = document.getElementById('calendar') + const calendar = new FullCalendar.Calendar(calendarEl, { + initialView: 'dayGridMonth' + }) + calendar.render() + }) + + </script> + </head> + <body> + <div id='calendar'></div> + </body> +</html> +``` + +## Usage with NPM and ES modules + +```sh +npm install fullcalendar +``` + +```js +import { Calendar } from 'fullcalendar' + +document.addEventListener('DOMContentLoaded', function() { + const calendarEl = document.getElementById('calendar') + const calendar = new Calendar(calendarEl, { + initialView: 'dayGridMonth' + }) + calendar.render() +}) +``` diff --git a/fullcalendar-main/bundle/examples/background-events.html b/fullcalendar-main/bundle/examples/background-events.html new file mode 100644 index 0000000..911e0b3 --- /dev/null +++ b/fullcalendar-main/bundle/examples/background-events.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth' + }, + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + businessHours: true, // display business hours + editable: true, + selectable: true, + events: [ + { + title: 'Business Lunch', + start: '2023-01-03T13:00:00', + constraint: 'businessHours' + }, + { + title: 'Meeting', + start: '2023-01-13T11:00:00', + constraint: 'availableForMeeting', // defined below + color: '#257e4a' + }, + { + title: 'Conference', + start: '2023-01-18', + end: '2023-01-20' + }, + { + title: 'Party', + start: '2023-01-29T20:00:00' + }, + + // areas where "Meeting" must be dropped + { + groupId: 'availableForMeeting', + start: '2023-01-11T10:00:00', + end: '2023-01-11T16:00:00', + display: 'background' + }, + { + groupId: 'availableForMeeting', + start: '2023-01-13T10:00:00', + end: '2023-01-13T16:00:00', + display: 'background' + }, + + // red areas where no events can be dropped + { + start: '2023-01-24', + end: '2023-01-28', + overlap: false, + display: 'background', + color: '#ff9f89' + }, + { + start: '2023-01-06', + end: '2023-01-08', + overlap: false, + display: 'background', + color: '#ff9f89' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/daygrid-views.html b/fullcalendar-main/bundle/examples/daygrid-views.html new file mode 100644 index 0000000..9dafe43 --- /dev/null +++ b/fullcalendar-main/bundle/examples/daygrid-views.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prevYear,prev,next,nextYear today', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay' + }, + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/external-dragging-2cals.html b/fullcalendar-main/bundle/examples/external-dragging-2cals.html new file mode 100644 index 0000000..066685d --- /dev/null +++ b/fullcalendar-main/bundle/examples/external-dragging-2cals.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var srcCalendarEl = document.getElementById('source-calendar'); + var destCalendarEl = document.getElementById('destination-calendar'); + + var srcCalendar = new FullCalendar.Calendar(srcCalendarEl, { + editable: true, + initialDate: '2023-01-12', + events: [ + { + title: 'event1', + start: '2023-01-11T10:00:00', + end: '2023-01-11T16:00:00' + }, + { + title: 'event2', + start: '2023-01-13T10:00:00', + end: '2023-01-13T16:00:00' + } + ], + eventLeave: function(info) { + console.log('event left!', info.event); + } + }); + + var destCalendar = new FullCalendar.Calendar(destCalendarEl, { + initialDate: '2023-01-12', + editable: true, + droppable: true, // will let it receive events! + eventReceive: function(info) { + console.log('event received!', info.event); + } + }); + + srcCalendar.render(); + destCalendar.render(); + }); + +</script> +<style> + + body { + margin: 20px 0 0 20px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + } + + #source-calendar, + #destination-calendar { + float: left; + width: 600px; + margin: 0 20px 20px 0; + } + +</style> +</head> +<body> + + <div id='source-calendar'></div> + <div id='destination-calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/external-dragging-builtin.html b/fullcalendar-main/bundle/examples/external-dragging-builtin.html new file mode 100644 index 0000000..78fcd89 --- /dev/null +++ b/fullcalendar-main/bundle/examples/external-dragging-builtin.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + + /* initialize the external events + -----------------------------------------------------------------*/ + + var containerEl = document.getElementById('external-events-list'); + new FullCalendar.Draggable(containerEl, { + itemSelector: '.fc-event', + eventData: function(eventEl) { + return { + title: eventEl.innerText.trim() + } + } + }); + + //// the individual way to do it + // var containerEl = document.getElementById('external-events-list'); + // var eventEls = Array.prototype.slice.call( + // containerEl.querySelectorAll('.fc-event') + // ); + // eventEls.forEach(function(eventEl) { + // new FullCalendar.Draggable(eventEl, { + // eventData: { + // title: eventEl.innerText.trim(), + // } + // }); + // }); + + /* initialize the calendar + -----------------------------------------------------------------*/ + + var calendarEl = document.getElementById('calendar'); + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + editable: true, + droppable: true, // this allows things to be dropped onto the calendar + drop: function(arg) { + // is the "remove after drop" checkbox checked? + if (document.getElementById('drop-remove').checked) { + // if so, remove the element from the "Draggable Events" list + arg.draggedEl.parentNode.removeChild(arg.draggedEl); + } + } + }); + calendar.render(); + + }); + +</script> +<style> + + body { + margin-top: 40px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + } + + #external-events { + position: fixed; + left: 20px; + top: 20px; + width: 150px; + padding: 0 10px; + border: 1px solid #ccc; + background: #eee; + text-align: left; + } + + #external-events h4 { + font-size: 16px; + margin-top: 0; + padding-top: 1em; + } + + #external-events .fc-event { + margin: 3px 0; + cursor: move; + } + + #external-events p { + margin: 1.5em 0; + font-size: 11px; + color: #666; + } + + #external-events p input { + margin: 0; + vertical-align: middle; + } + + #calendar-wrap { + margin-left: 200px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + <div id='wrap'> + + <div id='external-events'> + <h4>Draggable Events</h4> + + <div id='external-events-list'> + <div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'> + <div class='fc-event-main'>My Event 1</div> + </div> + <div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'> + <div class='fc-event-main'>My Event 2</div> + </div> + <div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'> + <div class='fc-event-main'>My Event 3</div> + </div> + <div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'> + <div class='fc-event-main'>My Event 4</div> + </div> + <div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event'> + <div class='fc-event-main'>My Event 5</div> + </div> + </div> + + <p> + <input type='checkbox' id='drop-remove' /> + <label for='drop-remove'>remove after drop</label> + </p> + </div> + + <div id='calendar-wrap'> + <div id='calendar'></div> + </div> + + </div> +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/full-height.html b/fullcalendar-main/bundle/examples/full-height.html new file mode 100644 index 0000000..18b55f8 --- /dev/null +++ b/fullcalendar-main/bundle/examples/full-height.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + height: '100%', + expandRows: true, + slotMinTime: '08:00', + slotMaxTime: '20:00', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialView: 'dayGridMonth', + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + nowIndicator: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01', + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + html, body { + overflow: hidden; /* don't do scrollbars */ + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + .fc-header-toolbar { + /* + the calendar will be butting up against the edges, + but let's scoot in the header's buttons + */ + padding-top: 1em; + padding-left: 1em; + padding-right: 1em; + } + +</style> +</head> +<body> + + <div id='calendar-container'> + <div id='calendar'></div> + </div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/list-sticky-header.html b/fullcalendar-main/bundle/examples/list-sticky-header.html new file mode 100644 index 0000000..487af31 --- /dev/null +++ b/fullcalendar-main/bundle/examples/list-sticky-header.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + height: 'auto', + // stickyHeaderDates: false, // for disabling + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'listMonth,listYear' + }, + + // customize the button names, + // otherwise they'd all just say "list" + views: { + listMonth: { buttonText: 'list month' }, + listYear: { buttonText: 'list year' } + }, + + initialView: 'listYear', + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + events: [ + { + title: 'repeating event 1', + daysOfWeek: [ 1, 2, 3 ], + duration: '00:30' + }, + { + title: 'repeating event 2', + daysOfWeek: [ 1, 2, 3 ], + duration: '00:30' + }, + { + title: 'repeating event 3', + daysOfWeek: [ 1, 2, 3 ], + duration: '00:30' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/list-views.html b/fullcalendar-main/bundle/examples/list-views.html new file mode 100644 index 0000000..cb219d0 --- /dev/null +++ b/fullcalendar-main/bundle/examples/list-views.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'listDay,listWeek' + }, + + // customize the button names, + // otherwise they'd all just say "list" + views: { + listDay: { buttonText: 'list day' }, + listWeek: { buttonText: 'list week' } + }, + + initialView: 'listWeek', + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/month-view.html b/fullcalendar-main/bundle/examples/month-view.html new file mode 100644 index 0000000..dbd8861 --- /dev/null +++ b/fullcalendar-main/bundle/examples/month-view.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2023-01-12', + editable: true, + selectable: true, + businessHours: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/multimonth-view.html b/fullcalendar-main/bundle/examples/multimonth-view.html new file mode 100644 index 0000000..feb9d23 --- /dev/null +++ b/fullcalendar-main/bundle/examples/multimonth-view.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'multiMonthYear,dayGridMonth,timeGridWeek' + }, + initialView: 'multiMonthYear', + initialDate: '2023-01-12', + editable: true, + selectable: true, + dayMaxEvents: true, // allow "more" link when too many events + // multiMonthMaxColumns: 1, // guarantee single column + // showNonCurrentDates: true, + // fixedWeekCount: false, + // businessHours: true, + // weekends: false, + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1200px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/multiweek-view.html b/fullcalendar-main/bundle/examples/multiweek-view.html new file mode 100644 index 0000000..5a175fb --- /dev/null +++ b/fullcalendar-main/bundle/examples/multiweek-view.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridYear,dayGridMonth,timeGridWeek' + }, + initialView: 'dayGridYear', + initialDate: '2023-01-12', + editable: true, + selectable: true, + dayMaxEvents: true, // allow "more" link when too many events + // businessHours: true, + // weekends: false, + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1200px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/natural-height.html b/fullcalendar-main/bundle/examples/natural-height.html new file mode 100644 index 0000000..b62c41d --- /dev/null +++ b/fullcalendar-main/bundle/examples/natural-height.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2023-01-12', + initialView: 'timeGridWeek', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + height: 'auto', + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + selectMirror: true, + nowIndicator: true, + events: [ + { + title: 'All Day Event', + start: '2023-01-01', + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/selectable.html b/fullcalendar-main/bundle/examples/selectable.html new file mode 100644 index 0000000..785e90e --- /dev/null +++ b/fullcalendar-main/bundle/examples/selectable.html @@ -0,0 +1,123 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + initialDate: '2023-01-12', + navLinks: true, // can click day/week names to navigate views + selectable: true, + selectMirror: true, + select: function(arg) { + var title = prompt('Event Title:'); + if (title) { + calendar.addEvent({ + title: title, + start: arg.start, + end: arg.end, + allDay: arg.allDay + }) + } + calendar.unselect() + }, + eventClick: function(arg) { + if (confirm('Are you sure you want to delete this event?')) { + arg.event.remove() + } + }, + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01' + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/examples/timegrid-views.html b/fullcalendar-main/bundle/examples/timegrid-views.html new file mode 100644 index 0000000..9cd2527 --- /dev/null +++ b/fullcalendar-main/bundle/examples/timegrid-views.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2023-01-12', + initialView: 'timeGridWeek', + nowIndicator: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + selectMirror: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2023-01-01', + }, + { + title: 'Long Event', + start: '2023-01-07', + end: '2023-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2023-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13' + }, + { + title: 'Meeting', + start: '2023-01-12T10:30:00', + end: '2023-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2023-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2023-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2023-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2023-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2023-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2023-01-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/bundle/package.json b/fullcalendar-main/bundle/package.json new file mode 100644 index 0000000..de1608f --- /dev/null +++ b/fullcalendar-main/bundle/package.json @@ -0,0 +1,49 @@ +{ + "name": "fullcalendar", + "version": "6.1.11", + "title": "FullCalendar Standard Bundle", + "description": "Easily render a full-sized drag & drop calendar with a combination of standard plugins", + "homepage": "https://fullcalendar.io/docs/initialize-globals", + "dependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar/daygrid": "~6.1.11", + "@fullcalendar/interaction": "~6.1.11", + "@fullcalendar/list": "~6.1.11", + "@fullcalendar/multimonth": "~6.1.11", + "@fullcalendar/timegrid": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar", + "*": "" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/bundle/src/index.global.ts b/fullcalendar-main/bundle/src/index.global.ts new file mode 100644 index 0000000..d6040ae --- /dev/null +++ b/fullcalendar-main/bundle/src/index.global.ts @@ -0,0 +1,5 @@ +import * as Internal from '@fullcalendar/core/internal' +import * as Preact from '@fullcalendar/core/preact' + +export { Internal, Preact } +export * from './index.js' diff --git a/fullcalendar-main/bundle/src/index.ts b/fullcalendar-main/bundle/src/index.ts new file mode 100644 index 0000000..8d83cd5 --- /dev/null +++ b/fullcalendar-main/bundle/src/index.ts @@ -0,0 +1,17 @@ +import { globalPlugins } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import listPlugin from '@fullcalendar/list' +import multiMonthPlugin from '@fullcalendar/multimonth' + +globalPlugins.push( + interactionPlugin, + dayGridPlugin, + timeGridPlugin, + listPlugin, + multiMonthPlugin, +) + +export * from '@fullcalendar/core' +export * from '@fullcalendar/interaction' // for Draggable diff --git a/fullcalendar-main/package.json b/fullcalendar-main/package.json new file mode 100644 index 0000000..f9fbbfa --- /dev/null +++ b/fullcalendar-main/package.json @@ -0,0 +1,47 @@ +{ + "private": true, + "name": "@fullcalendar-monorepos/standard", + "description": "Full-sized drag & drop event calendar in JavaScript", + "version": "6.1.11", + "keywords": [ + "calendar", + "event", + "full-sized", + "fullcalendar" + ], + "homepage": "https://fullcalendar.io", + "bugs": "https://fullcalendar.io/reporting-bugs", + "repository": { + "type": "git", + "url": "https://github.com/fullcalendar/fullcalendar.git" + }, + "license": "MIT", + "author": { + "name": "Adam Shaw", + "email": "arshaw@arshaw.com", + "url": "http://arshaw.com/" + }, + "copyright": "2023 Adam Shaw", + "devDependencies": { + "@fullcalendar-scripts/standard": "*" + }, + "type": "module", + "scripts": { + "postinstall": "standard-scripts postinstall", + "lint": "standard-scripts lint", + "build": "standard-scripts build", + "dev": "standard-scripts dev", + "test": "standard-scripts test", + "test:dev": "standard-scripts test --dev", + "clean": "standard-scripts clean", + "ci": "pnpm run lint && pnpm run build && pnpm run test" + }, + "engines": { + "pnpm": ">=7.9.5" + }, + "pnpm": { + "patchedDependencies": { + "jasmine-jquery@2.1.1": "scripts/patches/jasmine-jquery@2.1.1.patch" + } + } +} diff --git a/fullcalendar-main/packages/bootstrap4/.eslintrc.cjs b/fullcalendar-main/packages/bootstrap4/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/bootstrap4/README.md b/fullcalendar-main/packages/bootstrap4/README.md new file mode 100644 index 0000000..8bce53a --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/README.md @@ -0,0 +1,46 @@ + +# FullCalendar Bootstrap 4 Plugin + +[Bootstrap 4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) theme for [FullCalendar](https://fullcalendar.io) + +> For [Bootstrap 5](https://getbootstrap.com/), use the [@fullcalendar/bootstrap5](https://github.com/fullcalendar/fullcalendar/tree/main/packages/bootstrap5) package + +## Installation + +First, ensure the necessary Bootstrap packages are installed: + +```sh +npm install bootstrap@4 @fortawesome/fontawesome-free +``` + +Then, install the FullCalendar core package, the Bootstrap plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/bootstrap @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import bootstrapPlugin from '@fullcalendar/bootstrap' +import dayGridPlugin from '@fullcalendar/daygrid' + +// import third-party stylesheets directly from your JS +import 'bootstrap/dist/css/bootstrap.css' +import '@fortawesome/fontawesome-free/css/all.css' // needs additional webpack config! + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + bootstrapPlugin, + dayGridPlugin + ], + themeSystem: 'bootstrap', // important! + initialView: 'dayGridMonth' +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/bootstrap4/package.json b/fullcalendar-main/packages/bootstrap4/package.json new file mode 100644 index 0000000..90d40fc --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/bootstrap", + "version": "6.1.11", + "title": "FullCalendar Bootstrap 4 Plugin", + "description": "Bootstrap 4 theme for FullCalendar", + "keywords": [ + "bootstrap", + "bootstrap4" + ], + "homepage": "https://fullcalendar.io/docs/bootstrap4", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./internal": {} + }, + "iifeGlobals": { + ".": "FullCalendar.Bootstrap", + "./internal": "FullCalendar.Bootstrap.Internal" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/bootstrap4/src/BootstrapTheme.ts b/fullcalendar-main/packages/bootstrap4/src/BootstrapTheme.ts new file mode 100644 index 0000000..973edaa --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/src/BootstrapTheme.ts @@ -0,0 +1,37 @@ +import { Theme } from '@fullcalendar/core/internal' + +class BootstrapTheme extends Theme { +} + +BootstrapTheme.prototype.classes = { + root: 'fc-theme-bootstrap', // TODO: compute this off of registered theme name + table: 'table-bordered', // don't attache the `table` class. we only want the borders, not any layout + tableCellShaded: 'table-active', + buttonGroup: 'btn-group', + button: 'btn btn-primary', + buttonActive: 'active', + popover: 'popover', + popoverHeader: 'popover-header', + popoverContent: 'popover-body', +} + +BootstrapTheme.prototype.baseIconClass = 'fa' +BootstrapTheme.prototype.iconClasses = { + close: 'fa-times', + prev: 'fa-chevron-left', + next: 'fa-chevron-right', + prevYear: 'fa-angle-double-left', + nextYear: 'fa-angle-double-right', +} +BootstrapTheme.prototype.rtlIconClasses = { + prev: 'fa-chevron-right', + next: 'fa-chevron-left', + prevYear: 'fa-angle-double-right', + nextYear: 'fa-angle-double-left', +} + +BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome' // TODO: make TS-friendly. move the option-processing into this plugin +BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome' +BootstrapTheme.prototype.iconOverridePrefix = 'fa-' + +export { BootstrapTheme } diff --git a/fullcalendar-main/packages/bootstrap4/src/index.css b/fullcalendar-main/packages/bootstrap4/src/index.css new file mode 100644 index 0000000..5f0cb05 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/src/index.css @@ -0,0 +1,12 @@ + +.fc-theme-bootstrap { + + & a:not([href]) { + color: inherit; // natural color for navlinks + } + + & .fc-more-link:hover { + text-decoration: none; + } + +} diff --git a/fullcalendar-main/packages/bootstrap4/src/index.global.ts b/fullcalendar-main/packages/bootstrap4/src/index.global.ts new file mode 100644 index 0000000..c70b8e6 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/src/index.global.ts @@ -0,0 +1,8 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' +import * as Internal from './internal.js' + +globalPlugins.push(plugin) + +export { plugin as default, Internal } +export * from './index.js' diff --git a/fullcalendar-main/packages/bootstrap4/src/index.ts b/fullcalendar-main/packages/bootstrap4/src/index.ts new file mode 100644 index 0000000..66e13ee --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/src/index.ts @@ -0,0 +1,10 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { BootstrapTheme } from './BootstrapTheme.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + themeClasses: { + bootstrap: BootstrapTheme, + }, +}) as PluginDef diff --git a/fullcalendar-main/packages/bootstrap4/src/internal.ts b/fullcalendar-main/packages/bootstrap4/src/internal.ts new file mode 100644 index 0000000..5361cda --- /dev/null +++ b/fullcalendar-main/packages/bootstrap4/src/internal.ts @@ -0,0 +1,3 @@ +import './index.css' + +export { BootstrapTheme } from './BootstrapTheme.js' diff --git a/fullcalendar-main/packages/bootstrap5/.eslintrc.cjs b/fullcalendar-main/packages/bootstrap5/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/bootstrap5/README.md b/fullcalendar-main/packages/bootstrap5/README.md new file mode 100644 index 0000000..a83ec28 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/README.md @@ -0,0 +1,44 @@ + +# FullCalendar Bootstrap 5 Plugin + +[Bootstrap 5](https://getbootstrap.com/) theme for [FullCalendar](https://fullcalendar.io) + +## Installation + +First, ensure the necessary Bootstrap packages are installed: + +```sh +npm install bootstrap@5 bootstrap-icons +``` + +Then, install the FullCalendar core package, the Bootstrap plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/bootstrap5 @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import bootstrap5Plugin from '@fullcalendar/bootstrap5' +import dayGridPlugin from '@fullcalendar/daygrid' + +// import bootstrap stylesheets directly from your JS +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-icons/font/bootstrap-icons.css' // needs additional webpack config! + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + bootstrap5Plugin, + dayGridPlugin + ], + themeSystem: 'bootstrap5', // important! + initialView: 'dayGridMonth' +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/bootstrap5/package.json b/fullcalendar-main/packages/bootstrap5/package.json new file mode 100644 index 0000000..fe3a718 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/bootstrap5", + "version": "6.1.11", + "title": "FullCalendar Bootstrap 5 Plugin", + "description": "Bootstrap 5 theme for FullCalendar", + "keywords": [ + "bootstrap", + "bootstrap5" + ], + "homepage": "https://fullcalendar.io/docs/bootstrap5", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./internal": {} + }, + "iifeGlobals": { + ".": "FullCalendar.Bootstrap5", + "./internal": "FullCalendar.Bootstrap5.Internal" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/bootstrap5/src/BootstrapTheme.ts b/fullcalendar-main/packages/bootstrap5/src/BootstrapTheme.ts new file mode 100644 index 0000000..dd67468 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/src/BootstrapTheme.ts @@ -0,0 +1,37 @@ +import { Theme } from '@fullcalendar/core/internal' + +export class BootstrapTheme extends Theme { +} + +BootstrapTheme.prototype.classes = { + root: 'fc-theme-bootstrap5', + tableCellShaded: 'fc-theme-bootstrap5-shaded', + buttonGroup: 'btn-group', + button: 'btn btn-primary', + buttonActive: 'active', + popover: 'popover', + popoverHeader: 'popover-header', + popoverContent: 'popover-body', +} + +BootstrapTheme.prototype.baseIconClass = 'bi' +BootstrapTheme.prototype.iconClasses = { + close: 'bi-x-lg', + prev: 'bi-chevron-left', + next: 'bi-chevron-right', + prevYear: 'bi-chevron-double-left', + nextYear: 'bi-chevron-double-right', +} +BootstrapTheme.prototype.rtlIconClasses = { + prev: 'bi-chevron-right', + next: 'bi-chevron-left', + prevYear: 'bi-chevron-double-right', + nextYear: 'bi-chevron-double-left', +} + +// wtf +BootstrapTheme.prototype.iconOverrideOption = 'buttonIcons' // TODO: make TS-friendly +BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'icon' +BootstrapTheme.prototype.iconOverridePrefix = 'bi-' + +export { Theme } diff --git a/fullcalendar-main/packages/bootstrap5/src/index.css b/fullcalendar-main/packages/bootstrap5/src/index.css new file mode 100644 index 0000000..776b1b7 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/src/index.css @@ -0,0 +1,25 @@ + +.fc-theme-bootstrap5 { + + & a:not([href]) { + color: inherit; + text-decoration: inherit; + } + + & .fc-list, + & .fc-scrollgrid, + & td, + & th { + border: 1px solid var(--bs-gray-400); + } + + // HACK: reapply core styles after highe-precedence border statement above + & .fc-scrollgrid { + border-right-width: 0; + border-bottom-width: 0; + } +} + +.fc-theme-bootstrap5-shaded { + background-color: var(--bs-gray-200); +} diff --git a/fullcalendar-main/packages/bootstrap5/src/index.global.ts b/fullcalendar-main/packages/bootstrap5/src/index.global.ts new file mode 100644 index 0000000..c70b8e6 --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/src/index.global.ts @@ -0,0 +1,8 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' +import * as Internal from './internal.js' + +globalPlugins.push(plugin) + +export { plugin as default, Internal } +export * from './index.js' diff --git a/fullcalendar-main/packages/bootstrap5/src/index.ts b/fullcalendar-main/packages/bootstrap5/src/index.ts new file mode 100644 index 0000000..280071f --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/src/index.ts @@ -0,0 +1,10 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { BootstrapTheme } from './BootstrapTheme.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + themeClasses: { + bootstrap5: BootstrapTheme, + }, +}) as PluginDef diff --git a/fullcalendar-main/packages/bootstrap5/src/internal.ts b/fullcalendar-main/packages/bootstrap5/src/internal.ts new file mode 100644 index 0000000..5361cda --- /dev/null +++ b/fullcalendar-main/packages/bootstrap5/src/internal.ts @@ -0,0 +1,3 @@ +import './index.css' + +export { BootstrapTheme } from './BootstrapTheme.js' diff --git a/fullcalendar-main/packages/core/.eslintrc.cjs b/fullcalendar-main/packages/core/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/core/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/core/README.md b/fullcalendar-main/packages/core/README.md new file mode 100644 index 0000000..b91980e --- /dev/null +++ b/fullcalendar-main/packages/core/README.md @@ -0,0 +1,44 @@ + +# FullCalendar Core + +FullCalendar core package for rendering a calendar + +## Installation + +This package is never used alone. Use it with least one plugin (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/daygrid +``` + +## Usage + +First, ensure there's a DOM element for your calendar to render into: + +```html +<body> + <div id='calendar'></div> +</body> +``` + +Then, instantiate a Calendar object with [options](https://fullcalendar.io/docs#toc) and call its `render` method: + +```js +import { Calendar } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + dayGridPlugin + // any other plugins + ], + initialView: 'dayGridMonth', + weekends: false, + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/core/package.json b/fullcalendar-main/packages/core/package.json new file mode 100644 index 0000000..875d318 --- /dev/null +++ b/fullcalendar-main/packages/core/package.json @@ -0,0 +1,58 @@ +{ + "name": "@fullcalendar/core", + "version": "6.1.11", + "title": "FullCalendar Core", + "description": "FullCalendar core package for rendering a calendar", + "dependencies": { + "preact": "~10.12.1" + }, + "devDependencies": { + "@fullcalendar-scripts/standard": "*", + "globby": "^13.1.2", + "handlebars": "^4.1.2" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./preact": {}, + "./internal": {}, + "./locales-all": { + "iife": true, + "generator": "./scripts/generate-locales-all.js" + }, + "./locales/*": { + "iife": true, + "iifeGenerator": "./scripts/generate-locale-iife.js" + } + }, + "iifeGlobals": { + ".": "FullCalendar", + "./preact": "FullCalendar.Preact", + "./internal": "FullCalendar.Internal", + "preact": "", + "preact/compat": "" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/core/scripts/generate-locale-iife.js b/fullcalendar-main/packages/core/scripts/generate-locale-iife.js new file mode 100644 index 0000000..48ab798 --- /dev/null +++ b/fullcalendar-main/packages/core/scripts/generate-locale-iife.js @@ -0,0 +1,21 @@ +import { join as joinPaths, basename } from 'path' +import { fileURLToPath } from 'url' +import { readFile } from 'fs/promises' +import handlebars from 'handlebars' + +const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..') +const templatePath = joinPaths(thisPkgDir, 'src/locales/global.js.tpl') + +export function getWatchPaths() { + return [templatePath, templatePath] +} + +export default async function(config) { + const localeCode = basename(config.entryAlias) + + const templateText = await readFile(templatePath, 'utf8') + const template = handlebars.compile(templateText) + const code = template({ localeCode }) + + return code +} diff --git a/fullcalendar-main/packages/core/scripts/generate-locales-all.js b/fullcalendar-main/packages/core/scripts/generate-locales-all.js new file mode 100644 index 0000000..d946336 --- /dev/null +++ b/fullcalendar-main/packages/core/scripts/generate-locales-all.js @@ -0,0 +1,27 @@ +import { join as joinPaths } from 'path' +import { fileURLToPath } from 'url' +import { readFile } from 'fs/promises' +import { globby } from 'globby' +import handlebars from 'handlebars' + +const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..') +const templatePath = joinPaths(thisPkgDir, 'src/locales-all.js.tpl') +const localesDir = joinPaths(thisPkgDir, 'src/locales') + +export function getWatchPaths() { + return [ + templatePath, + localesDir, + ] +} + +export default async function() { + const localeFilenames = await globby('*.ts', { cwd: localesDir }) + const localeCodes = localeFilenames.map((filename) => filename.replace(/\.ts$/, '')) + + const templateText = await readFile(templatePath, 'utf8') + const template = handlebars.compile(templateText) + const code = template({ localeCodes }) + + return code +} diff --git a/fullcalendar-main/packages/core/src/Calendar.tsx b/fullcalendar-main/packages/core/src/Calendar.tsx new file mode 100644 index 0000000..2299094 --- /dev/null +++ b/fullcalendar-main/packages/core/src/Calendar.tsx @@ -0,0 +1,156 @@ +import { CalendarOptions } from './options.js' +import { DelayedRunner } from './util/DelayedRunner.js' +import { CalendarDataManager } from './reducers/CalendarDataManager.js' +import { Action } from './reducers/Action.js' +import { CalendarData } from './reducers/data-types.js' +import { CalendarRoot } from './CalendarRoot.js' +import { CalendarContent } from './CalendarContent.js' +import { createElement, render, flushSync } from './preact.js' +import { isArraysEqual } from './util/array.js' +import { CssDimValue } from './scrollgrid/util.js' +import { applyStyleProp } from './util/dom-manip.js' +import { RenderId } from './content-inject/RenderId.js' +import { CalendarImpl } from './api/CalendarImpl.js' +import { ensureElHasStyles } from './styleUtils.js' + +export class Calendar extends CalendarImpl { + el: HTMLElement + + private currentData: CalendarData + private renderRunner: DelayedRunner + private isRendering = false + private isRendered = false + private currentClassNames: string[] = [] + private customContentRenderId = 0 + + constructor(el: HTMLElement, optionOverrides: CalendarOptions = {}) { + super() + ensureElHasStyles(el) + + this.el = el + this.renderRunner = new DelayedRunner(this.handleRenderRequest) + + new CalendarDataManager({ // eslint-disable-line no-new + optionOverrides, + calendarApi: this, + onAction: this.handleAction, + onData: this.handleData, + }) + } + + private handleAction = (action: Action) => { + // actions we know we want to render immediately + switch (action.type) { + case 'SET_EVENT_DRAG': + case 'SET_EVENT_RESIZE': + this.renderRunner.tryDrain() + } + } + + private handleData = (data: CalendarData) => { + this.currentData = data + this.renderRunner.request(data.calendarOptions.rerenderDelay) + } + + private handleRenderRequest = () => { + if (this.isRendering) { + this.isRendered = true + let { currentData } = this + + flushSync(() => { + render( + <CalendarRoot options={currentData.calendarOptions} theme={currentData.theme} emitter={currentData.emitter}> + {(classNames, height, isHeightAuto, forPrint) => { + this.setClassNames(classNames) + this.setHeight(height) + + return ( + <RenderId.Provider value={this.customContentRenderId}> + <CalendarContent + isHeightAuto={isHeightAuto} + forPrint={forPrint} + {...currentData} + /> + </RenderId.Provider> + ) + }} + </CalendarRoot>, + this.el, + ) + }) + } else if (this.isRendered) { + this.isRendered = false + render(null, this.el) + + this.setClassNames([]) + this.setHeight('') + } + } + + render() { + let wasRendering = this.isRendering + + if (!wasRendering) { + this.isRendering = true + } else { + this.customContentRenderId += 1 + } + + this.renderRunner.request() + + if (wasRendering) { + this.updateSize() + } + } + + destroy(): void { + if (this.isRendering) { + this.isRendering = false + this.renderRunner.request() + } + } + + updateSize(): void { + flushSync(() => { + super.updateSize() + }) + } + + batchRendering(func): void { + this.renderRunner.pause('batchRendering') + func() + this.renderRunner.resume('batchRendering') + } + + pauseRendering() { // available to plugins + this.renderRunner.pause('pauseRendering') + } + + resumeRendering() { // available to plugins + this.renderRunner.resume('pauseRendering', true) + } + + resetOptions(optionOverrides, changedOptionNames?: string[]) { + this.currentDataManager.resetOptions(optionOverrides, changedOptionNames) + } + + private setClassNames(classNames: string[]) { + if (!isArraysEqual(classNames, this.currentClassNames)) { + let { classList } = this.el + + for (let className of this.currentClassNames) { + classList.remove(className) + } + + for (let className of classNames) { + classList.add(className) + } + + this.currentClassNames = classNames + } + } + + private setHeight(height: CssDimValue) { + applyStyleProp(this.el, 'height', height) + } +} diff --git a/fullcalendar-main/packages/core/src/CalendarContent.tsx b/fullcalendar-main/packages/core/src/CalendarContent.tsx new file mode 100644 index 0000000..4eb5ecc --- /dev/null +++ b/fullcalendar-main/packages/core/src/CalendarContent.tsx @@ -0,0 +1,289 @@ +import { ViewContextType, buildViewContext } from './ViewContext.js' +import { ViewSpec } from './structs/view-spec.js' +import { ViewProps } from './View.js' +import { Toolbar } from './Toolbar.js' +import { DateProfileGenerator, DateProfile } from './DateProfileGenerator.js' +import { rangeContainsMarker } from './datelib/date-range.js' +import { memoize } from './util/memoize.js' +import { DateMarker } from './datelib/marker.js' +import { CalendarData } from './reducers/data-types.js' +import { ViewPropsTransformerClass } from './plugin-system-struct.js' +import { createElement, createRef, Fragment, VNode } from './preact.js' +import { ViewHarness } from './ViewHarness.js' +import { + Interaction, + InteractionSettingsInput, + InteractionClass, + parseInteractionSettings, + interactionSettingsStore, +} from './interactions/interaction.js' +import { DateComponent } from './component/DateComponent.js' +import { EventClicking } from './interactions/EventClicking.js' +import { EventHovering } from './interactions/EventHovering.js' +import { getNow } from './reducers/current-date.js' +import { CalendarInteraction } from './calendar-utils.js' +import { DelayedRunner } from './util/DelayedRunner.js' +import { PureComponent } from './vdom-util.js' +import { getUniqueDomId } from './util/dom-manip.js' + +export interface CalendarContentProps extends CalendarData { + forPrint: boolean + isHeightAuto: boolean +} + +export class CalendarContent extends PureComponent<CalendarContentProps> { + private buildViewContext = memoize(buildViewContext) + private buildViewPropTransformers = memoize(buildViewPropTransformers) + private buildToolbarProps = memoize(buildToolbarProps) + private headerRef = createRef<Toolbar>() + private footerRef = createRef<Toolbar>() + private interactionsStore: { [componentUid: string]: Interaction[] } = {} + private calendarInteractions: CalendarInteraction[] + + // eslint-disable-next-line + state = { + viewLabelId: getUniqueDomId(), + } + + /* + renders INSIDE of an outer div + */ + render() { + let { props } = this + let { toolbarConfig, options } = props + + let toolbarProps = this.buildToolbarProps( + props.viewSpec, + props.dateProfile, + props.dateProfileGenerator, + props.currentDate, + getNow(props.options.now, props.dateEnv), // TODO: use NowTimer???? + props.viewTitle, + ) + + let viewVGrow = false + let viewHeight: string | number = '' + let viewAspectRatio: number | undefined + + if (props.isHeightAuto || props.forPrint) { + viewHeight = '' + } else if (options.height != null) { + viewVGrow = true + } else if (options.contentHeight != null) { + viewHeight = options.contentHeight + } else { + viewAspectRatio = Math.max(options.aspectRatio, 0.5) // prevent from getting too tall + } + + let viewContext = this.buildViewContext( + props.viewSpec, + props.viewApi, + props.options, + props.dateProfileGenerator, + props.dateEnv, + props.theme, + props.pluginHooks, + props.dispatch, + props.getCurrentData, + props.emitter, + props.calendarApi, + this.registerInteractiveComponent, + this.unregisterInteractiveComponent, + ) + + let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle) + ? this.state.viewLabelId + : undefined + + return ( + <ViewContextType.Provider value={viewContext}> + {toolbarConfig.header && ( + <Toolbar + ref={this.headerRef} + extraClassName="fc-header-toolbar" + model={toolbarConfig.header} + titleId={viewLabelId} + {...toolbarProps} + /> + )} + <ViewHarness + liquid={viewVGrow} + height={viewHeight} + aspectRatio={viewAspectRatio} + labeledById={viewLabelId} + > + {this.renderView(props)} + {this.buildAppendContent()} + </ViewHarness> + {toolbarConfig.footer && ( + <Toolbar + ref={this.footerRef} + extraClassName="fc-footer-toolbar" + model={toolbarConfig.footer} + titleId="" + {...toolbarProps} + /> + )} + </ViewContextType.Provider> + ) + } + + componentDidMount() { + let { props } = this + + this.calendarInteractions = props.pluginHooks.calendarInteractions + .map((CalendarInteractionClass) => new CalendarInteractionClass(props)) + + window.addEventListener('resize', this.handleWindowResize) + + let { propSetHandlers } = props.pluginHooks + for (let propName in propSetHandlers) { + propSetHandlers[propName](props[propName], props) + } + } + + componentDidUpdate(prevProps: CalendarContentProps) { + let { props } = this + + let { propSetHandlers } = props.pluginHooks + for (let propName in propSetHandlers) { + if (props[propName] !== prevProps[propName]) { + propSetHandlers[propName](props[propName], props) + } + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleWindowResize) + this.resizeRunner.clear() + + for (let interaction of this.calendarInteractions) { + interaction.destroy() + } + + this.props.emitter.trigger('_unmount') + } + + buildAppendContent(): VNode { + let { props } = this + + let children = props.pluginHooks.viewContainerAppends.map( + (buildAppendContent) => buildAppendContent(props), + ) + + return createElement(Fragment, {}, ...children) + } + + renderView(props: CalendarContentProps) { + let { pluginHooks } = props + let { viewSpec } = props + + let viewProps: ViewProps = { + dateProfile: props.dateProfile, + businessHours: props.businessHours, + eventStore: props.renderableEventStore, // ! + eventUiBases: props.eventUiBases, + dateSelection: props.dateSelection, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize, + isHeightAuto: props.isHeightAuto, + forPrint: props.forPrint, + } + + let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers) + + for (let transformer of transformers) { + Object.assign( + viewProps, + transformer.transform(viewProps, props), + ) + } + + let ViewComponent = viewSpec.component + + return ( + <ViewComponent {...viewProps} /> + ) + } + + // Component Registration + // ----------------------------------------------------------------------------------------------------------------- + + registerInteractiveComponent = (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => { + let settings = parseInteractionSettings(component, settingsInput) + let DEFAULT_INTERACTIONS: InteractionClass[] = [ + EventClicking, + EventHovering, + ] + let interactionClasses: InteractionClass[] = DEFAULT_INTERACTIONS.concat( + this.props.pluginHooks.componentInteractions, + ) + let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings)) + + this.interactionsStore[component.uid] = interactions + interactionSettingsStore[component.uid] = settings + } + + unregisterInteractiveComponent = (component: DateComponent<any>) => { + let listeners = this.interactionsStore[component.uid] + + if (listeners) { + for (let listener of listeners) { + listener.destroy() + } + delete this.interactionsStore[component.uid] + } + + delete interactionSettingsStore[component.uid] + } + + // Resizing + // ----------------------------------------------------------------------------------------------------------------- + + resizeRunner = new DelayedRunner(() => { + this.props.emitter.trigger('_resize', true) // should window resizes be considered "forced" ? + this.props.emitter.trigger('windowResize', { view: this.props.viewApi }) + }) + + handleWindowResize = (ev: UIEvent) => { + let { options } = this.props + + if ( + options.handleWindowResize && + ev.target === window // avoid jqui events + ) { + this.resizeRunner.request(options.windowResizeDelay) + } + } +} + +function buildToolbarProps( + viewSpec: ViewSpec, + dateProfile: DateProfile, + dateProfileGenerator: DateProfileGenerator, + currentDate: DateMarker, + now: DateMarker, + title: string, +) { + // don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid + let todayInfo = dateProfileGenerator.build(now, undefined, false) // TODO: need `undefined` or else INFINITE LOOP for some reason + let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false) + let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false) + + return { + title, + activeButton: viewSpec.type, + navUnit: viewSpec.singleUnit, + isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now), + isPrevEnabled: prevInfo.isValid, + isNextEnabled: nextInfo.isValid, + } +} + +// Plugin +// ----------------------------------------------------------------------------------------------------------------- + +function buildViewPropTransformers(theClasses: ViewPropsTransformerClass[]) { + return theClasses.map((TheClass) => new TheClass()) +} diff --git a/fullcalendar-main/packages/core/src/CalendarContext.ts b/fullcalendar-main/packages/core/src/CalendarContext.ts new file mode 100644 index 0000000..acba94a --- /dev/null +++ b/fullcalendar-main/packages/core/src/CalendarContext.ts @@ -0,0 +1,17 @@ +import { DateEnv } from './datelib/env.js' +import { BaseOptionsRefined, CalendarListeners } from './options.js' +import { PluginHooks } from './plugin-system-struct.js' +import { Emitter } from './common/Emitter.js' +import { Action } from './reducers/Action.js' +import { CalendarImpl } from './api/CalendarImpl.js' +import { CalendarData } from './reducers/data-types.js' + +export interface CalendarContext { + dateEnv: DateEnv + options: BaseOptionsRefined // does not have calendar-specific properties. aims to be compatible with ViewOptionsRefined + pluginHooks: PluginHooks + emitter: Emitter<CalendarListeners> + dispatch(action: Action): void + getCurrentData(): CalendarData + calendarApi: CalendarImpl +} diff --git a/fullcalendar-main/packages/core/src/CalendarRoot.tsx b/fullcalendar-main/packages/core/src/CalendarRoot.tsx new file mode 100644 index 0000000..1b27c77 --- /dev/null +++ b/fullcalendar-main/packages/core/src/CalendarRoot.tsx @@ -0,0 +1,70 @@ +import { ComponentChildren, flushSync } from './preact.js' +import { BaseComponent } from './vdom-util.js' +import { CssDimValue } from './scrollgrid/util.js' +import { CalendarOptions, CalendarListeners } from './options.js' +import { Theme } from './theme/Theme.js' +import { getCanVGrowWithinCell } from './util/table-styling.js' +import { Emitter } from './common/Emitter.js' + +export interface CalendarRootProps { + options: CalendarOptions + theme: Theme + emitter: Emitter<CalendarListeners> + children: (classNames: string[], height: CssDimValue, isHeightAuto: boolean, forPrint: boolean) => ComponentChildren +} + +interface CalendarRootState { + forPrint: boolean +} + +export class CalendarRoot extends BaseComponent<CalendarRootProps, CalendarRootState> { + state = { + forPrint: false, + } + + render() { + let { props } = this + let { options } = props + let { forPrint } = this.state + + let isHeightAuto = forPrint || options.height === 'auto' || options.contentHeight === 'auto' + let height = (!isHeightAuto && options.height != null) ? options.height : '' + + let classNames: string[] = [ + 'fc', + forPrint ? 'fc-media-print' : 'fc-media-screen', + `fc-direction-${options.direction}`, + props.theme.getClass('root'), + ] + + if (!getCanVGrowWithinCell()) { + classNames.push('fc-liquid-hack') + } + + return props.children(classNames, height, isHeightAuto, forPrint) + } + + componentDidMount() { + let { emitter } = this.props + emitter.on('_beforeprint', this.handleBeforePrint) + emitter.on('_afterprint', this.handleAfterPrint) + } + + componentWillUnmount() { + let { emitter } = this.props + emitter.off('_beforeprint', this.handleBeforePrint) + emitter.off('_afterprint', this.handleAfterPrint) + } + + handleBeforePrint = () => { + flushSync(() => { + this.setState({ forPrint: true }) + }) + } + + handleAfterPrint = () => { + flushSync(() => { + this.setState({ forPrint: false }) + }) + } +} diff --git a/fullcalendar-main/packages/core/src/DateProfileGenerator.ts b/fullcalendar-main/packages/core/src/DateProfileGenerator.ts new file mode 100644 index 0000000..fcd9ec1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/DateProfileGenerator.ts @@ -0,0 +1,455 @@ +import { DateMarker, startOfDay, addDays } from './datelib/marker.js' +import { Duration, createDuration, asRoughDays, asRoughMs, greatestDurationDenominator } from './datelib/duration.js' +import { + DateRange, + OpenDateRange, + constrainMarkerToRange, + intersectRanges, + rangeContainsMarker, + rangesIntersect, + parseRange, + DateRangeInput, +} from './datelib/date-range.js' +import { DateEnv, DateInput } from './datelib/env.js' +import { computeVisibleDayRange } from './util/date.js' +import { getNow } from './reducers/current-date.js' +import { CalendarImpl } from './api/CalendarImpl.js' + +export interface DateProfile { + currentDate: DateMarker + isValid: boolean + validRange: OpenDateRange // dates in past/present/future the user can interact with + renderRange: DateRange // dates that get rendered (even if they're completely blank) + activeRange: DateRange | null // dates where content is rendered + currentRange: DateRange // dates for current interval (TODO: include slotMinTime/slotMaxTime?) + currentRangeUnit: string + isRangeAllDay: boolean + dateIncrement: Duration + slotMinTime: Duration + slotMaxTime: Duration +} + +export interface DateProfileGeneratorProps extends DateProfileOptions { + dateProfileGeneratorClass: DateProfileGeneratorClass // not used by DateProfileGenerator itself + duration: Duration + durationUnit: string + usesMinMaxTime: boolean + dateEnv: DateEnv + calendarApi: CalendarImpl +} + +export interface DateProfileOptions { + slotMinTime: Duration + slotMaxTime: Duration + showNonCurrentDates?: boolean + dayCount?: number + dateAlignment?: string + dateIncrement?: Duration + hiddenDays?: number[] + weekends?: boolean + nowInput?: DateInput | (() => DateInput) + validRangeInput?: DateRangeInput | ((this: CalendarImpl, nowDate: Date) => DateRangeInput) + visibleRangeInput?: DateRangeInput | ((this: CalendarImpl, nowDate: Date) => DateRangeInput) + fixedWeekCount?: boolean +} + +export type DateProfileGeneratorClass = { + new(props: DateProfileGeneratorProps): DateProfileGenerator +} + +export class DateProfileGenerator { // only publicly used for isHiddenDay :( + nowDate: DateMarker + + isHiddenDayHash: boolean[] + + constructor(protected props: DateProfileGeneratorProps) { + this.nowDate = getNow(props.nowInput, props.dateEnv) + this.initHiddenDays() + } + + /* Date Range Computation + ------------------------------------------------------------------------------------------------------------------*/ + + // Builds a structure with info about what the dates/ranges will be for the "prev" view. + buildPrev(currentDateProfile: DateProfile, currentDate: DateMarker, forceToValid?: boolean): DateProfile { + let { dateEnv } = this.props + + let prevDate = dateEnv.subtract( + dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement, + ) + + return this.build(prevDate, -1, forceToValid) + } + + // Builds a structure with info about what the dates/ranges will be for the "next" view. + buildNext(currentDateProfile: DateProfile, currentDate: DateMarker, forceToValid?: boolean): DateProfile { + let { dateEnv } = this.props + + let nextDate = dateEnv.add( + dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month + currentDateProfile.dateIncrement, + ) + + return this.build(nextDate, 1, forceToValid) + } + + // Builds a structure holding dates/ranges for rendering around the given date. + // Optional direction param indicates whether the date is being incremented/decremented + // from its previous value. decremented = -1, incremented = 1 (default). + build(currentDate: DateMarker, direction?, forceToValid = true): DateProfile { + let { props } = this + let validRange: DateRange + let currentInfo + let isRangeAllDay + let renderRange: DateRange + let activeRange: DateRange + let isValid + + validRange = this.buildValidRange() + validRange = this.trimHiddenDays(validRange) + + if (forceToValid) { + currentDate = constrainMarkerToRange(currentDate, validRange) + } + + currentInfo = this.buildCurrentRangeInfo(currentDate, direction) + isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit) + renderRange = this.buildRenderRange( + this.trimHiddenDays(currentInfo.range), + currentInfo.unit, + isRangeAllDay, + ) + renderRange = this.trimHiddenDays(renderRange) + activeRange = renderRange + + if (!props.showNonCurrentDates) { + activeRange = intersectRanges(activeRange, currentInfo.range) + } + + activeRange = this.adjustActiveRange(activeRange) + activeRange = intersectRanges(activeRange, validRange) // might return null + + // it's invalid if the originally requested date is not contained, + // or if the range is completely outside of the valid range. + isValid = rangesIntersect(currentInfo.range, validRange) + + // HACK: constrain to render-range so `currentDate` is more useful to view rendering + if (!rangeContainsMarker(renderRange, currentDate)) { + currentDate = renderRange.start + } + + return { + currentDate, + + // constraint for where prev/next operations can go and where events can be dragged/resized to. + // an object with optional start and end properties. + validRange, + + // range the view is formally responsible for. + // for example, a month view might have 1st-31st, excluding padded dates + currentRange: currentInfo.range, + + // name of largest unit being displayed, like "month" or "week" + currentRangeUnit: currentInfo.unit, + + isRangeAllDay, + + // dates that display events and accept drag-n-drop + // will be `null` if no dates accept events + activeRange, + + // date range with a rendered skeleton + // includes not-active days that need some sort of DOM + renderRange, + + // Duration object that denotes the first visible time of any given day + slotMinTime: props.slotMinTime, + + // Duration object that denotes the exclusive visible end time of any given day + slotMaxTime: props.slotMaxTime, + + isValid, + + // how far the current date will move for a prev/next operation + dateIncrement: this.buildDateIncrement(currentInfo.duration), + // pass a fallback (might be null) ^ + } + } + + // Builds an object with optional start/end properties. + // Indicates the minimum/maximum dates to display. + // not responsible for trimming hidden days. + buildValidRange(): OpenDateRange { + let input = this.props.validRangeInput + let simpleInput = typeof input === 'function' + ? input.call(this.props.calendarApi, this.nowDate) + : input + + return this.refineRange(simpleInput) || + { start: null, end: null } // completely open-ended + } + + // Builds a structure with info about the "current" range, the range that is + // highlighted as being the current month for example. + // See build() for a description of `direction`. + // Guaranteed to have `range` and `unit` properties. `duration` is optional. + buildCurrentRangeInfo(date: DateMarker, direction) { + let { props } = this + let duration = null + let unit = null + let range = null + let dayCount + + if (props.duration) { + duration = props.duration + unit = props.durationUnit + range = this.buildRangeFromDuration(date, direction, duration, unit) + } else if ((dayCount = this.props.dayCount)) { + unit = 'day' + range = this.buildRangeFromDayCount(date, direction, dayCount) + } else if ((range = this.buildCustomVisibleRange(date))) { + unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit + } else { + duration = this.getFallbackDuration() + unit = greatestDurationDenominator(duration).unit + range = this.buildRangeFromDuration(date, direction, duration, unit) + } + + return { duration, unit, range } + } + + getFallbackDuration(): Duration { + return createDuration({ day: 1 }) + } + + // Returns a new activeRange to have time values (un-ambiguate) + // slotMinTime or slotMaxTime causes the range to expand. + adjustActiveRange(range: DateRange) { + let { dateEnv, usesMinMaxTime, slotMinTime, slotMaxTime } = this.props + let { start, end } = range + + if (usesMinMaxTime) { + // expand active range if slotMinTime is negative (why not when positive?) + if (asRoughDays(slotMinTime) < 0) { + start = startOfDay(start) // necessary? + start = dateEnv.add(start, slotMinTime) + } + + // expand active range if slotMaxTime is beyond one day (why not when negative?) + if (asRoughDays(slotMaxTime) > 1) { + end = startOfDay(end) // necessary? + end = addDays(end, -1) + end = dateEnv.add(end, slotMaxTime) + } + } + + return { start, end } + } + + // Builds the "current" range when it is specified as an explicit duration. + // `unit` is the already-computed greatestDurationDenominator unit of duration. + buildRangeFromDuration(date: DateMarker, direction, duration: Duration, unit) { + let { dateEnv, dateAlignment } = this.props + let start: DateMarker + let end: DateMarker + let res + + // compute what the alignment should be + if (!dateAlignment) { + let { dateIncrement } = this.props + + if (dateIncrement) { + // use the smaller of the two units + if (asRoughMs(dateIncrement) < asRoughMs(duration)) { + dateAlignment = greatestDurationDenominator(dateIncrement).unit + } else { + dateAlignment = unit + } + } else { + dateAlignment = unit + } + } + + // if the view displays a single day or smaller + if (asRoughDays(duration) <= 1) { + if (this.isHiddenDay(start)) { + start = this.skipHiddenDays(start, direction) + start = startOfDay(start) + } + } + + function computeRes() { + start = dateEnv.startOf(date, dateAlignment) + end = dateEnv.add(start, duration) + res = { start, end } + } + + computeRes() + + // if range is completely enveloped by hidden days, go past the hidden days + if (!this.trimHiddenDays(res)) { + date = this.skipHiddenDays(date, direction) + computeRes() + } + + return res + } + + // Builds the "current" range when a dayCount is specified. + buildRangeFromDayCount(date: DateMarker, direction, dayCount) { + let { dateEnv, dateAlignment } = this.props + let runningCount = 0 + let start: DateMarker = date + let end: DateMarker + + if (dateAlignment) { + start = dateEnv.startOf(start, dateAlignment) + } + + start = startOfDay(start) + start = this.skipHiddenDays(start, direction) + + end = start + do { + end = addDays(end, 1) + if (!this.isHiddenDay(end)) { + runningCount += 1 + } + } while (runningCount < dayCount) + + return { start, end } + } + + // Builds a normalized range object for the "visible" range, + // which is a way to define the currentRange and activeRange at the same time. + buildCustomVisibleRange(date: DateMarker) { + let { props } = this + let input = props.visibleRangeInput + let simpleInput = typeof input === 'function' + ? input.call(props.calendarApi, props.dateEnv.toDate(date)) + : input + + let range = this.refineRange(simpleInput) + + if (range && (range.start == null || range.end == null)) { + return null + } + + return range + } + + // Computes the range that will represent the element/cells for *rendering*, + // but which may have voided days/times. + // not responsible for trimming hidden days. + buildRenderRange(currentRange: DateRange, currentRangeUnit, isRangeAllDay) { + return currentRange + } + + // Compute the duration value that should be added/substracted to the current date + // when a prev/next operation happens. + buildDateIncrement(fallback): Duration { + let { dateIncrement } = this.props + let customAlignment + + if (dateIncrement) { + return dateIncrement + } + + if ((customAlignment = this.props.dateAlignment)) { + return createDuration(1, customAlignment) + } + + if (fallback) { + return fallback + } + + return createDuration({ days: 1 }) + } + + refineRange(rangeInput: DateRangeInput | undefined): DateRange | null { + if (rangeInput) { + let range = parseRange(rangeInput, this.props.dateEnv) + + if (range) { + range = computeVisibleDayRange(range) + } + + return range + } + + return null + } + + /* Hidden Days + ------------------------------------------------------------------------------------------------------------------*/ + + // Initializes internal variables related to calculating hidden days-of-week + initHiddenDays() { + let hiddenDays = this.props.hiddenDays || [] // array of day-of-week indices that are hidden + let isHiddenDayHash = [] // is the day-of-week hidden? (hash with day-of-week-index -> bool) + let dayCnt = 0 + let i + + if (this.props.weekends === false) { + hiddenDays.push(0, 6) // 0=sunday, 6=saturday + } + + for (i = 0; i < 7; i += 1) { + if ( + !(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1) + ) { + dayCnt += 1 + } + } + + if (!dayCnt) { + throw new Error('invalid hiddenDays') // all days were hidden? bad. + } + + this.isHiddenDayHash = isHiddenDayHash + } + + // Remove days from the beginning and end of the range that are computed as hidden. + // If the whole range is trimmed off, returns null + trimHiddenDays(range: DateRange): DateRange | null { + let { start, end } = range + + if (start) { + start = this.skipHiddenDays(start) + } + + if (end) { + end = this.skipHiddenDays(end, -1, true) + } + + if (start == null || end == null || start < end) { + return { start, end } + } + + return null + } + + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Date (used for UTC) + isHiddenDay(day) { + if (day instanceof Date) { + day = day.getUTCDay() + } + return this.isHiddenDayHash[day] + } + + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // DOES NOT CONSIDER validRange! + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + skipHiddenDays(date: DateMarker, inc = 1, isExclusive = false) { + while ( + this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7] + ) { + date = addDays(date, inc) + } + return date + } +} diff --git a/fullcalendar-main/packages/core/src/NowTimer.ts b/fullcalendar-main/packages/core/src/NowTimer.ts new file mode 100644 index 0000000..5f23989 --- /dev/null +++ b/fullcalendar-main/packages/core/src/NowTimer.ts @@ -0,0 +1,94 @@ +import { DateMarker, addMs, startOfDay, addDays } from './datelib/marker.js' +import { createDuration } from './datelib/duration.js' +import { ViewContext, ViewContextType } from './ViewContext.js' +import { ComponentChildren, Component } from './preact.js' +import { DateRange } from './datelib/date-range.js' +import { getNow } from './reducers/current-date.js' + +export interface NowTimerProps { + unit: string // TODO: add type of unit + children: (now: DateMarker, todayRange: DateRange) => ComponentChildren +} + +interface NowTimerState { + nowDate: DateMarker + todayRange: DateRange +} + +export class NowTimer extends Component<NowTimerProps, NowTimerState> { + static contextType: any = ViewContextType + context: ViewContext // do this for all components that use the context!!! + initialNowDate: DateMarker + initialNowQueriedMs: number + timeoutId: any + + constructor(props: NowTimerProps, context: ViewContext) { + super(props, context) + + this.initialNowDate = getNow(context.options.now, context.dateEnv) + this.initialNowQueriedMs = new Date().valueOf() + + this.state = this.computeTiming().currentState + } + + render() { + let { props, state } = this + return props.children(state.nowDate, state.todayRange) + } + + componentDidMount() { + this.setTimeout() + } + + componentDidUpdate(prevProps: NowTimerProps) { + if (prevProps.unit !== this.props.unit) { + this.clearTimeout() + this.setTimeout() + } + } + + componentWillUnmount() { + this.clearTimeout() + } + + private computeTiming() { + let { props, context } = this + let unroundedNow = addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs) + let currentUnitStart = context.dateEnv.startOf(unroundedNow, props.unit) + let nextUnitStart = context.dateEnv.add(currentUnitStart, createDuration(1, props.unit)) + let waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf() + + // there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342) + // ensure no longer than a day + waitMs = Math.min(1000 * 60 * 60 * 24, waitMs) + + return { + currentState: { nowDate: currentUnitStart, todayRange: buildDayRange(currentUnitStart) } as NowTimerState, + nextState: { nowDate: nextUnitStart, todayRange: buildDayRange(nextUnitStart) } as NowTimerState, + waitMs, + } + } + + private setTimeout() { + let { nextState, waitMs } = this.computeTiming() + + this.timeoutId = setTimeout(() => { + this.setState(nextState, () => { + this.setTimeout() + }) + }, waitMs) + } + + private clearTimeout() { + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + } +} + +function buildDayRange(date: DateMarker): DateRange { // TODO: make this a general util + let start = startOfDay(date) + let end = addDays(start, 1) + + return { start, end } +} diff --git a/fullcalendar-main/packages/core/src/ScrollResponder.ts b/fullcalendar-main/packages/core/src/ScrollResponder.ts new file mode 100644 index 0000000..3568fd9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/ScrollResponder.ts @@ -0,0 +1,53 @@ +import { Duration } from './datelib/duration.js' +import { Emitter } from './common/Emitter.js' +import { CalendarListeners } from './options.js' + +export interface ScrollRequest { + time?: Duration + [otherProp: string]: any +} + +export type ScrollRequestHandler = (request: ScrollRequest) => boolean + +export class ScrollResponder { + queuedRequest: ScrollRequest + + constructor( + private execFunc: ScrollRequestHandler, + private emitter: Emitter<CalendarListeners>, + private scrollTime: Duration, + private scrollTimeReset: boolean, + ) { + emitter.on('_scrollRequest', this.handleScrollRequest) + this.fireInitialScroll() + } + + detach() { + this.emitter.off('_scrollRequest', this.handleScrollRequest) + } + + update(isDatesNew: boolean) { + if (isDatesNew && this.scrollTimeReset) { + this.fireInitialScroll() // will drain + } else { + this.drain() + } + } + + private fireInitialScroll() { + this.handleScrollRequest({ + time: this.scrollTime, + }) + } + + private handleScrollRequest = (request: ScrollRequest) => { + this.queuedRequest = Object.assign({}, this.queuedRequest || {}, request) + this.drain() + } + + private drain() { + if (this.queuedRequest && this.execFunc(this.queuedRequest)) { + this.queuedRequest = null + } + } +} diff --git a/fullcalendar-main/packages/core/src/Toolbar.tsx b/fullcalendar-main/packages/core/src/Toolbar.tsx new file mode 100644 index 0000000..beac4e2 --- /dev/null +++ b/fullcalendar-main/packages/core/src/Toolbar.tsx @@ -0,0 +1,67 @@ +import { createElement } from './preact.js' +import { BaseComponent } from './vdom-util.js' +import { ToolbarModel, ToolbarWidget } from './toolbar-struct.js' +import { ToolbarSection, ToolbarContent } from './ToolbarSection.js' + +export interface ToolbarProps extends ToolbarContent { + extraClassName: string // wish this could be array, but easier for pureness + model: ToolbarModel + titleId: string +} + +export class Toolbar extends BaseComponent<ToolbarProps> { + render() { + let { model, extraClassName } = this.props + let forceLtr = false + let startContent + let endContent + let sectionWidgets = model.sectionWidgets + let centerContent = sectionWidgets.center + + if (sectionWidgets.left) { + forceLtr = true + startContent = sectionWidgets.left + } else { + startContent = sectionWidgets.start + } + + if (sectionWidgets.right) { + forceLtr = true + endContent = sectionWidgets.right + } else { + endContent = sectionWidgets.end + } + + let classNames = [ + extraClassName || '', + 'fc-toolbar', + forceLtr ? 'fc-toolbar-ltr' : '', + ] + + return ( + <div className={classNames.join(' ')}> + {this.renderSection('start', startContent || [])} + {this.renderSection('center', centerContent || [])} + {this.renderSection('end', endContent || [])} + </div> + ) + } + + renderSection(key: string, widgetGroups: ToolbarWidget[][]) { + let { props } = this + + return ( + <ToolbarSection + key={key} + widgetGroups={widgetGroups} + title={props.title} + navUnit={props.navUnit} + activeButton={props.activeButton} + isTodayEnabled={props.isTodayEnabled} + isPrevEnabled={props.isPrevEnabled} + isNextEnabled={props.isNextEnabled} + titleId={props.titleId} + /> + ) + } +} diff --git a/fullcalendar-main/packages/core/src/ToolbarSection.tsx b/fullcalendar-main/packages/core/src/ToolbarSection.tsx new file mode 100644 index 0000000..6028d59 --- /dev/null +++ b/fullcalendar-main/packages/core/src/ToolbarSection.tsx @@ -0,0 +1,74 @@ +import { createElement, VNode } from './preact.js' +import { BaseComponent } from './vdom-util.js' +import { ToolbarWidget } from './toolbar-struct.js' + +export interface ToolbarContent { + title: string + titleId: string + navUnit: string + activeButton: string + isTodayEnabled: boolean + isPrevEnabled: boolean + isNextEnabled: boolean +} + +export interface ToolbarSectionProps extends ToolbarContent { + widgetGroups: ToolbarWidget[][] +} + +export class ToolbarSection extends BaseComponent<ToolbarSectionProps> { + render(): any { + let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup)) + + return createElement('div', { className: 'fc-toolbar-chunk' }, ...children) + } + + renderWidgetGroup(widgetGroup: ToolbarWidget[]): any { + let { props } = this + let { theme } = this.context + let children: VNode[] = [] + let isOnlyButtons = true + + for (let widget of widgetGroup) { + let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget + + if (buttonName === 'title') { + isOnlyButtons = false + children.push( + <h2 className="fc-toolbar-title" id={props.titleId}>{props.title}</h2>, + ) + } else { + let isPressed = buttonName === props.activeButton + let isDisabled = + (!props.isTodayEnabled && buttonName === 'today') || + (!props.isPrevEnabled && buttonName === 'prev') || + (!props.isNextEnabled && buttonName === 'next') + + let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')] + if (isPressed) { + buttonClasses.push(theme.getClass('buttonActive')) + } + + children.push( + <button + type="button" + title={typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint} + disabled={isDisabled} + aria-pressed={isPressed} + className={buttonClasses.join(' ')} + onClick={buttonClick} + > + {buttonText || (buttonIcon ? <span className={buttonIcon} role="img" /> : '')} + </button>, + ) + } + } + + if (children.length > 1) { + let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || '' + + return createElement('div', { className: groupClassName }, ...children) + } + return children[0] + } +} diff --git a/fullcalendar-main/packages/core/src/View.ts b/fullcalendar-main/packages/core/src/View.ts new file mode 100644 index 0000000..65d4277 --- /dev/null +++ b/fullcalendar-main/packages/core/src/View.ts @@ -0,0 +1,38 @@ +import { DateProfile } from './DateProfileGenerator.js' +import { EventStore } from './structs/event-store.js' +import { EventUiHash } from './component/event-ui.js' +import { sliceEventStore, EventRenderRange } from './component/event-rendering.js' +import { DateSpan } from './structs/date-span.js' +import { EventInteractionState } from './interactions/event-interaction-state.js' +import { Duration } from './datelib/duration.js' + +export interface ViewProps { + dateProfile: DateProfile + businessHours: EventStore + eventStore: EventStore + eventUiBases: EventUiHash + dateSelection: DateSpan | null + eventSelection: string + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null + isHeightAuto: boolean + forPrint: boolean +} + +// HELPERS + +/* +if nextDayThreshold is specified, slicing is done in an all-day fashion. +you can get nextDayThreshold from context.nextDayThreshold +*/ +export function sliceEvents( + props: ViewProps & { dateProfile: DateProfile, nextDayThreshold: Duration }, + allDay?: boolean, +): EventRenderRange[] { + return sliceEventStore( + props.eventStore, + props.eventUiBases, + props.dateProfile.activeRange, + allDay ? props.nextDayThreshold : null, + ).fg +} diff --git a/fullcalendar-main/packages/core/src/ViewContext.ts b/fullcalendar-main/packages/core/src/ViewContext.ts new file mode 100644 index 0000000..3a004a4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/ViewContext.ts @@ -0,0 +1,85 @@ +import { CalendarImpl } from './api/CalendarImpl.js' +import { ViewImpl } from './api/ViewImpl.js' +import { Theme } from './theme/Theme.js' +import { DateEnv } from './datelib/env.js' +import { PluginHooks } from './plugin-system-struct.js' +import { createContext, Context } from './preact.js' +import { ScrollResponder, ScrollRequestHandler } from './ScrollResponder.js' +import { DateProfileGenerator } from './DateProfileGenerator.js' +import { ViewSpec } from './structs/view-spec.js' +import { CalendarData } from './reducers/data-types.js' +import { Action } from './reducers/Action.js' +import { Emitter } from './common/Emitter.js' +import { InteractionSettingsInput } from './interactions/interaction.js' +import { DateComponent } from './component/DateComponent.js' +import { CalendarContext } from './CalendarContext.js' +import { createDuration } from './datelib/duration.js' +import { ViewOptionsRefined, CalendarListeners } from './options.js' + +export const ViewContextType: Context<any> = createContext<ViewContext>({} as any) // for Components +export type ResizeHandler = (force: boolean) => void + +/* +it's important that ViewContext extends CalendarContext so that components that subscribe to ViewContext +can pass in their ViewContext to util functions that accept CalendarContext. +*/ +export interface ViewContext extends CalendarContext { + options: ViewOptionsRefined // more specific than BaseOptionsRefined + theme: Theme + isRtl: boolean + dateProfileGenerator: DateProfileGenerator + viewSpec: ViewSpec + viewApi: ViewImpl + addResizeHandler: (handler: ResizeHandler) => void + removeResizeHandler: (handler: ResizeHandler) => void + createScrollResponder: (execFunc: ScrollRequestHandler) => ScrollResponder + registerInteractiveComponent: (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => void + unregisterInteractiveComponent: (component: DateComponent<any>) => void +} + +export function buildViewContext( + viewSpec: ViewSpec, + viewApi: ViewImpl, + viewOptions: ViewOptionsRefined, + dateProfileGenerator: DateProfileGenerator, + dateEnv: DateEnv, + theme: Theme, + pluginHooks: PluginHooks, + dispatch: (action: Action) => void, + getCurrentData: () => CalendarData, + emitter: Emitter<CalendarListeners>, + calendarApi: CalendarImpl, + registerInteractiveComponent: (component: DateComponent<any>, settingsInput: InteractionSettingsInput) => void, + unregisterInteractiveComponent: (component: DateComponent<any>) => void, +): ViewContext { + return { + dateEnv, + options: viewOptions, + pluginHooks, + emitter, + dispatch, + getCurrentData, + calendarApi, + viewSpec, + viewApi, + dateProfileGenerator, + theme, + isRtl: viewOptions.direction === 'rtl', + addResizeHandler(handler: ResizeHandler) { + emitter.on('_resize', handler) + }, + removeResizeHandler(handler: ResizeHandler) { + emitter.off('_resize', handler) + }, + createScrollResponder(execFunc: ScrollRequestHandler) { + return new ScrollResponder( + execFunc, + emitter, + createDuration(viewOptions.scrollTime), + viewOptions.scrollTimeReset, + ) + }, + registerInteractiveComponent, + unregisterInteractiveComponent, + } +} diff --git a/fullcalendar-main/packages/core/src/ViewHarness.tsx b/fullcalendar-main/packages/core/src/ViewHarness.tsx new file mode 100644 index 0000000..f2611cd --- /dev/null +++ b/fullcalendar-main/packages/core/src/ViewHarness.tsx @@ -0,0 +1,90 @@ +import { BaseComponent, setRef } from './vdom-util.js' +import { ComponentChildren, Ref, createElement } from './preact.js' +import { CssDimValue } from './scrollgrid/util.js' + +export interface ViewHarnessProps { + elRef?: Ref<HTMLDivElement> + labeledById?: string + liquid?: boolean + height?: CssDimValue + aspectRatio?: number + children?: ComponentChildren +} + +interface ViewHarnessState { + availableWidth: number | null +} + +export class ViewHarness extends BaseComponent<ViewHarnessProps, ViewHarnessState> { + el: HTMLElement + + state: ViewHarnessState = { + availableWidth: null, + } + + render() { + let { props, state } = this + let { aspectRatio } = props + + let classNames = [ + 'fc-view-harness', + (aspectRatio || props.liquid || props.height) + ? 'fc-view-harness-active' // harness controls the height + : 'fc-view-harness-passive', // let the view do the height + ] + let height: CssDimValue = '' + let paddingBottom: CssDimValue = '' + + if (aspectRatio) { + if (state.availableWidth !== null) { + height = state.availableWidth / aspectRatio + } else { + // while waiting to know availableWidth, we can't set height to *zero* + // because will cause lots of unnecessary scrollbars within scrollgrid. + // BETTER: don't start rendering ANYTHING yet until we know container width + // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606) + paddingBottom = `${(1 / aspectRatio) * 100}%` + } + } else { + height = props.height || '' + } + + return ( + <div + aria-labelledby={props.labeledById} + ref={this.handleEl} + className={classNames.join(' ')} + style={{ height, paddingBottom }} + > + {props.children} + </div> + ) + } + + componentDidMount() { + this.context.addResizeHandler(this.handleResize) + } + + componentWillUnmount() { + this.context.removeResizeHandler(this.handleResize) + } + + handleEl = (el: HTMLElement | null) => { + this.el = el + setRef(this.props.elRef, el) + this.updateAvailableWidth() + } + + handleResize = () => { + this.updateAvailableWidth() + } + + updateAvailableWidth() { + if ( + this.el && // needed. but why? + this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth + ) { + this.setState({ availableWidth: this.el.offsetWidth }) + } + } +} diff --git a/fullcalendar-main/packages/core/src/api/CalendarApi.ts b/fullcalendar-main/packages/core/src/api/CalendarApi.ts new file mode 100644 index 0000000..46a64b1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/CalendarApi.ts @@ -0,0 +1,85 @@ +import { ViewApi } from './ViewApi.js' +import { EventSourceApi } from './EventSourceApi.js' +import { EventApi } from './EventApi.js' +import { + CalendarOptions, + CalendarListeners, + DateInput, + DurationInput, + DateRangeInput, + EventSourceInput, + EventInput, + FormatterInput, +} from './structs.js' + +export interface CalendarApi { + view: ViewApi + updateSize(): void + + // Options + // ----------------------------------------------------------------------------------------------------------------- + + setOption<OptionName extends keyof CalendarOptions>(name: OptionName, val: CalendarOptions[OptionName]): void + getOption<OptionName extends keyof CalendarOptions>(name: OptionName): CalendarOptions[OptionName] + getAvailableLocaleCodes(): string[] + + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + + on<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void + off<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void + trigger<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, ...args: Parameters<CalendarListeners[ListenerName]>): void + + // View + // ----------------------------------------------------------------------------------------------------------------- + + changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput): void + zoomTo(dateMarker: Date, viewType?: string): void + + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + + prev(): void + next(): void + prevYear(): void + nextYear(): void + today(): void + gotoDate(zonedDateInput: DateInput): void + incrementDate(deltaInput: DurationInput): void + getDate(): Date + + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + + formatDate(d: DateInput, formatter: FormatterInput): string + formatRange(d0: DateInput, d1: DateInput, settings: any): string // TODO: settings type + formatIso(d: DateInput, omitTime?: boolean): string + + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + + select(dateOrObj: DateInput | any, endDate?: DateInput): void + unselect(): void + + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + + addEvent(eventInput: EventInput, sourceInput?: EventSourceApi | string | boolean): EventApi | null + getEventById(id: string): EventApi | null + getEvents(): EventApi[] + removeAllEvents(): void + + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + + getEventSources(): EventSourceApi[] + getEventSourceById(id: string): EventSourceApi | null + addEventSource(sourceInput: EventSourceInput): EventSourceApi + removeAllEventSources(): void + refetchEvents(): void + + // Scroll + // ----------------------------------------------------------------------------------------------------------------- + + scrollToTime(timeInput: DurationInput): void +} diff --git a/fullcalendar-main/packages/core/src/api/CalendarImpl.ts b/fullcalendar-main/packages/core/src/api/CalendarImpl.ts new file mode 100644 index 0000000..b05b3b0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/CalendarImpl.ts @@ -0,0 +1,511 @@ +import { createFormatter, FormatterInput } from '../datelib/formatting.js' +import { createDuration } from '../datelib/duration.js' +import { parseDateSpan } from '../structs/date-span.js' +import { parseEventSource } from '../structs/event-source-parse.js' +import { parseEvent } from '../structs/event-parse.js' +import { eventTupleToStore } from '../structs/event-store.js' +import { ViewSpec } from '../structs/view-spec.js' +import { PointerDragEvent } from '../interactions/pointer.js' +import { getNow } from '../reducers/current-date.js' +import { triggerDateSelect, triggerDateUnselect } from '../calendar-utils.js' +import { hashValuesToArray } from '../util/object.js' +import { CalendarDataManager } from '../reducers/CalendarDataManager.js' +import { Action } from '../reducers/Action.js' +import { EventSource } from '../structs/event-source.js' +import { eventApiToStore, buildEventApis, EventImpl } from './EventImpl.js' +import { CalendarData } from '../reducers/data-types.js' +import { CalendarApi } from './CalendarApi.js' +import { ViewImpl } from './ViewImpl.js' +import { EventSourceImpl } from './EventSourceImpl.js' +import { + CalendarOptions, + CalendarListeners, + DateInput, + DurationInput, + DateSpanInput, + DateRangeInput, + EventSourceInput, + EventInput, +} from './structs.js' + +export class CalendarImpl implements CalendarApi { + currentDataManager?: CalendarDataManager // will be set by CalendarDataManager + + getCurrentData(): CalendarData { + return this.currentDataManager!.getCurrentData() + } + + dispatch(action: Action): void { + this.currentDataManager!.dispatch(action) + } + + get view(): ViewImpl { return this.getCurrentData().viewApi } + + batchRendering(callback: () => void): void { // subclasses should implement + callback() + } + + updateSize(): void { + this.trigger('_resize', true) + } + + // Options + // ----------------------------------------------------------------------------------------------------------------- + + setOption<OptionName extends keyof CalendarOptions>(name: OptionName, val: CalendarOptions[OptionName]): void { + this.dispatch({ + type: 'SET_OPTION', + optionName: name, + rawOptionValue: val, + }) + } + + getOption<OptionName extends keyof CalendarOptions>(name: OptionName): CalendarOptions[OptionName] { + return this.currentDataManager!.currentCalendarOptionsInput[name] + } + + getAvailableLocaleCodes(): string[] { + return Object.keys(this.getCurrentData().availableRawLocales) + } + + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + + on<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void { + let { currentDataManager } = this + + if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) { + currentDataManager.emitter.on(handlerName, handler) + } else { + console.warn(`Unknown listener name '${handlerName}'`) + } + } + + off<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, handler: CalendarListeners[ListenerName]): void { + this.currentDataManager!.emitter.off(handlerName, handler) + } + + // not meant for public use + trigger<ListenerName extends keyof CalendarListeners>(handlerName: ListenerName, ...args: Parameters<CalendarListeners[ListenerName]>): void { + this.currentDataManager!.emitter.trigger(handlerName, ...args) + } + + // View + // ----------------------------------------------------------------------------------------------------------------- + + changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput): void { + this.batchRendering(() => { + this.unselect() + + if (dateOrRange) { + if ((dateOrRange as DateRangeInput).start && (dateOrRange as DateRangeInput).end) { // a range + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType, + }) + this.dispatch({ // not very efficient to do two dispatches + type: 'SET_OPTION', + optionName: 'visibleRange', + rawOptionValue: dateOrRange, + }) + } else { + let { dateEnv } = this.getCurrentData() + + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType, + dateMarker: dateEnv.createMarker(dateOrRange as DateInput), + }) + } + } else { + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType, + }) + } + }) + } + + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + // needs to change + zoomTo(dateMarker: Date, viewType?: string): void { + let state = this.getCurrentData() + let spec + + viewType = viewType || 'day' // day is default zoom + spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType) + + this.unselect() + + if (spec) { + this.dispatch({ + type: 'CHANGE_VIEW_TYPE', + viewType: spec.type, + dateMarker, + }) + } else { + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker, + }) + } + } + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + private getUnitViewSpec(unit: string): ViewSpec | null { + let { viewSpecs, toolbarConfig } = this.getCurrentData() + let viewTypes = [].concat( + toolbarConfig.header ? toolbarConfig.header.viewsWithButtons : [], + toolbarConfig.footer ? toolbarConfig.footer.viewsWithButtons : [], + ) + let i + let spec + + for (let viewType in viewSpecs) { + viewTypes.push(viewType) + } + + for (i = 0; i < viewTypes.length; i += 1) { + spec = viewSpecs[viewTypes[i]] + if (spec) { + if (spec.singleUnit === unit) { + return spec + } + } + } + + return null + } + + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + + prev(): void { + this.unselect() + this.dispatch({ type: 'PREV' }) + } + + next(): void { + this.unselect() + this.dispatch({ type: 'NEXT' }) + } + + prevYear(): void { + let state = this.getCurrentData() + this.unselect() + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, -1), + }) + } + + nextYear(): void { + let state = this.getCurrentData() + + this.unselect() + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.addYears(state.currentDate, 1), + }) + } + + today(): void { + let state = this.getCurrentData() + + this.unselect() + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: getNow(state.calendarOptions.now, state.dateEnv), + }) + } + + gotoDate(zonedDateInput: DateInput): void { + let state = this.getCurrentData() + + this.unselect() + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.createMarker(zonedDateInput), + }) + } + + incrementDate(deltaInput: DurationInput): void { + let state = this.getCurrentData() + let delta = createDuration(deltaInput) + + if (delta) { // else, warn about invalid input? + this.unselect() + this.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.add(state.currentDate, delta), + }) + } + } + + getDate(): Date { + let state = this.getCurrentData() + return state.dateEnv.toDate(state.currentDate) + } + + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + + formatDate(d: DateInput, formatter: FormatterInput): string { + let { dateEnv } = this.getCurrentData() + + return dateEnv.format( + dateEnv.createMarker(d), + createFormatter(formatter), + ) + } + + // `settings` is for formatter AND isEndExclusive + formatRange(d0: DateInput, d1: DateInput, settings: any): string { // TODO: settings type + let { dateEnv } = this.getCurrentData() + + return dateEnv.formatRange( + dateEnv.createMarker(d0), + dateEnv.createMarker(d1), + createFormatter(settings), + settings, + ) + } + + formatIso(d: DateInput, omitTime?: boolean): string { + let { dateEnv } = this.getCurrentData() + + return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime }) + } + + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + + select(dateOrObj: DateInput | any, endDate?: DateInput): void { + let selectionInput: DateSpanInput + + if (endDate == null) { + if (dateOrObj.start != null) { + selectionInput = dateOrObj as DateSpanInput + } else { + selectionInput = { + start: dateOrObj, + end: null, + } + } + } else { + selectionInput = { + start: dateOrObj, + end: endDate, + } as DateSpanInput + } + + let state = this.getCurrentData() + let selection = parseDateSpan( + selectionInput, + state.dateEnv, + createDuration({ days: 1 }), // TODO: cache this? + ) + + if (selection) { // throw parse error otherwise? + this.dispatch({ type: 'SELECT_DATES', selection }) + triggerDateSelect(selection, null, state) + } + } + + unselect(pev?: PointerDragEvent): void { + let state = this.getCurrentData() + + if (state.dateSelection) { + this.dispatch({ type: 'UNSELECT_DATES' }) + triggerDateUnselect(pev, state) + } + } + + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + + addEvent(eventInput: EventInput, sourceInput?: EventSourceImpl | string | boolean): EventImpl | null { + if (eventInput instanceof EventImpl) { + let def = eventInput._def + let instance = eventInput._instance + let currentData = this.getCurrentData() + + // not already present? don't want to add an old snapshot + if (!currentData.eventStore.defs[def.defId]) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore({ def, instance }), // TODO: better util for two args? + }) + this.triggerEventAdd(eventInput) + } + + return eventInput + } + + let state = this.getCurrentData() + let eventSource: EventSource<any> + + if (sourceInput instanceof EventSourceImpl) { + eventSource = sourceInput.internalEventSource + } else if (typeof sourceInput === 'boolean') { + if (sourceInput) { // true. part of the first event source + [eventSource] = hashValuesToArray(state.eventSources) + } + } else if (sourceInput != null) { // an ID. accepts a number too + let sourceApi = this.getEventSourceById(sourceInput) // TODO: use an internal function + + if (!sourceApi) { + console.warn(`Could not find an event source with ID "${sourceInput}"`) // TODO: test + return null + } + eventSource = sourceApi.internalEventSource + } + + let tuple = parseEvent(eventInput, eventSource, state, false) + + if (tuple) { + let newEventApi = new EventImpl( + state, + tuple.def, + tuple.def.recurringDef ? null : tuple.instance, + ) + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore(tuple), + }) + this.triggerEventAdd(newEventApi) + + return newEventApi + } + + return null + } + + private triggerEventAdd(eventApi: EventImpl): void { + let { emitter } = this.getCurrentData() + + emitter.trigger('eventAdd', { + event: eventApi, + relatedEvents: [], + revert: () => { + this.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: eventApiToStore(eventApi), + }) + }, + }) + } + + // TODO: optimize + getEventById(id: string): EventImpl | null { + let state = this.getCurrentData() + let { defs, instances } = state.eventStore + id = String(id) + + for (let defId in defs) { + let def = defs[defId] + + if (def.publicId === id) { + if (def.recurringDef) { + return new EventImpl(state, def, null) + } + + for (let instanceId in instances) { + let instance = instances[instanceId] + + if (instance.defId === def.defId) { + return new EventImpl(state, def, instance) + } + } + } + } + + return null + } + + getEvents(): EventImpl[] { + let currentData = this.getCurrentData() + + return buildEventApis(currentData.eventStore, currentData) + } + + removeAllEvents(): void { + this.dispatch({ type: 'REMOVE_ALL_EVENTS' }) + } + + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + + getEventSources(): EventSourceImpl[] { + let state = this.getCurrentData() + let sourceHash = state.eventSources + let sourceApis: EventSourceImpl[] = [] + + for (let internalId in sourceHash) { + sourceApis.push(new EventSourceImpl(state, sourceHash[internalId])) + } + + return sourceApis + } + + getEventSourceById(id: string): EventSourceImpl | null { + let state = this.getCurrentData() + let sourceHash = state.eventSources + id = String(id) + + for (let sourceId in sourceHash) { + if (sourceHash[sourceId].publicId === id) { + return new EventSourceImpl(state, sourceHash[sourceId]) + } + } + + return null + } + + addEventSource(sourceInput: EventSourceInput): EventSourceImpl { + let state = this.getCurrentData() + + if (sourceInput instanceof EventSourceImpl) { + // not already present? don't want to add an old snapshot + if (!state.eventSources[sourceInput.internalEventSource.sourceId]) { + this.dispatch({ + type: 'ADD_EVENT_SOURCES', + sources: [sourceInput.internalEventSource], + }) + } + + return sourceInput + } + + let eventSource = parseEventSource(sourceInput, state) + + if (eventSource) { // TODO: error otherwise? + this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] }) + + return new EventSourceImpl(state, eventSource) + } + + return null + } + + removeAllEventSources(): void { + this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' }) + } + + refetchEvents(): void { + this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true }) + } + + // Scroll + // ----------------------------------------------------------------------------------------------------------------- + + scrollToTime(timeInput: DurationInput): void { + let time = createDuration(timeInput) + + if (time) { + this.trigger('_scrollRequest', { time }) + } + } +} diff --git a/fullcalendar-main/packages/core/src/api/EventApi.ts b/fullcalendar-main/packages/core/src/api/EventApi.ts new file mode 100644 index 0000000..6831488 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/EventApi.ts @@ -0,0 +1,45 @@ +import { Dictionary } from '../options.js' +import { EventSourceApi } from './EventSourceApi.js' +import { + DateInput, + DurationInput, + FormatterInput, +} from './structs.js' + +export interface EventApi { + source: EventSourceApi | null + start: Date | null + end: Date | null + startStr: string + endStr: string + id: string + groupId: string + allDay: boolean + title: string + url: string + display: string // TODO: better + startEditable: boolean + durationEditable: boolean + constraint: any // TODO: better + overlap: boolean + allow: any // TODO: better + backgroundColor: string + borderColor: string + textColor: string + classNames: string[] + extendedProps: Dictionary + + setProp(name: string, val: any): void + setExtendedProp(name: string, val: any): void + setStart(startInput: DateInput, options?: { granularity?: string, maintainDuration?: boolean }): void + setEnd(endInput: DateInput | null, options?: { granularity?: string }): void + setDates(startInput: DateInput, endInput: DateInput | null, options?: { allDay?: boolean, granularity?: string }): void + moveStart(deltaInput: DurationInput): void + moveEnd(deltaInput: DurationInput): void + moveDates(deltaInput: DurationInput): void + setAllDay(allDay: boolean, options?: { maintainDuration?: boolean }): void + formatRange(formatInput: FormatterInput) + remove(): void + toPlainObject(settings?: { collapseExtendedProps?: boolean, collapseColor?: boolean }): Dictionary + toJSON(): Dictionary +} diff --git a/fullcalendar-main/packages/core/src/api/EventImpl.ts b/fullcalendar-main/packages/core/src/api/EventImpl.ts new file mode 100644 index 0000000..543d3b8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/EventImpl.ts @@ -0,0 +1,451 @@ +import { EventDef } from '../structs/event-def.js' +import { EVENT_NON_DATE_REFINERS, EVENT_DATE_REFINERS } from '../structs/event-parse.js' +import { EventInstance } from '../structs/event-instance.js' +import { EVENT_UI_REFINERS, EventUiHash } from '../component/event-ui.js' +import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation.js' +import { diffDates, computeAlignedDayRange } from '../util/date.js' +import { createDuration, durationsEqual } from '../datelib/duration.js' +import { createFormatter } from '../datelib/formatting.js' +import { CalendarContext } from '../CalendarContext.js' +import { getRelevantEvents, EventStore } from '../structs/event-store.js' +import { Dictionary } from '../options.js' +import { EventApi } from './EventApi.js' +import { EventSourceImpl } from './EventSourceImpl.js' +import { + DateInput, + DurationInput, + FormatterInput, +} from './structs.js' + +export class EventImpl implements EventApi { + _context: CalendarContext + _def: EventDef + _instance: EventInstance | null + // instance will be null if expressing a recurring event that has no current instances, + // OR if trying to validate an incoming external event that has no dates assigned + + constructor(context: CalendarContext, def: EventDef, instance?: EventInstance) { + this._context = context + this._def = def + this._instance = instance || null + } + + /* + TODO: make event struct more responsible for this + */ + setProp(name: string, val: any): void { + if (name in EVENT_DATE_REFINERS) { + console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.') + // TODO: make proper aliasing system? + } else if (name === 'id') { + val = EVENT_NON_DATE_REFINERS[name](val) + + this.mutate({ + standardProps: { publicId: val }, // hardcoded internal name + }) + } else if (name in EVENT_NON_DATE_REFINERS) { + val = EVENT_NON_DATE_REFINERS[name](val) + + this.mutate({ + standardProps: { [name]: val }, + }) + } else if (name in EVENT_UI_REFINERS) { + let ui = EVENT_UI_REFINERS[name](val) + + if (name === 'color') { + ui = { backgroundColor: val, borderColor: val } + } else if (name === 'editable') { + ui = { startEditable: val, durationEditable: val } + } else { + ui = { [name]: val } + } + + this.mutate({ + standardProps: { ui }, + }) + } else { + console.warn(`Could not set prop '${name}'. Use setExtendedProp instead.`) + } + } + + setExtendedProp(name: string, val: any): void { + this.mutate({ + extendedProps: { [name]: val }, + }) + } + + setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}): void { + let { dateEnv } = this._context + let start = dateEnv.createMarker(startInput) + + if (start && this._instance) { // TODO: warning if parsed bad + let instanceRange = this._instance.range + let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity) // what if parsed bad!? + + if (options.maintainDuration) { + this.mutate({ datesDelta: startDelta }) + } else { + this.mutate({ startDelta }) + } + } + } + + setEnd(endInput: DateInput | null, options: { granularity?: string } = {}): void { + let { dateEnv } = this._context + let end + + if (endInput != null) { + end = dateEnv.createMarker(endInput) + + if (!end) { + return // TODO: warning if parsed bad + } + } + + if (this._instance) { + if (end) { + let endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity) + this.mutate({ endDelta }) + } else { + this.mutate({ standardProps: { hasEnd: false } }) + } + } + } + + setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}): void { + let { dateEnv } = this._context + let standardProps = { allDay: options.allDay } as any + let start = dateEnv.createMarker(startInput) + let end + + if (!start) { + return // TODO: warning if parsed bad + } + + if (endInput != null) { + end = dateEnv.createMarker(endInput) + + if (!end) { // TODO: warning if parsed bad + return + } + } + + if (this._instance) { + let instanceRange = this._instance.range + + // when computing the diff for an event being converted to all-day, + // compute diff off of the all-day values the way event-mutation does. + if (options.allDay === true) { + instanceRange = computeAlignedDayRange(instanceRange) + } + + let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity) + + if (end) { + let endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity) + + if (durationsEqual(startDelta, endDelta)) { + this.mutate({ datesDelta: startDelta, standardProps }) + } else { + this.mutate({ startDelta, endDelta, standardProps }) + } + } else { // means "clear the end" + standardProps.hasEnd = false + this.mutate({ datesDelta: startDelta, standardProps }) + } + } + } + + moveStart(deltaInput: DurationInput): void { + let delta = createDuration(deltaInput) + + if (delta) { // TODO: warning if parsed bad + this.mutate({ startDelta: delta }) + } + } + + moveEnd(deltaInput: DurationInput): void { + let delta = createDuration(deltaInput) + + if (delta) { // TODO: warning if parsed bad + this.mutate({ endDelta: delta }) + } + } + + moveDates(deltaInput: DurationInput): void { + let delta = createDuration(deltaInput) + + if (delta) { // TODO: warning if parsed bad + this.mutate({ datesDelta: delta }) + } + } + + setAllDay(allDay: boolean, options: { maintainDuration?: boolean } = {}): void { + let standardProps = { allDay } as any + let { maintainDuration } = options + + if (maintainDuration == null) { + maintainDuration = this._context.options.allDayMaintainDuration + } + + if (this._def.allDay !== allDay) { + standardProps.hasEnd = maintainDuration + } + + this.mutate({ standardProps }) + } + + formatRange(formatInput: FormatterInput): string { + let { dateEnv } = this._context + let instance = this._instance + let formatter = createFormatter(formatInput) + + if (this._def.hasEnd) { + return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, { + forcedStartTzo: instance.forcedStartTzo, + forcedEndTzo: instance.forcedEndTzo, + }) + } + return dateEnv.format(instance.range.start, formatter, { + forcedTzo: instance.forcedStartTzo, + }) + } + + mutate(mutation: EventMutation): void { // meant to be private. but plugins need access + let instance = this._instance + + if (instance) { + let def = this._def + let context = this._context + let { eventStore } = context.getCurrentData() + let relevantEvents = getRelevantEvents(eventStore, instance.instanceId) + let eventConfigBase = { + '': { // HACK. always allow API to mutate events + display: '', + startEditable: true, + durationEditable: true, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], + }, + } as EventUiHash + + relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context) + + let oldEvent = new EventImpl(context, def, instance) // snapshot + this._def = relevantEvents.defs[def.defId] + this._instance = relevantEvents.instances[instance.instanceId] + + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, + }) + + context.emitter.trigger('eventChange', { + oldEvent, + event: this, + relatedEvents: buildEventApis(relevantEvents, context, instance), + revert() { + context.dispatch({ + type: 'RESET_EVENTS', + eventStore, // the ORIGINAL store + }) + }, + }) + } + } + + remove(): void { + let context = this._context + let asStore = eventApiToStore(this) + + context.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: asStore, + }) + + context.emitter.trigger('eventRemove', { + event: this, + relatedEvents: [], + revert() { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: asStore, + }) + }, + }) + } + + get source(): EventSourceImpl | null { + let { sourceId } = this._def + + if (sourceId) { + return new EventSourceImpl( + this._context, + this._context.getCurrentData().eventSources[sourceId], + ) + } + return null + } + + get start(): Date | null { + return this._instance ? + this._context.dateEnv.toDate(this._instance.range.start) : + null + } + + get end(): Date | null { + return (this._instance && this._def.hasEnd) ? + this._context.dateEnv.toDate(this._instance.range.end) : + null + } + + get startStr(): string { + let instance = this._instance + if (instance) { + return this._context.dateEnv.formatIso(instance.range.start, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedStartTzo, + }) + } + return '' + } + + get endStr(): string { + let instance = this._instance + if (instance && this._def.hasEnd) { + return this._context.dateEnv.formatIso(instance.range.end, { + omitTime: this._def.allDay, + forcedTzo: instance.forcedEndTzo, + }) + } + return '' + } + + // computable props that all access the def + // TODO: find a TypeScript-compatible way to do this at scale + get id() { return this._def.publicId } + get groupId() { return this._def.groupId } + get allDay() { return this._def.allDay } + get title() { return this._def.title } + get url() { return this._def.url } + get display() { return this._def.ui.display || 'auto' } // bad. just normalize the type earlier + get startEditable() { return this._def.ui.startEditable } + get durationEditable() { return this._def.ui.durationEditable } + get constraint() { return this._def.ui.constraints[0] || null } + get overlap() { return this._def.ui.overlap } + get allow() { return this._def.ui.allows[0] || null } + get backgroundColor() { return this._def.ui.backgroundColor } + get borderColor() { return this._def.ui.borderColor } + get textColor() { return this._def.ui.textColor } + + // NOTE: user can't modify these because Object.freeze was called in event-def parsing + get classNames() { return this._def.ui.classNames } + get extendedProps() { return this._def.extendedProps } + + toPlainObject(settings: { collapseExtendedProps?: boolean, collapseColor?: boolean } = {}): Dictionary { + let def = this._def + let { ui } = def + let { startStr, endStr } = this + let res: Dictionary = { + allDay: def.allDay, + } + + if (def.title) { + res.title = def.title + } + + if (startStr) { + res.start = startStr + } + + if (endStr) { + res.end = endStr + } + + if (def.publicId) { + res.id = def.publicId + } + + if (def.groupId) { + res.groupId = def.groupId + } + + if (def.url) { + res.url = def.url + } + + if (ui.display && ui.display !== 'auto') { + res.display = ui.display + } + + // TODO: what about recurring-event properties??? + // TODO: include startEditable/durationEditable/constraint/overlap/allow + + if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) { + res.color = ui.backgroundColor + } else { + if (ui.backgroundColor) { + res.backgroundColor = ui.backgroundColor + } + if (ui.borderColor) { + res.borderColor = ui.borderColor + } + } + + if (ui.textColor) { + res.textColor = ui.textColor + } + + if (ui.classNames.length) { + res.classNames = ui.classNames + } + + if (Object.keys(def.extendedProps).length) { + if (settings.collapseExtendedProps) { + Object.assign(res, def.extendedProps) + } else { + res.extendedProps = def.extendedProps + } + } + + return res + } + + toJSON(): Dictionary { + return this.toPlainObject() + } +} + +export function eventApiToStore(eventApi: EventImpl): EventStore { + let def = eventApi._def + let instance = eventApi._instance + + return { + defs: { [def.defId]: def }, + instances: instance + ? { [instance.instanceId]: instance } + : {}, + } +} + +export function buildEventApis(eventStore: EventStore, context: CalendarContext, excludeInstance?: EventInstance): EventImpl[] { + let { defs, instances } = eventStore + let eventApis: EventImpl[] = [] + let excludeInstanceId = excludeInstance ? excludeInstance.instanceId : '' + + for (let id in instances) { + let instance = instances[id] + let def = defs[instance.defId] + + if (instance.instanceId !== excludeInstanceId) { + eventApis.push(new EventImpl(context, def, instance)) + } + } + + return eventApis +} diff --git a/fullcalendar-main/packages/core/src/api/EventSourceApi.ts b/fullcalendar-main/packages/core/src/api/EventSourceApi.ts new file mode 100644 index 0000000..b183e18 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/EventSourceApi.ts @@ -0,0 +1,8 @@ + +export interface EventSourceApi { + id: string + url: string + format: string + remove(): void + refetch(): void +} diff --git a/fullcalendar-main/packages/core/src/api/EventSourceImpl.ts b/fullcalendar-main/packages/core/src/api/EventSourceImpl.ts new file mode 100644 index 0000000..c78eeee --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/EventSourceImpl.ts @@ -0,0 +1,38 @@ +import { EventSource } from '../structs/event-source.js' +import { CalendarContext } from '../CalendarContext.js' +import { EventSourceApi } from './EventSourceApi.js' + +export class EventSourceImpl implements EventSourceApi { + constructor( + private context: CalendarContext, + public internalEventSource: EventSource<any>, // rename? + ) { + } + + remove(): void { + this.context.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: this.internalEventSource.sourceId, + }) + } + + refetch(): void { + this.context.dispatch({ + type: 'FETCH_EVENT_SOURCES', + sourceIds: [this.internalEventSource.sourceId], + isRefetch: true, + }) + } + + get id(): string { + return this.internalEventSource.publicId + } + + get url(): string { + return this.internalEventSource.meta.url + } + + get format(): string { + return this.internalEventSource.meta.format // TODO: bad. not guaranteed + } +} diff --git a/fullcalendar-main/packages/core/src/api/ViewApi.ts b/fullcalendar-main/packages/core/src/api/ViewApi.ts new file mode 100644 index 0000000..8df3817 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/ViewApi.ts @@ -0,0 +1,14 @@ +import { CalendarApi } from './CalendarApi.js' + +export interface ViewApi { + calendar: CalendarApi + + type: string + title: string + activeStart: Date + activeEnd: Date + currentStart: Date + currentEnd: Date + + getOption(name: string): any +} diff --git a/fullcalendar-main/packages/core/src/api/ViewImpl.ts b/fullcalendar-main/packages/core/src/api/ViewImpl.ts new file mode 100644 index 0000000..e015959 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/ViewImpl.ts @@ -0,0 +1,42 @@ +import { DateEnv } from '../datelib/env.js' +import { CalendarData } from '../reducers/data-types.js' +import { CalendarApi } from './CalendarApi.js' +import { ViewApi } from './ViewApi.js' + +// always represents the current view. otherwise, it'd need to change value every time date changes +export class ViewImpl implements ViewApi { + constructor( + public type: string, + private getCurrentData: () => CalendarData, + private dateEnv: DateEnv, + ) { + } + + get calendar(): CalendarApi { + return this.getCurrentData().calendarApi + } + + get title(): string { + return this.getCurrentData().viewTitle + } + + get activeStart(): Date { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start) + } + + get activeEnd(): Date { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end) + } + + get currentStart(): Date { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start) + } + + get currentEnd(): Date { + return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end) + } + + getOption(name: string): any { + return this.getCurrentData().options[name] // are the view-specific options + } +} diff --git a/fullcalendar-main/packages/core/src/api/structs.ts b/fullcalendar-main/packages/core/src/api/structs.ts new file mode 100644 index 0000000..44faf99 --- /dev/null +++ b/fullcalendar-main/packages/core/src/api/structs.ts @@ -0,0 +1,44 @@ +export type { CalendarOptions, CalendarListeners } from '../options.js' +export type { DateInput } from '../datelib/env.js' +export type { DurationInput } from '../datelib/duration.js' +export type { DateSpanInput } from '../structs/date-span.js' +export type { DateRangeInput } from '../datelib/date-range.js' +export type { EventSourceInput } from '../structs/event-source-parse.js' +export type { EventSourceFunc, EventSourceFuncArg } from '../event-sources/func-event-source.js' +export type { EventInput, EventInputTransformer } from '../structs/event-parse.js' +export type { FormatterInput } from '../datelib/formatting.js' +export type { CssDimValue } from '../scrollgrid/util.js' +export type { BusinessHoursInput } from '../structs/business-hours.js' +export type { LocaleSingularArg, LocaleInput } from '../datelib/locale.js' +export type { OverlapFunc, ConstraintInput, AllowFunc } from '../structs/constraint.js' +export type { PluginDef, PluginDefInput } from '../plugin-system-struct.js' +export type { ViewComponentType, SpecificViewContentArg, SpecificViewMountArg } from '../structs/view-config.js' +export type { ClassNamesGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler } from '../common/render-hook.js' +export type { NowIndicatorContentArg, NowIndicatorMountArg } from '../common/NowIndicatorContainer.js' +export type { WeekNumberContentArg, WeekNumberMountArg } from '../common/WeekNumberContainer.js' +export type { MoreLinkContentArg, MoreLinkMountArg } from '../common/MoreLinkContainer.js' +export * from '../common/more-link-public-types.js' +export type { + SlotLaneContentArg, SlotLaneMountArg, + SlotLabelContentArg, SlotLabelMountArg, + AllDayContentArg, AllDayMountArg, + DayHeaderContentArg, + DayHeaderMountArg, +} from '../render-hook-misc.js' +export type { DayCellContentArg, DayCellMountArg } from '../common/DayCellContainer.js' +export type { ViewContentArg, ViewMountArg } from '../common/ViewContainer.js' +export type { EventClickArg } from '../interactions/EventClicking.js' +export type { EventHoveringArg } from '../interactions/EventHovering.js' +export type { DateSelectArg, DateUnselectArg } from '../calendar-utils.js' +export type { WeekNumberCalculation } from '../datelib/env.js' +export type { ToolbarInput, CustomButtonInput, ButtonIconsInput, ButtonTextCompoundInput } from '../toolbar-struct.js' +export type { EventContentArg, EventMountArg } from '../component/event-rendering.js' +export type { DatesSetArg } from '../dates-set.js' +export type { EventAddArg, EventChangeArg, EventDropArg, EventRemoveArg } from '../event-crud.js' +export type { ButtonHintCompoundInput } from '../toolbar-struct.js' +export type { CustomRenderingHandler, CustomRenderingStore } from '../content-inject/CustomRenderingStore.js' +export type { DateSpanApi, DatePointApi } from '../structs/date-span.js' +export type { DateSelectionApi } from '../calendar-utils.js' + +// used by some args +export type { Duration } from '../datelib/duration.js' diff --git a/fullcalendar-main/packages/core/src/calendar-utils.ts b/fullcalendar-main/packages/core/src/calendar-utils.ts new file mode 100644 index 0000000..2ecc711 --- /dev/null +++ b/fullcalendar-main/packages/core/src/calendar-utils.ts @@ -0,0 +1,79 @@ +import { PointerDragEvent } from './interactions/pointer.js' +import { buildDateSpanApi, DateSpanApi, DatePointApi, DateSpan } from './structs/date-span.js' +import { CalendarContext } from './CalendarContext.js' +import { ViewApi } from './api/ViewApi.js' +import { ViewImpl } from './api/ViewImpl.js' +import { DateMarker, startOfDay } from './datelib/marker.js' + +export interface DateClickApi extends DatePointApi { + dayEl: HTMLElement + jsEvent: UIEvent + view: ViewApi +} + +export interface DateSelectionApi extends DateSpanApi { + jsEvent: UIEvent + view: ViewApi +} + +export type DatePointTransform = (dateSpan: DateSpan, context: CalendarContext) => any +export type DateSpanTransform = (dateSpan: DateSpan, context: CalendarContext) => any + +export type CalendarInteraction = { destroy: () => void } +export type CalendarInteractionClass = { new(context: CalendarContext): CalendarInteraction } + +export type OptionChangeHandler = (propValue: any, context: CalendarContext) => void +export type OptionChangeHandlerMap = { [propName: string]: OptionChangeHandler } + +export interface DateSelectArg extends DateSpanApi { + jsEvent: MouseEvent | null + view: ViewApi +} + +export function triggerDateSelect(selection: DateSpan, pev: PointerDragEvent | null, context: CalendarContext & { viewApi?: ViewImpl }) { + context.emitter.trigger('select', { + ...buildDateSpanApiWithContext(selection, context), + jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655 + view: context.viewApi || context.calendarApi.view, + } as DateSelectArg) +} + +export interface DateUnselectArg { + jsEvent: MouseEvent + view: ViewApi +} + +export function triggerDateUnselect(pev: PointerDragEvent | null, context: CalendarContext & { viewApi?: ViewImpl }) { + context.emitter.trigger('unselect', { + jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655 + view: context.viewApi || context.calendarApi.view, + } as DateUnselectArg) +} + +export function buildDateSpanApiWithContext(dateSpan: DateSpan, context: CalendarContext) { + let props = {} as DateSpanApi + + for (let transform of context.pluginHooks.dateSpanTransforms) { + Object.assign(props, transform(dateSpan, context)) + } + + Object.assign(props, buildDateSpanApi(dateSpan, context.dateEnv)) + + return props +} + +// Given an event's allDay status and start date, return what its fallback end date should be. +// TODO: rename to computeDefaultEventEnd +export function getDefaultEventEnd(allDay: boolean, marker: DateMarker, context: CalendarContext): DateMarker { + let { dateEnv, options } = context + let end = marker + + if (allDay) { + end = startOfDay(end) + end = dateEnv.add(end, options.defaultAllDayEventDuration) + } else { + end = dateEnv.add(end, options.defaultTimedEventDuration) + } + + return end +} diff --git a/fullcalendar-main/packages/core/src/common/DayCellContainer.tsx b/fullcalendar-main/packages/core/src/common/DayCellContainer.tsx new file mode 100644 index 0000000..593f748 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/DayCellContainer.tsx @@ -0,0 +1,117 @@ +import { ComponentChild, createElement } from '../preact.js' +import { DateMarker } from '../datelib/marker.js' +import { DateRange } from '../datelib/date-range.js' +import { getDateMeta, DateMeta, getDayClassNames } from '../component/date-rendering.js' +import { createFormatter } from '../datelib/formatting.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { formatDayString } from '../datelib/formatting-utils.js' +import { MountArg } from './render-hook.js' +import { ViewApi } from '../api/ViewApi.js' +import { BaseComponent } from '../vdom-util.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { memoizeObjArg } from '../util/memoize.js' +import { Dictionary, ViewOptions } from '../options.js' +import { DateEnv } from '../datelib/env.js' +import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js' +import { ElProps, hasCustomRenderingHandler } from '../content-inject/ContentInjector.js' + +export interface DayCellContentArg extends DateMeta { + date: DateMarker // localized + view: ViewApi + dayNumberText: string + [extraProp: string]: any // so can include a resource +} + +export type DayCellMountArg = MountArg<DayCellContentArg> + +export interface DayCellContainerProps extends Partial<ElProps> { + date: DateMarker + dateProfile: DateProfile + todayRange: DateRange + isMonthStart?: boolean + showDayNumber?: boolean // defaults to false + extraRenderProps?: Dictionary + defaultGenerator?: (renderProps: DayCellContentArg) => ComponentChild + children?: InnerContainerFunc<DayCellContentArg> +} + +const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' }) + +export class DayCellContainer extends BaseComponent<DayCellContainerProps> { + refineRenderProps = memoizeObjArg(refineRenderProps) + + render() { + let { props, context } = this + let { options } = context + let renderProps = this.refineRenderProps({ + date: props.date, + dateProfile: props.dateProfile, + todayRange: props.todayRange, + isMonthStart: props.isMonthStart || false, + showDayNumber: props.showDayNumber, + extraRenderProps: props.extraRenderProps, + viewApi: context.viewApi, + dateEnv: context.dateEnv, + monthStartFormat: options.monthStartFormat, + }) + + return ( + <ContentContainer + {...props /* includes children */} + elClasses={[ + ...getDayClassNames(renderProps, context.theme), + ...(props.elClasses || []), + ]} + elAttrs={{ + ...props.elAttrs, + ...(renderProps.isDisabled ? {} : { 'data-date': formatDayString(props.date) }), + }} + renderProps={renderProps} + generatorName="dayCellContent" + customGenerator={options.dayCellContent} + defaultGenerator={props.defaultGenerator} + classNameGenerator={ + // don't use custom classNames if disabled + renderProps.isDisabled ? undefined : options.dayCellClassNames + } + didMount={options.dayCellDidMount} + willUnmount={options.dayCellWillUnmount} + /> + ) + } +} + +export function hasCustomDayCellContent(options: ViewOptions): boolean { + return Boolean(options.dayCellContent || hasCustomRenderingHandler('dayCellContent', options)) +} + +// Render Props + +interface DayCellRenderPropsInput { + date: DateMarker // generic + dateProfile: DateProfile + todayRange: DateRange + dateEnv: DateEnv + viewApi: ViewApi + monthStartFormat: DateFormatter + isMonthStart: boolean // defaults to false + showDayNumber?: boolean // defaults to false + extraRenderProps?: Dictionary // so can include a resource +} + +function refineRenderProps(raw: DayCellRenderPropsInput): DayCellContentArg { + let { date, dateEnv, dateProfile, isMonthStart } = raw + let dayMeta = getDateMeta(date, raw.todayRange, null, dateProfile) + let dayNumberText = raw.showDayNumber ? ( + dateEnv.format(date, isMonthStart ? raw.monthStartFormat : DAY_NUM_FORMAT) + ) : '' + + return { + date: dateEnv.toDate(date), + view: raw.viewApi, + ...dayMeta, + isMonthStart, + dayNumberText, + ...raw.extraRenderProps, + } +} diff --git a/fullcalendar-main/packages/core/src/common/DayHeader.tsx b/fullcalendar-main/packages/core/src/common/DayHeader.tsx new file mode 100644 index 0000000..cc97425 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/DayHeader.tsx @@ -0,0 +1,65 @@ +import { BaseComponent } from '../vdom-util.js' +import { DateMarker } from '../datelib/marker.js' +import { computeFallbackHeaderFormat } from './table-utils.js' +import { VNode, createElement } from '../preact.js' +import { TableDateCell } from './TableDateCell.js' +import { TableDowCell } from './TableDowCell.js' +import { NowTimer } from '../NowTimer.js' +import { DateRange } from '../datelib/date-range.js' +import { memoize } from '../util/memoize.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { DateFormatter } from '../datelib/DateFormatter.js' + +export interface DayHeaderProps { + dateProfile: DateProfile + dates: DateMarker[] + datesRepDistinctDays: boolean + renderIntro?: (rowKey: string) => VNode +} + +export class DayHeader extends BaseComponent<DayHeaderProps> { // TODO: rename to DayHeaderTr? + createDayHeaderFormatter = memoize(createDayHeaderFormatter) + + render() { + let { context } = this + let { dates, dateProfile, datesRepDistinctDays, renderIntro } = this.props + + let dayHeaderFormat = this.createDayHeaderFormatter( + context.options.dayHeaderFormat, + datesRepDistinctDays, + dates.length, + ) + + return ( + <NowTimer unit="day"> + {(nowDate: DateMarker, todayRange: DateRange) => ( + <tr role="row"> + {renderIntro && renderIntro('day')} + {dates.map((date) => ( + datesRepDistinctDays ? ( + <TableDateCell + key={date.toISOString()} + date={date} + dateProfile={dateProfile} + todayRange={todayRange} + colCnt={dates.length} + dayHeaderFormat={dayHeaderFormat} + /> + ) : ( + <TableDowCell + key={date.getUTCDay()} + dow={date.getUTCDay()} + dayHeaderFormat={dayHeaderFormat} + /> + ) + ))} + </tr> + )} + </NowTimer> + ) + } +} + +function createDayHeaderFormatter(explicitFormat: DateFormatter, datesRepDistinctDays, dateCnt) { + return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt) +} diff --git a/fullcalendar-main/packages/core/src/common/DaySeriesModel.ts b/fullcalendar-main/packages/core/src/common/DaySeriesModel.ts new file mode 100644 index 0000000..940ea65 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/DaySeriesModel.ts @@ -0,0 +1,81 @@ +import { DateProfileGenerator } from '../DateProfileGenerator.js' +import { DateMarker, addDays, diffDays } from '../datelib/marker.js' +import { DateRange } from '../datelib/date-range.js' + +export interface DaySeriesSeg { + firstIndex: number + lastIndex: number + isStart: boolean + isEnd: boolean +} + +export class DaySeriesModel { + cnt: number + dates: DateMarker[] // whole-day dates for each column. left to right + indices: number[] // for each day from start, the offset + + constructor(range: DateRange, dateProfileGenerator: DateProfileGenerator) { + let date: DateMarker = range.start + let { end } = range + let indices: number[] = [] + let dates: DateMarker[] = [] + let dayIndex = -1 + + while (date < end) { // loop each day from start to end + if (dateProfileGenerator.isHiddenDay(date)) { + indices.push(dayIndex + 0.5) // mark that it's between indices + } else { + dayIndex += 1 + indices.push(dayIndex) + dates.push(date) + } + date = addDays(date, 1) + } + + this.dates = dates + this.indices = indices + this.cnt = dates.length + } + + sliceRange(range: DateRange): DaySeriesSeg | null { + let firstIndex = this.getDateDayIndex(range.start) // inclusive first index + let lastIndex = this.getDateDayIndex(addDays(range.end, -1)) // inclusive last index + + let clippedFirstIndex = Math.max(0, firstIndex) + let clippedLastIndex = Math.min(this.cnt - 1, lastIndex) + + // deal with in-between indices + clippedFirstIndex = Math.ceil(clippedFirstIndex) // in-between starts round to next cell + clippedLastIndex = Math.floor(clippedLastIndex) // in-between ends round to prev cell + + if (clippedFirstIndex <= clippedLastIndex) { + return { + firstIndex: clippedFirstIndex, + lastIndex: clippedLastIndex, + isStart: firstIndex === clippedFirstIndex, + isEnd: lastIndex === clippedLastIndex, + } + } + return null + } + + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + private getDateDayIndex(date: DateMarker) { + let { indices } = this + let dayOffset = Math.floor(diffDays(this.dates[0], date)) + + if (dayOffset < 0) { + return indices[0] - 1 + } + + if (dayOffset >= indices.length) { + return indices[indices.length - 1] + 1 + } + + return indices[dayOffset] + } +} diff --git a/fullcalendar-main/packages/core/src/common/DayTableModel.ts b/fullcalendar-main/packages/core/src/common/DayTableModel.ts new file mode 100644 index 0000000..12496ea --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/DayTableModel.ts @@ -0,0 +1,120 @@ +import { DaySeriesModel } from './DaySeriesModel.js' +import { DateRange } from '../datelib/date-range.js' +import { DateMarker } from '../datelib/marker.js' +import { Seg } from '../component/DateComponent.js' +import { Dictionary } from '../options.js' + +export interface DayTableSeg extends Seg { + row: number + firstCol: number + lastCol: number +} + +export interface DayTableCell { + key: string // probably just the serialized date, but could be other metadata if this col is specific to another entity + date: DateMarker + extraRenderProps?: Dictionary + extraDataAttrs?: Dictionary + extraClassNames?: string[] + extraDateSpan?: Dictionary +} + +export class DayTableModel { + rowCnt: number + colCnt: number + cells: DayTableCell[][] + headerDates: DateMarker[] + + private daySeries: DaySeriesModel + + constructor(daySeries: DaySeriesModel, breakOnWeeks: boolean) { + let { dates } = daySeries + let daysPerRow + let firstDay + let rowCnt + + if (breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dates[0].getUTCDay() + for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) { + if (dates[daysPerRow].getUTCDay() === firstDay) { + break + } + } + rowCnt = Math.ceil(dates.length / daysPerRow) + } else { + rowCnt = 1 + daysPerRow = dates.length + } + + this.rowCnt = rowCnt + this.colCnt = daysPerRow + this.daySeries = daySeries + this.cells = this.buildCells() + this.headerDates = this.buildHeaderDates() + } + + private buildCells() { + let rows = [] + + for (let row = 0; row < this.rowCnt; row += 1) { + let cells = [] + + for (let col = 0; col < this.colCnt; col += 1) { + cells.push( + this.buildCell(row, col), + ) + } + + rows.push(cells) + } + + return rows + } + + private buildCell(row, col): DayTableCell { + let date = this.daySeries.dates[row * this.colCnt + col] + return { + key: date.toISOString(), + date, + } + } + + private buildHeaderDates() { + let dates = [] + + for (let col = 0; col < this.colCnt; col += 1) { + dates.push(this.cells[0][col].date) + } + + return dates + } + + sliceRange(range: DateRange): DayTableSeg[] { + let { colCnt } = this + let seriesSeg = this.daySeries.sliceRange(range) + let segs: DayTableSeg[] = [] + + if (seriesSeg) { + let { firstIndex, lastIndex } = seriesSeg + let index = firstIndex + + while (index <= lastIndex) { + let row = Math.floor(index / colCnt) + let nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1) + + segs.push({ + row, + firstCol: index % colCnt, + lastCol: (nextIndex - 1) % colCnt, + isStart: seriesSeg.isStart && index === firstIndex, + isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex, + }) + + index = nextIndex + } + } + + return segs + } +} diff --git a/fullcalendar-main/packages/core/src/common/Emitter.ts b/fullcalendar-main/packages/core/src/common/Emitter.ts new file mode 100644 index 0000000..d4c2e6c --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/Emitter.ts @@ -0,0 +1,59 @@ +export interface HandlerFuncTypeHash { + [eventName: string]: (...args: any[]) => any // with all properties required +} + +export class Emitter<HandlerFuncs extends HandlerFuncTypeHash> { + private handlers: { [Prop in keyof HandlerFuncs]?: HandlerFuncs[Prop][] } = {} + + private options: Partial<HandlerFuncs> + + private thisContext: any = null + + setThisContext(thisContext) { + this.thisContext = thisContext + } + + setOptions(options: Partial<HandlerFuncs>) { + this.options = options + } + + on<Prop extends keyof HandlerFuncs>(type: Prop, handler: HandlerFuncs[Prop]) { + addToHash(this.handlers, type, handler) + } + + off<Prop extends keyof HandlerFuncs>(type: Prop, handler?: HandlerFuncs[Prop]) { + removeFromHash(this.handlers, type, handler) + } + + trigger<Prop extends keyof HandlerFuncs>(type: Prop, ...args: Parameters<HandlerFuncs[Prop]>) { + let attachedHandlers = this.handlers[type] || [] + let optionHandler = this.options && this.options[type] + let handlers = [].concat(optionHandler || [], attachedHandlers) + + for (let handler of handlers) { + handler.apply(this.thisContext, args) + } + } + + hasHandlers(type: keyof HandlerFuncs): boolean { + return Boolean( + (this.handlers[type] && this.handlers[type].length) || + (this.options && this.options[type]), + ) + } +} + +function addToHash(hash, type, handler) { + (hash[type] || (hash[type] = [])) + .push(handler) +} + +function removeFromHash(hash, type, handler?) { + if (handler) { + if (hash[type]) { + hash[type] = hash[type].filter((func) => func !== handler) + } + } else { + delete hash[type] // remove all handler funcs for this type + } +} diff --git a/fullcalendar-main/packages/core/src/common/EventContainer.tsx b/fullcalendar-main/packages/core/src/common/EventContainer.tsx new file mode 100644 index 0000000..78c498d --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/EventContainer.tsx @@ -0,0 +1,99 @@ +import { ComponentChild, createElement } from '../preact.js' +import { BaseComponent } from '../vdom-util.js' +import { Seg } from '../component/DateComponent.js' +import { EventImpl } from '../api/EventImpl.js' +import { + computeSegDraggable, + computeSegStartResizable, + computeSegEndResizable, + EventContentArg, + getEventClassNames, + setElSeg, +} from '../component/event-rendering.js' +import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js' +import { ElProps } from '../content-inject/ContentInjector.js' + +export interface MinimalEventProps { + seg: Seg + isDragging: boolean // rename to isMirrorDragging? make optional? + isResizing: boolean // rename to isMirrorResizing? make optional? + isDateSelecting: boolean // rename to isMirrorDateSelecting? make optional? + isSelected: boolean + isPast: boolean + isFuture: boolean + isToday: boolean +} + +export type EventContainerProps = ElProps & MinimalEventProps & { + defaultGenerator: (renderProps: EventContentArg) => ComponentChild + disableDragging?: boolean + disableResizing?: boolean + timeText: string + children?: InnerContainerFunc<EventContentArg> +} + +export class EventContainer extends BaseComponent<EventContainerProps> { + el: HTMLElement + + render() { + const { props, context } = this + const { options } = context + const { seg } = props + const { eventRange } = seg + const { ui } = eventRange + + const renderProps: EventContentArg = { + event: new EventImpl(context, eventRange.def, eventRange.instance), + view: context.viewApi, + timeText: props.timeText, + textColor: ui.textColor, + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + isDraggable: !props.disableDragging && computeSegDraggable(seg, context), + isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context), + isEndResizable: !props.disableResizing && computeSegEndResizable(seg, context), + isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting), + isStart: Boolean(seg.isStart), + isEnd: Boolean(seg.isEnd), + isPast: Boolean(props.isPast), // TODO: don't cast. getDateMeta does it + isFuture: Boolean(props.isFuture), // TODO: don't cast. getDateMeta does it + isToday: Boolean(props.isToday), // TODO: don't cast. getDateMeta does it + isSelected: Boolean(props.isSelected), + isDragging: Boolean(props.isDragging), + isResizing: Boolean(props.isResizing), + } + + return ( + <ContentContainer + {...props /* contains children */} + elRef={this.handleEl} + elClasses={[ + ...getEventClassNames(renderProps), + ...seg.eventRange.ui.classNames, + ...(props.elClasses || []), + ]} + renderProps={renderProps} + generatorName="eventContent" + customGenerator={options.eventContent} + defaultGenerator={props.defaultGenerator} + classNameGenerator={options.eventClassNames} + didMount={options.eventDidMount} + willUnmount={options.eventWillUnmount} + /> + ) + } + + handleEl = (el: HTMLElement | null) => { + this.el = el + + if (el) { + setElSeg(el, this.props.seg) + } + } + + componentDidUpdate(prevProps: EventContainerProps): void { + if (this.el && this.props.seg !== prevProps.seg) { + setElSeg(this.el, this.props.seg) + } + } +} diff --git a/fullcalendar-main/packages/core/src/common/MoreLinkContainer.tsx b/fullcalendar-main/packages/core/src/common/MoreLinkContainer.tsx new file mode 100644 index 0000000..4ab638e --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/MoreLinkContainer.tsx @@ -0,0 +1,229 @@ +import { EventImpl } from '../api/EventImpl.js' +import { Seg } from '../component/DateComponent.js' +import { DateRange } from '../datelib/date-range.js' +import { addDays, DateMarker } from '../datelib/marker.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { Dictionary } from '../options.js' +import { elementClosest, getUniqueDomId } from '../util/dom-manip.js' +import { formatWithOrdinals } from '../util/misc.js' +import { createElement, Fragment, ComponentChild, RefObject } from '../preact.js' +import { BaseComponent, setRef } from '../vdom-util.js' +import { ViewApi } from '../api/ViewApi.js' +import { ViewContext, ViewContextType } from '../ViewContext.js' +import { MorePopover } from './MorePopover.js' +import { MountArg } from './render-hook.js' +import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js' +import { ElProps } from '../content-inject/ContentInjector.js' +import { createAriaClickAttrs } from '../util/dom-event.js' + +export interface MoreLinkContainerProps extends Partial<ElProps> { + dateProfile: DateProfile + todayRange: DateRange + allDayDate: DateMarker | null + moreCnt: number // can't always derive from hiddenSegs. some hiddenSegs might be due to lack of dimensions + allSegs: Seg[] + hiddenSegs: Seg[] + extraDateSpan?: Dictionary + alignmentElRef?: RefObject<HTMLElement> // will use internal <a> if unspecified + alignGridTop?: boolean // for popover + forceTimed?: boolean // for popover + popoverContent: () => ComponentChild + defaultGenerator?: (renderProps: MoreLinkContentArg) => ComponentChild + children?: InnerContainerFunc<MoreLinkContentArg> +} + +export interface MoreLinkContentArg { + num: number + text: string + shortText: string + view: ViewApi +} + +export type MoreLinkMountArg = MountArg<MoreLinkContentArg> + +interface MoreLinkContainerState { + isPopoverOpen: boolean + popoverId: string +} + +export class MoreLinkContainer extends BaseComponent<MoreLinkContainerProps, MoreLinkContainerState> { + private linkEl: HTMLElement + private parentEl: HTMLElement + + state = { + isPopoverOpen: false, + popoverId: getUniqueDomId(), + } + + render() { + let { props, state } = this + return ( + <ViewContextType.Consumer> + {(context: ViewContext) => { + let { viewApi, options, calendarApi } = context + let { moreLinkText } = options + let { moreCnt } = props + let range = computeRange(props) + + let text = typeof moreLinkText === 'function' // TODO: eventually use formatWithOrdinals + ? moreLinkText.call(calendarApi, moreCnt) + : `+${moreCnt} ${moreLinkText}` + let hint = formatWithOrdinals(options.moreLinkHint, [moreCnt], text) + + let renderProps: MoreLinkContentArg = { + num: moreCnt, + shortText: `+${moreCnt}`, // TODO: offer hook or i18n? + text, + view: viewApi, + } + + return ( + <Fragment> + {Boolean(props.moreCnt) && ( + <ContentContainer + elTag={props.elTag || 'a'} + elRef={this.handleLinkEl} + elClasses={[ + ...(props.elClasses || []), + 'fc-more-link', + ]} + elStyle={props.elStyle} + elAttrs={{ + ...props.elAttrs, + ...createAriaClickAttrs(this.handleClick), + title: hint, + 'aria-expanded': state.isPopoverOpen, + 'aria-controls': state.isPopoverOpen ? state.popoverId : '', + }} + renderProps={renderProps} + generatorName="moreLinkContent" + customGenerator={options.moreLinkContent} + defaultGenerator={props.defaultGenerator || renderMoreLinkInner} + classNameGenerator={options.moreLinkClassNames} + didMount={options.moreLinkDidMount} + willUnmount={options.moreLinkWillUnmount} + >{props.children}</ContentContainer> + )} + {state.isPopoverOpen && ( + <MorePopover + id={state.popoverId} + startDate={range.start} + endDate={range.end} + dateProfile={props.dateProfile} + todayRange={props.todayRange} + extraDateSpan={props.extraDateSpan} + parentEl={this.parentEl} + alignmentEl={ + props.alignmentElRef ? + props.alignmentElRef.current : + this.linkEl + } + alignGridTop={props.alignGridTop} + forceTimed={props.forceTimed} + onClose={this.handlePopoverClose} + > + {props.popoverContent()} + </MorePopover> + )} + </Fragment> + ) + }} + </ViewContextType.Consumer> + ) + } + + componentDidMount() { + this.updateParentEl() + } + + componentDidUpdate() { + this.updateParentEl() + } + + handleLinkEl = (linkEl: HTMLElement | null) => { + this.linkEl = linkEl + + if (this.props.elRef) { + setRef(this.props.elRef, linkEl) + } + } + + updateParentEl() { + if (this.linkEl) { + this.parentEl = elementClosest(this.linkEl, '.fc-view-harness') + } + } + + handleClick = (ev: MouseEvent) => { + let { props, context } = this + let { moreLinkClick } = context.options + let date = computeRange(props).start + + function buildPublicSeg(seg: Seg) { + let { def, instance, range } = seg.eventRange + return { + event: new EventImpl(context, def, instance), + start: context.dateEnv.toDate(range.start), + end: context.dateEnv.toDate(range.end), + isStart: seg.isStart, + isEnd: seg.isEnd, + } + } + + if (typeof moreLinkClick === 'function') { + moreLinkClick = moreLinkClick({ + date, + allDay: Boolean(props.allDayDate), + allSegs: props.allSegs.map(buildPublicSeg), + hiddenSegs: props.hiddenSegs.map(buildPublicSeg), + jsEvent: ev, + view: context.viewApi, + }) as string | undefined + } + + if (!moreLinkClick || moreLinkClick === 'popover') { + this.setState({ isPopoverOpen: true }) + } else if (typeof moreLinkClick === 'string') { // a view name + context.calendarApi.zoomTo(date, moreLinkClick) + } + } + + handlePopoverClose = () => { + this.setState({ isPopoverOpen: false }) + } +} + +function renderMoreLinkInner(props: MoreLinkContentArg) { + return props.text +} + +function computeRange(props: MoreLinkContainerProps): DateRange { + if (props.allDayDate) { + return { + start: props.allDayDate, + end: addDays(props.allDayDate, 1), + } + } + + let { hiddenSegs } = props + return { + start: computeEarliestSegStart(hiddenSegs), + end: computeLatestSegEnd(hiddenSegs), + } +} + +export function computeEarliestSegStart(segs: Seg[]): DateMarker { + return segs.reduce(pickEarliestStart).eventRange.range.start +} + +function pickEarliestStart(seg0: Seg, seg1: Seg): Seg { + return seg0.eventRange.range.start < seg1.eventRange.range.start ? seg0 : seg1 +} + +function computeLatestSegEnd(segs: Seg[]): DateMarker { + return segs.reduce(pickLatestEnd).eventRange.range.end +} + +function pickLatestEnd(seg0: Seg, seg1: Seg): Seg { + return seg0.eventRange.range.end > seg1.eventRange.range.end ? seg0 : seg1 +} diff --git a/fullcalendar-main/packages/core/src/common/MorePopover.tsx b/fullcalendar-main/packages/core/src/common/MorePopover.tsx new file mode 100644 index 0000000..e2bf8cb --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/MorePopover.tsx @@ -0,0 +1,114 @@ +import { DateComponent } from '../component/DateComponent.js' +import { DateRange } from '../datelib/date-range.js' +import { DateMarker } from '../datelib/marker.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { Hit } from '../interactions/hit.js' +import { Dictionary } from '../options.js' +import { createElement, ComponentChildren } from '../preact.js' +import { DayCellContainer, hasCustomDayCellContent } from './DayCellContainer.js' +import { Popover } from './Popover.js' + +export interface MorePopoverProps { + id: string + startDate: DateMarker + endDate: DateMarker + dateProfile: DateProfile + parentEl: HTMLElement + alignmentEl: HTMLElement + alignGridTop?: boolean + forceTimed?: boolean + todayRange: DateRange + extraDateSpan: Dictionary + children: ComponentChildren + onClose?: () => void +} + +export class MorePopover extends DateComponent<MorePopoverProps> { + rootEl: HTMLElement + + render() { + let { options, dateEnv } = this.context + let { props } = this + let { startDate, todayRange, dateProfile } = props + let title = dateEnv.format(startDate, options.dayPopoverFormat) + + return ( + <DayCellContainer + elRef={this.handleRootEl} + date={startDate} + dateProfile={dateProfile} + todayRange={todayRange} + > + {(InnerContent, renderProps, elAttrs) => ( + <Popover + elRef={elAttrs.ref} + id={props.id} + title={title} + extraClassNames={ + ['fc-more-popover'].concat( + (elAttrs.className as (string | undefined)) || [], + ) + } + extraAttrs={elAttrs /* TODO: make these time-based when not whole-day? */} + parentEl={props.parentEl} + alignmentEl={props.alignmentEl} + alignGridTop={props.alignGridTop} + onClose={props.onClose} + > + {hasCustomDayCellContent(options) && ( + <InnerContent + elTag="div" + elClasses={['fc-more-popover-misc']} + /> + )} + {props.children} + </Popover> + )} + </DayCellContainer> + ) + } + + handleRootEl = (rootEl: HTMLElement | null) => { + this.rootEl = rootEl + + if (rootEl) { + this.context.registerInteractiveComponent(this, { + el: rootEl, + useEventCenter: false, + }) + } else { + this.context.unregisterInteractiveComponent(this) + } + } + + queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit { + let { rootEl, props } = this + + if ( + positionLeft >= 0 && positionLeft < elWidth && + positionTop >= 0 && positionTop < elHeight + ) { + return { + dateProfile: props.dateProfile, + dateSpan: { + allDay: !props.forceTimed, + range: { + start: props.startDate, + end: props.endDate, + }, + ...props.extraDateSpan, + }, + dayEl: rootEl, + rect: { + left: 0, + top: 0, + right: elWidth, + bottom: elHeight, + }, + layer: 1, // important when comparing with hits from other components + } + } + + return null + } +} diff --git a/fullcalendar-main/packages/core/src/common/NowIndicatorContainer.tsx b/fullcalendar-main/packages/core/src/common/NowIndicatorContainer.tsx new file mode 100644 index 0000000..1c42c5c --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/NowIndicatorContainer.tsx @@ -0,0 +1,47 @@ +import { MountArg } from './render-hook.js' +import { DateMarker } from '../datelib/marker.js' +import { ViewContext, ViewContextType } from '../ViewContext.js' +import { createElement } from '../preact.js' +import { ViewApi } from '../api/ViewApi.js' +import { ElProps } from '../content-inject/ContentInjector.js' +import { InnerContainerFunc, ContentContainer } from '../content-inject/ContentContainer.js' + +export interface NowIndicatorContainerProps extends Partial<ElProps> { + isAxis: boolean + date: DateMarker + children?: InnerContainerFunc<NowIndicatorContentArg> +} + +export interface NowIndicatorContentArg { + isAxis: boolean + date: Date + view: ViewApi +} + +export type NowIndicatorMountArg = MountArg<NowIndicatorContentArg> + +export const NowIndicatorContainer = (props: NowIndicatorContainerProps) => ( + <ViewContextType.Consumer> + {(context: ViewContext) => { + let { options } = context + let renderProps: NowIndicatorContentArg = { + isAxis: props.isAxis, + date: context.dateEnv.toDate(props.date), + view: context.viewApi, + } + + return ( + <ContentContainer + {...props /* includes children */} + elTag={props.elTag || 'div'} + renderProps={renderProps} + generatorName="nowIndicatorContent" + customGenerator={options.nowIndicatorContent} + classNameGenerator={options.nowIndicatorClassNames} + didMount={options.nowIndicatorDidMount} + willUnmount={options.nowIndicatorWillUnmount} + /> + ) + }} + </ViewContextType.Consumer> +) diff --git a/fullcalendar-main/packages/core/src/common/Popover.tsx b/fullcalendar-main/packages/core/src/common/Popover.tsx new file mode 100644 index 0000000..1b6be9f --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/Popover.tsx @@ -0,0 +1,132 @@ +import { Dictionary } from '../options.js' +import { computeClippedClientRect } from '../util/dom-geom.js' +import { applyStyle, elementClosest, getEventTargetViaRoot, getUniqueDomId } from '../util/dom-manip.js' +import { createElement, ComponentChildren, Ref, createPortal } from '../preact.js' +import { BaseComponent, setRef } from '../vdom-util.js' + +export interface PopoverProps { + elRef?: Ref<HTMLElement> + id: string + title: string + extraClassNames?: string[] + extraAttrs?: Dictionary + parentEl: HTMLElement + alignmentEl: HTMLElement + alignGridTop?: boolean + children?: ComponentChildren + onClose?: () => void +} + +const PADDING_FROM_VIEWPORT = 10 + +export class Popover extends BaseComponent<PopoverProps> { + private rootEl: HTMLElement + state = { + titleId: getUniqueDomId(), + } + + render(): any { + let { theme, options } = this.context + let { props, state } = this + let classNames = [ + 'fc-popover', + theme.getClass('popover'), + ].concat( + props.extraClassNames || [], + ) + + return createPortal( + <div + {...props.extraAttrs} + id={props.id} + className={classNames.join(' ')} + aria-labelledby={state.titleId} + ref={this.handleRootEl} + > + <div className={'fc-popover-header ' + theme.getClass('popoverHeader')}> + <span className="fc-popover-title" id={state.titleId}> + {props.title} + </span> + <span + className={'fc-popover-close ' + theme.getIconClass('close')} + title={options.closeHint} + onClick={this.handleCloseClick} + /> + </div> + <div className={'fc-popover-body ' + theme.getClass('popoverContent')}> + {props.children} + </div> + </div>, + props.parentEl, + ) + } + + componentDidMount() { + document.addEventListener('mousedown', this.handleDocumentMouseDown) + document.addEventListener('keydown', this.handleDocumentKeyDown) + this.updateSize() + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleDocumentMouseDown) + document.removeEventListener('keydown', this.handleDocumentKeyDown) + } + + handleRootEl = (el: HTMLElement | null) => { + this.rootEl = el + + if (this.props.elRef) { + setRef(this.props.elRef, el) + } + } + + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + handleDocumentMouseDown = (ev) => { + // only hide the popover if the click happened outside the popover + const target = getEventTargetViaRoot(ev) as HTMLElement + if (!this.rootEl.contains(target)) { + this.handleCloseClick() + } + } + + handleDocumentKeyDown = (ev) => { + if (ev.key === 'Escape') { + this.handleCloseClick() + } + } + + handleCloseClick = () => { + let { onClose } = this.props + if (onClose) { + onClose() + } + } + + private updateSize() { + let { isRtl } = this.context + let { alignmentEl, alignGridTop } = this.props + let { rootEl } = this + + let alignmentRect = computeClippedClientRect(alignmentEl) + if (alignmentRect) { + let popoverDims = rootEl.getBoundingClientRect() + + // position relative to viewport + let popoverTop = alignGridTop + ? elementClosest(alignmentEl, '.fc-scrollgrid').getBoundingClientRect().top + : alignmentRect.top + let popoverLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left + + // constrain + popoverTop = Math.max(popoverTop, PADDING_FROM_VIEWPORT) + popoverLeft = Math.min(popoverLeft, document.documentElement.clientWidth - PADDING_FROM_VIEWPORT - popoverDims.width) + popoverLeft = Math.max(popoverLeft, PADDING_FROM_VIEWPORT) + + let origin = rootEl.offsetParent.getBoundingClientRect() + applyStyle(rootEl, { + top: popoverTop - origin.top, + left: popoverLeft - origin.left, + }) + } + } +} diff --git a/fullcalendar-main/packages/core/src/common/PositionCache.ts b/fullcalendar-main/packages/core/src/common/PositionCache.ts new file mode 100644 index 0000000..cc8aede --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/PositionCache.ts @@ -0,0 +1,125 @@ +/* +Records offset information for a set of elements, relative to an origin element. +Can record the left/right OR the top/bottom OR both. +Provides methods for querying the cache by position. +*/ +export class PositionCache { + els: HTMLElement[] // assumed to be siblings + originClientRect: ClientRect + + // arrays of coordinates (from topleft of originEl) + // caller can access these directly + lefts: any + rights: any + tops: any + bottoms: any + + constructor(originEl: HTMLElement, els: HTMLElement[], isHorizontal: boolean, isVertical: boolean) { + this.els = els + + let originClientRect = this.originClientRect = originEl.getBoundingClientRect() // relative to viewport top-left + + if (isHorizontal) { + this.buildElHorizontals(originClientRect.left) + } + + if (isVertical) { + this.buildElVerticals(originClientRect.top) + } + } + + // Populates the left/right internal coordinate arrays + buildElHorizontals(originClientLeft: number) { + let lefts = [] + let rights = [] + + for (let el of this.els) { + let rect = el.getBoundingClientRect() + lefts.push(rect.left - originClientLeft) + rights.push(rect.right - originClientLeft) + } + + this.lefts = lefts + this.rights = rights + } + + // Populates the top/bottom internal coordinate arrays + buildElVerticals(originClientTop: number) { + let tops = [] + let bottoms = [] + + for (let el of this.els) { + let rect = el.getBoundingClientRect() + tops.push(rect.top - originClientTop) + bottoms.push(rect.bottom - originClientTop) + } + + this.tops = tops + this.bottoms = bottoms + } + + // Given a left offset (from document left), returns the index of the el that it horizontally intersects. + // If no intersection is made, returns undefined. + leftToIndex(leftPosition: number) { + let { lefts, rights } = this + let len = lefts.length + let i + + for (i = 0; i < len; i += 1) { + if (leftPosition >= lefts[i] && leftPosition < rights[i]) { + return i + } + } + + return undefined // TODO: better + } + + // Given a top offset (from document top), returns the index of the el that it vertically intersects. + // If no intersection is made, returns undefined. + topToIndex(topPosition: number) { + let { tops, bottoms } = this + let len = tops.length + let i + + for (i = 0; i < len; i += 1) { + if (topPosition >= tops[i] && topPosition < bottoms[i]) { + return i + } + } + + return undefined // TODO: better + } + + // Gets the width of the element at the given index + getWidth(leftIndex: number) { + return this.rights[leftIndex] - this.lefts[leftIndex] + } + + // Gets the height of the element at the given index + getHeight(topIndex: number) { + return this.bottoms[topIndex] - this.tops[topIndex] + } + + similarTo(otherCache: PositionCache) { + return similarNumArrays(this.tops || [], otherCache.tops || []) && + similarNumArrays(this.bottoms || [], otherCache.bottoms || []) && + similarNumArrays(this.lefts || [], otherCache.lefts || []) && + similarNumArrays(this.rights || [], otherCache.rights || []) + } +} + +function similarNumArrays(a: number[], b: number[]): boolean { + const len = a.length + + if (len !== b.length) { + return false + } + + for (let i = 0; i < len; i++) { + if (Math.round(a[i]) !== Math.round(b[i])) { + return false + } + } + + return true +} diff --git a/fullcalendar-main/packages/core/src/common/StandardEvent.tsx b/fullcalendar-main/packages/core/src/common/StandardEvent.tsx new file mode 100644 index 0000000..061d3bd --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/StandardEvent.tsx @@ -0,0 +1,88 @@ +import { createElement, Fragment } from '../preact.js' +import { BaseComponent } from '../vdom-util.js' +import { buildSegTimeText, EventContentArg, getSegAnchorAttrs } from '../component/event-rendering.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { EventContainer } from './EventContainer.js' +import { Seg } from '../component/DateComponent.js' +import { ElRef } from '../content-inject/ContentInjector.js' + +export interface StandardEventProps { + elRef?: ElRef + elClasses?: string[] + seg: Seg + isDragging: boolean // rename to isMirrorDragging? make optional? + isResizing: boolean // rename to isMirrorResizing? make optional? + isDateSelecting: boolean // rename to isMirrorDateSelecting? make optional? + isSelected: boolean + isPast: boolean + isFuture: boolean + isToday: boolean + disableDragging?: boolean // defaults false + disableResizing?: boolean // defaults false + defaultTimeFormat: DateFormatter + defaultDisplayEventTime?: boolean // default true + defaultDisplayEventEnd?: boolean // default true +} + +// should not be a purecomponent +export class StandardEvent extends BaseComponent<StandardEventProps> { + render() { + let { props, context } = this + let { options } = context + let { seg } = props + let { ui } = seg.eventRange + let timeFormat = options.eventTimeFormat || props.defaultTimeFormat + let timeText = buildSegTimeText( + seg, + timeFormat, + context, + props.defaultDisplayEventTime, + props.defaultDisplayEventEnd, + ) + + return ( + <EventContainer + {...props /* includes elRef */} + elTag="a" + elStyle={{ + borderColor: ui.borderColor, + backgroundColor: ui.backgroundColor, + }} + elAttrs={getSegAnchorAttrs(seg, context)} + defaultGenerator={renderInnerContent} + timeText={timeText} + > + {(InnerContent, eventContentArg) => ( + <Fragment> + <InnerContent + elTag="div" + elClasses={['fc-event-main']} + elStyle={{ color: eventContentArg.textColor }} + /> + {Boolean(eventContentArg.isStartResizable) && ( + <div className="fc-event-resizer fc-event-resizer-start" /> + )} + {Boolean(eventContentArg.isEndResizable) && ( + <div className="fc-event-resizer fc-event-resizer-end" /> + )} + </Fragment> + )} + </EventContainer> + ) + } +} + +function renderInnerContent(innerProps: EventContentArg) { + return ( + <div className="fc-event-main-frame"> + {innerProps.timeText && ( + <div className="fc-event-time">{innerProps.timeText}</div> + )} + <div className="fc-event-title-container"> + <div className="fc-event-title fc-sticky"> + {innerProps.event.title || <Fragment> </Fragment>} + </div> + </div> + </div> + ) +} diff --git a/fullcalendar-main/packages/core/src/common/TableDateCell.tsx b/fullcalendar-main/packages/core/src/common/TableDateCell.tsx new file mode 100644 index 0000000..f95f656 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/TableDateCell.tsx @@ -0,0 +1,88 @@ +import { DateRange } from '../datelib/date-range.js' +import { getDayClassNames, getDateMeta } from '../component/date-rendering.js' +import { DateMarker } from '../datelib/marker.js' +import { createElement } from '../preact.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { formatDayString } from '../datelib/formatting-utils.js' +import { BaseComponent } from '../vdom-util.js' +import { buildNavLinkAttrs } from './nav-link.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { DayHeaderContentArg } from '../render-hook-misc.js' +import { Dictionary } from '../options.js' +import { CLASS_NAME, renderInner } from './table-cell-util.js' +import { ContentContainer } from '../content-inject/ContentContainer.js' + +export interface TableDateCellProps { + date: DateMarker + dateProfile: DateProfile + todayRange: DateRange + colCnt: number + dayHeaderFormat: DateFormatter + colSpan?: number + isSticky?: boolean // TODO: get this outta here somehow + extraDataAttrs?: Dictionary + extraRenderProps?: Dictionary +} + +// BAD name for this class now. used in the Header +export class TableDateCell extends BaseComponent<TableDateCellProps> { + render() { + let { dateEnv, options, theme, viewApi } = this.context + let { props } = this + let { date, dateProfile } = props + let dayMeta = getDateMeta(date, props.todayRange, null, dateProfile) + + let classNames = [CLASS_NAME].concat( + getDayClassNames(dayMeta, theme), + ) + let text = dateEnv.format(date, props.dayHeaderFormat) + + // if colCnt is 1, we are already in a day-view and don't need a navlink + let navLinkAttrs = (!dayMeta.isDisabled && props.colCnt > 1) + ? buildNavLinkAttrs(this.context, date) + : {} + + let renderProps: DayHeaderContentArg = { + date: dateEnv.toDate(date), + view: viewApi, + ...props.extraRenderProps, + text, + ...dayMeta, + } + + return ( + <ContentContainer + elTag="th" + elClasses={classNames} + elAttrs={{ + role: 'columnheader', + colSpan: props.colSpan, + 'data-date': !dayMeta.isDisabled ? formatDayString(date) : undefined, + ...props.extraDataAttrs, + }} + renderProps={renderProps} + generatorName="dayHeaderContent" + customGenerator={options.dayHeaderContent} + defaultGenerator={renderInner} + classNameGenerator={options.dayHeaderClassNames} + didMount={options.dayHeaderDidMount} + willUnmount={options.dayHeaderWillUnmount} + > + {(InnerContainer) => ( + <div className="fc-scrollgrid-sync-inner"> + {!dayMeta.isDisabled && ( + <InnerContainer + elTag="a" + elAttrs={navLinkAttrs} + elClasses={[ + 'fc-col-header-cell-cushion', + props.isSticky && 'fc-sticky', + ]} + /> + )} + </div> + )} + </ContentContainer> + ) + } +} diff --git a/fullcalendar-main/packages/core/src/common/TableDowCell.tsx b/fullcalendar-main/packages/core/src/common/TableDowCell.tsx new file mode 100644 index 0000000..4999dc8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/TableDowCell.tsx @@ -0,0 +1,84 @@ +import { getDayClassNames, DateMeta } from '../component/date-rendering.js' +import { addDays } from '../datelib/marker.js' +import { createElement } from '../preact.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { BaseComponent } from '../vdom-util.js' +import { Dictionary } from '../options.js' +import { CLASS_NAME, renderInner } from './table-cell-util.js' +import { DayHeaderContentArg } from '../render-hook-misc.js' +import { createFormatter } from '../datelib/formatting.js' +import { ContentContainer } from '../content-inject/ContentContainer.js' + +export interface TableDowCellProps { + dow: number + dayHeaderFormat: DateFormatter + colSpan?: number + isSticky?: boolean // TODO: get this outta here somehow + extraRenderProps?: Dictionary + extraDataAttrs?: Dictionary + extraClassNames?: string[] +} + +const WEEKDAY_FORMAT = createFormatter({ weekday: 'long' }) + +export class TableDowCell extends BaseComponent<TableDowCellProps> { + render() { + let { props } = this + let { dateEnv, theme, viewApi, options } = this.context + let date = addDays(new Date(259200000), props.dow) // start with Sun, 04 Jan 1970 00:00:00 GMT + let dateMeta: DateMeta = { + dow: props.dow, + isDisabled: false, + isFuture: false, + isPast: false, + isToday: false, + isOther: false, + } + let text = dateEnv.format(date, props.dayHeaderFormat) + let renderProps: DayHeaderContentArg = { // TODO: make this public? + date, + ...dateMeta, + view: viewApi, + ...props.extraRenderProps, + text, + } + + return ( + <ContentContainer + elTag="th" + elClasses={[ + CLASS_NAME, + ...getDayClassNames(dateMeta, theme), + ...(props.extraClassNames || []), + ]} + elAttrs={{ + role: 'columnheader', + colSpan: props.colSpan, + ...props.extraDataAttrs, + }} + renderProps={renderProps} + generatorName="dayHeaderContent" + customGenerator={options.dayHeaderContent} + defaultGenerator={renderInner} + classNameGenerator={options.dayHeaderClassNames} + didMount={options.dayHeaderDidMount} + willUnmount={options.dayHeaderWillUnmount} + > + {(InnerContent) => ( + <div className="fc-scrollgrid-sync-inner"> + <InnerContent + elTag="a" + elClasses={[ + 'fc-col-header-cell-cushion', + props.isSticky && 'fc-sticky', + ]} + elAttrs={{ + 'aria-label': dateEnv.format(date, WEEKDAY_FORMAT), + }} + /> + </div> + )} + </ContentContainer> + ) + } +} diff --git a/fullcalendar-main/packages/core/src/common/ViewContainer.tsx b/fullcalendar-main/packages/core/src/common/ViewContainer.tsx new file mode 100644 index 0000000..736ad0b --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/ViewContainer.tsx @@ -0,0 +1,51 @@ +import { ViewSpec } from '../structs/view-spec.js' +import { MountArg } from './render-hook.js' +import { ComponentChildren, createElement } from '../preact.js' +import { BaseComponent } from '../vdom-util.js' +import { ViewApi } from '../api/ViewApi.js' +import { ContentContainer } from '../content-inject/ContentContainer.js' +import { ElProps } from '../content-inject/ContentInjector.js' + +export interface ViewContainerProps extends Partial<ElProps> { + viewSpec: ViewSpec + children: ComponentChildren +} + +export interface ViewContentArg { + view: ViewApi +} + +export type ViewMountArg = MountArg<ViewContentArg> + +export class ViewContainer extends BaseComponent<ViewContainerProps> { + render() { + let { props, context } = this + let { options } = context + let renderProps: ViewContentArg = { view: context.viewApi } + + return ( + <ContentContainer + {...props} + elTag={props.elTag || 'div'} + elClasses={[ + ...buildViewClassNames(props.viewSpec), + ...(props.elClasses || []), + ]} + renderProps={renderProps} + classNameGenerator={options.viewClassNames} + generatorName={undefined} + didMount={options.viewDidMount} + willUnmount={options.viewWillUnmount} + > + {() => props.children} + </ContentContainer> + ) + } +} + +export function buildViewClassNames(viewSpec: ViewSpec): string[] { + return [ + `fc-${viewSpec.type}-view`, + 'fc-view', + ] +} diff --git a/fullcalendar-main/packages/core/src/common/WeekNumberContainer.tsx b/fullcalendar-main/packages/core/src/common/WeekNumberContainer.tsx new file mode 100644 index 0000000..16d17be --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/WeekNumberContainer.tsx @@ -0,0 +1,51 @@ +import { ViewContext, ViewContextType } from '../ViewContext.js' +import { DateMarker } from '../datelib/marker.js' +import { MountArg } from './render-hook.js' +import { createElement } from '../preact.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { ElProps } from '../content-inject/ContentInjector.js' +import { ContentContainer, InnerContainerFunc } from '../content-inject/ContentContainer.js' + +export interface WeekNumberContainerProps extends ElProps { + date: DateMarker + defaultFormat: DateFormatter + children?: InnerContainerFunc<WeekNumberContentArg> +} + +export interface WeekNumberContentArg { + num: number + text: string + date: Date +} + +export type WeekNumberMountArg = MountArg<WeekNumberContentArg> + +export const WeekNumberContainer = (props: WeekNumberContainerProps) => ( + <ViewContextType.Consumer> + {(context: ViewContext) => { + let { dateEnv, options } = context + let { date } = props + let format = options.weekNumberFormat || props.defaultFormat + let num = dateEnv.computeWeekNumber(date) // TODO: somehow use for formatting as well? + let text = dateEnv.format(date, format) + let renderProps: WeekNumberContentArg = { num, text, date } + + return ( + <ContentContainer // why isn't WeekNumberContentArg being auto-detected? + {...props /* includes children */} + renderProps={renderProps} + generatorName="weekNumberContent" + customGenerator={options.weekNumberContent} + defaultGenerator={renderInner} + classNameGenerator={options.weekNumberClassNames} + didMount={options.weekNumberDidMount} + willUnmount={options.weekNumberWillUnmount} + /> + ) + }} + </ViewContextType.Consumer> +) + +function renderInner(innerProps) { + return innerProps.text +} diff --git a/fullcalendar-main/packages/core/src/common/bg-fill.tsx b/fullcalendar-main/packages/core/src/common/bg-fill.tsx new file mode 100644 index 0000000..f9c0b68 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/bg-fill.tsx @@ -0,0 +1,53 @@ +import { createElement } from '../preact.js' +import { BaseComponent } from '../vdom-util.js' +import { Seg } from '../component/DateComponent.js' +import { EventContentArg } from '../component/event-rendering.js' +import { EventContainer } from './EventContainer.js' + +export interface BgEventProps { + seg: Seg + isPast: boolean + isFuture: boolean + isToday: boolean +} + +export class BgEvent extends BaseComponent<BgEventProps> { + render() { + let { props } = this + let { seg } = props + + return ( + <EventContainer + elTag="div" + elClasses={['fc-bg-event']} + elStyle={{ backgroundColor: seg.eventRange.ui.backgroundColor }} + defaultGenerator={renderInnerContent} + seg={seg} + timeText="" + isDragging={false} + isResizing={false} + isDateSelecting={false} + isSelected={false} + isPast={props.isPast} + isFuture={props.isFuture} + isToday={props.isToday} + disableDragging={true} + disableResizing={true} + /> + ) + } +} + +function renderInnerContent(props: EventContentArg) { + let { title } = props.event + + return title && ( + <div className="fc-event-title">{props.event.title}</div> + ) +} + +export function renderFill(fillType: string) { + return ( + <div className={`fc-${fillType}`} /> + ) +} diff --git a/fullcalendar-main/packages/core/src/common/more-link-public-types.ts b/fullcalendar-main/packages/core/src/common/more-link-public-types.ts new file mode 100644 index 0000000..a27ac95 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/more-link-public-types.ts @@ -0,0 +1,24 @@ +import { EventApi } from '../api/EventApi.js' +import { ViewApi } from '../api/ViewApi.js' + +export interface EventSegment { + event: EventApi + start: Date + end: Date + isStart: boolean + isEnd: boolean +} + +export type MoreLinkAction = MoreLinkSimpleAction | MoreLinkHandler +export type MoreLinkSimpleAction = 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string + +export interface MoreLinkArg { + date: Date + allDay: boolean + allSegs: EventSegment[] + hiddenSegs: EventSegment[] + jsEvent: UIEvent + view: ViewApi +} + +export type MoreLinkHandler = (arg: MoreLinkArg) => MoreLinkSimpleAction | void diff --git a/fullcalendar-main/packages/core/src/common/nav-link.ts b/fullcalendar-main/packages/core/src/common/nav-link.ts new file mode 100644 index 0000000..b566960 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/nav-link.ts @@ -0,0 +1,48 @@ +import { createFormatter } from '../datelib/formatting.js' +import { DateMarker } from '../datelib/marker.js' +import { createAriaClickAttrs } from '../util/dom-event.js' +import { formatWithOrdinals } from '../util/misc.js' +import { ViewContext } from '../ViewContext.js' + +const DAY_FORMAT = createFormatter({ year: 'numeric', month: 'long', day: 'numeric' }) +const WEEK_FORMAT = createFormatter({ week: 'long' }) + +export function buildNavLinkAttrs( + context: ViewContext, + dateMarker: DateMarker, + viewType = 'day', + isTabbable = true, +) { + const { dateEnv, options, calendarApi } = context + let dateStr = dateEnv.format(dateMarker, viewType === 'week' ? WEEK_FORMAT : DAY_FORMAT) + + if (options.navLinks) { + let zonedDate = dateEnv.toDate(dateMarker) + + const handleInteraction = (ev: UIEvent) => { + let customAction = + viewType === 'day' ? options.navLinkDayClick : + viewType === 'week' ? options.navLinkWeekClick : null + + if (typeof customAction === 'function') { + customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev) + } else { + if (typeof customAction === 'string') { + viewType = customAction + } + calendarApi.zoomTo(dateMarker, viewType) + } + } + + return { + title: formatWithOrdinals(options.navLinkHint, [dateStr, zonedDate], dateStr), + 'data-navlink': '', // for legacy selectors. TODO: use className? + ...(isTabbable + ? createAriaClickAttrs(handleInteraction) + : { onClick: handleInteraction } + ), + } + } + + return { 'aria-label': dateStr } +} diff --git a/fullcalendar-main/packages/core/src/common/render-hook.tsx b/fullcalendar-main/packages/core/src/common/render-hook.tsx new file mode 100644 index 0000000..6973852 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/render-hook.tsx @@ -0,0 +1,17 @@ +/* eslint max-classes-per-file: off */ + +import { ComponentChildren } from '../preact.js' +import { ClassNamesInput } from '../util/html.js' + +export type MountArg<ContentArg> = ContentArg & { el: HTMLElement } +export type DidMountHandler<TheMountArg extends { el: HTMLElement }> = (mountArg: TheMountArg) => void +export type WillUnmountHandler<TheMountArg extends { el: HTMLElement }> = (mountArg: TheMountArg) => void + +export interface ObjCustomContent { + html: string + domNodes: any[] +} + +export type CustomContent = ComponentChildren | ObjCustomContent +export type CustomContentGenerator<RenderProps> = CustomContent | ((renderProps: RenderProps, createElement: any) => (CustomContent | true)) +export type ClassNamesGenerator<RenderProps> = ClassNamesInput | ((renderProps: RenderProps) => ClassNamesInput) diff --git a/fullcalendar-main/packages/core/src/common/scroll-controller.ts b/fullcalendar-main/packages/core/src/common/scroll-controller.ts new file mode 100644 index 0000000..dacc5c1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/scroll-controller.ts @@ -0,0 +1,124 @@ +/* eslint max-classes-per-file: "off" */ + +/* +An object for getting/setting scroll-related information for an element. +Internally, this is done very differently for window versus DOM element, +so this object serves as a common interface. +*/ +export abstract class ScrollController { + abstract getScrollTop(): number + abstract getScrollLeft(): number + abstract setScrollTop(top: number): void + abstract setScrollLeft(left: number): void + abstract getClientWidth(): number + abstract getClientHeight(): number + abstract getScrollWidth(): number + abstract getScrollHeight(): number + + getMaxScrollTop() { + return this.getScrollHeight() - this.getClientHeight() + } + + getMaxScrollLeft() { + return this.getScrollWidth() - this.getClientWidth() + } + + canScrollVertically() { + return this.getMaxScrollTop() > 0 + } + + canScrollHorizontally() { + return this.getMaxScrollLeft() > 0 + } + + canScrollUp() { + return this.getScrollTop() > 0 + } + + canScrollDown() { + return this.getScrollTop() < this.getMaxScrollTop() + } + + canScrollLeft() { + return this.getScrollLeft() > 0 + } + + canScrollRight() { + return this.getScrollLeft() < this.getMaxScrollLeft() + } +} + +export class ElementScrollController extends ScrollController { + el: HTMLElement + + constructor(el: HTMLElement) { + super() + this.el = el + } + + getScrollTop() { + return this.el.scrollTop + } + + getScrollLeft() { + return this.el.scrollLeft + } + + setScrollTop(top: number) { + this.el.scrollTop = top + } + + setScrollLeft(left: number) { + this.el.scrollLeft = left + } + + getScrollWidth() { + return this.el.scrollWidth + } + + getScrollHeight() { + return this.el.scrollHeight + } + + getClientHeight() { + return this.el.clientHeight + } + + getClientWidth() { + return this.el.clientWidth + } +} + +export class WindowScrollController extends ScrollController { + getScrollTop() { + return window.pageYOffset + } + + getScrollLeft() { + return window.pageXOffset + } + + setScrollTop(n: number) { + window.scroll(window.pageXOffset, n) + } + + setScrollLeft(n: number) { + window.scroll(n, window.pageYOffset) + } + + getScrollWidth() { + return document.documentElement.scrollWidth + } + + getScrollHeight() { + return document.documentElement.scrollHeight + } + + getClientHeight() { + return document.documentElement.clientHeight + } + + getClientWidth() { + return document.documentElement.clientWidth + } +} diff --git a/fullcalendar-main/packages/core/src/common/slicing-utils.ts b/fullcalendar-main/packages/core/src/common/slicing-utils.ts new file mode 100644 index 0000000..02158cb --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/slicing-utils.ts @@ -0,0 +1,247 @@ +import { DateRange, intersectRanges } from '../datelib/date-range.js' +import { EventStore } from '../structs/event-store.js' +import { EventUiHash } from '../component/event-ui.js' +import { sliceEventStore, EventRenderRange } from '../component/event-rendering.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { Seg, EventSegUiInteractionState } from '../component/DateComponent.js' // TODO: rename EventSegUiInteractionState, move here +import { DateSpan, fabricateEventRange } from '../structs/date-span.js' +import { EventInteractionState } from '../interactions/event-interaction-state.js' +import { Duration } from '../datelib/duration.js' +import { memoize } from '../util/memoize.js' +import { DateMarker, addMs, addDays } from '../datelib/marker.js' +import { CalendarContext } from '../CalendarContext.js' +import { expandRecurring } from '../structs/recurring-event.js' + +export interface SliceableProps { + dateSelection: DateSpan + businessHours: EventStore + eventStore: EventStore + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null + eventSelection: string + eventUiBases: EventUiHash +} + +export interface SlicedProps<SegType extends Seg> { + dateSelectionSegs: SegType[] + businessHourSegs: SegType[] + fgEventSegs: SegType[] + bgEventSegs: SegType[] + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + eventSelection: string +} + +export abstract class Slicer<SegType extends Seg, ExtraArgs extends any[] = []> { + private sliceBusinessHours = memoize(this._sliceBusinessHours) + private sliceDateSelection = memoize(this._sliceDateSpan) + private sliceEventStore = memoize(this._sliceEventStore) + private sliceEventDrag = memoize(this._sliceInteraction) + private sliceEventResize = memoize(this._sliceInteraction) + + abstract sliceRange(dateRange: DateRange, ...extraArgs: ExtraArgs): SegType[] + protected forceDayIfListItem = false // hack + + sliceProps( + props: SliceableProps, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + context: CalendarContext, + ...extraArgs: ExtraArgs + ): SlicedProps<SegType> { + let { eventUiBases } = props + let eventSegs = this.sliceEventStore(props.eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) + + return { + dateSelectionSegs: this.sliceDateSelection(props.dateSelection, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs), + businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, context, ...extraArgs), + fgEventSegs: eventSegs.fg, + bgEventSegs: eventSegs.bg, + eventDrag: this.sliceEventDrag(props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs), + eventResize: this.sliceEventResize(props.eventResize, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs), + eventSelection: props.eventSelection, + } // TODO: give interactionSegs? + } + + sliceNowDate( // does not memoize + date: DateMarker, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + context: CalendarContext, + ...extraArgs: ExtraArgs + ): SegType[] { + return this._sliceDateSpan( + { range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range + dateProfile, + nextDayThreshold, + {}, + context, + ...extraArgs, + ) + } + + private _sliceBusinessHours( + businessHours: EventStore, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + context: CalendarContext, + ...extraArgs: ExtraArgs + ): SegType[] { + if (!businessHours) { + return [] + } + + return this._sliceEventStore( + expandRecurring( + businessHours, + computeActiveRange(dateProfile, Boolean(nextDayThreshold)), + context, + ), + {}, + dateProfile, + nextDayThreshold, + ...extraArgs, + ).bg + } + + private _sliceEventStore( + eventStore: EventStore, + eventUiBases: EventUiHash, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + ...extraArgs: ExtraArgs + ): { bg: SegType[], fg: SegType[] } { + if (eventStore) { + let rangeRes = sliceEventStore( + eventStore, + eventUiBases, + computeActiveRange(dateProfile, Boolean(nextDayThreshold)), + nextDayThreshold, + ) + + return { + bg: this.sliceEventRanges(rangeRes.bg, extraArgs), + fg: this.sliceEventRanges(rangeRes.fg, extraArgs), + } + } + return { bg: [], fg: [] } + } + + private _sliceInteraction( + interaction: EventInteractionState, + eventUiBases: EventUiHash, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + ...extraArgs: ExtraArgs + ): EventSegUiInteractionState { + if (!interaction) { + return null + } + + let rangeRes = sliceEventStore( + interaction.mutatedEvents, + eventUiBases, + computeActiveRange(dateProfile, Boolean(nextDayThreshold)), + nextDayThreshold, + ) + + return { + segs: this.sliceEventRanges(rangeRes.fg, extraArgs), + affectedInstances: interaction.affectedEvents.instances, + isEvent: interaction.isEvent, + } + } + + private _sliceDateSpan( + dateSpan: DateSpan, + dateProfile: DateProfile, + nextDayThreshold: Duration | null, + eventUiBases: EventUiHash, + context: CalendarContext, + ...extraArgs: ExtraArgs + ): SegType[] { + if (!dateSpan) { + return [] + } + + let activeRange = computeActiveRange(dateProfile, Boolean(nextDayThreshold)) + let activeDateSpanRange = intersectRanges(dateSpan.range, activeRange) + + if (activeDateSpanRange) { + dateSpan = { ...dateSpan, range: activeDateSpanRange } + + let eventRange = fabricateEventRange(dateSpan, eventUiBases, context) + let segs = this.sliceRange(dateSpan.range, ...extraArgs) + + for (let seg of segs) { + seg.eventRange = eventRange + } + + return segs + } + + return [] + } + + /* + "complete" seg means it has component and eventRange + */ + private sliceEventRanges( + eventRanges: EventRenderRange[], + extraArgs: ExtraArgs, + ): SegType[] { + let segs: SegType[] = [] + + for (let eventRange of eventRanges) { + segs.push(...this.sliceEventRange(eventRange, extraArgs)) + } + + return segs + } + + /* + "complete" seg means it has component and eventRange + */ + private sliceEventRange( + eventRange: EventRenderRange, + extraArgs: ExtraArgs, + ): SegType[] { + let dateRange = eventRange.range + + // hack to make multi-day events that are being force-displayed as list-items to take up only one day + if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') { + dateRange = { + start: dateRange.start, + end: addDays(dateRange.start, 1), + } + } + + let segs = this.sliceRange(dateRange, ...extraArgs) + + for (let seg of segs) { + seg.eventRange = eventRange + seg.isStart = eventRange.isStart && seg.isStart + seg.isEnd = eventRange.isEnd && seg.isEnd + } + + return segs + } +} + +/* +for incorporating slotMinTime/slotMaxTime if appropriate +TODO: should be part of DateProfile! +TimelineDateProfile already does this btw +*/ +function computeActiveRange(dateProfile: DateProfile, isComponentAllDay: boolean): DateRange { + let range = dateProfile.activeRange + + if (isComponentAllDay) { + return range + } + + return { + start: addMs(range.start, dateProfile.slotMinTime.milliseconds), + end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day + } +} diff --git a/fullcalendar-main/packages/core/src/common/table-cell-util.tsx b/fullcalendar-main/packages/core/src/common/table-cell-util.tsx new file mode 100644 index 0000000..f448582 --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/table-cell-util.tsx @@ -0,0 +1,8 @@ +import { ComponentChild } from '../preact.js' +import { DayHeaderContentArg } from '../render-hook-misc.js' + +export const CLASS_NAME = 'fc-col-header-cell' // do the cushion too? no + +export function renderInner(renderProps: DayHeaderContentArg): ComponentChild { + return renderProps.text +} diff --git a/fullcalendar-main/packages/core/src/common/table-utils.ts b/fullcalendar-main/packages/core/src/common/table-utils.ts new file mode 100644 index 0000000..90389dd --- /dev/null +++ b/fullcalendar-main/packages/core/src/common/table-utils.ts @@ -0,0 +1,17 @@ +import { createFormatter } from '../datelib/formatting.js' +import { DateFormatter } from '../datelib/DateFormatter.js' + +// Computes a default column header formatting string if `colFormat` is not explicitly defined +export function computeFallbackHeaderFormat(datesRepDistinctDays: boolean, dayCnt: number): DateFormatter { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (!datesRepDistinctDays || dayCnt > 10) { + return createFormatter({ weekday: 'short' }) // "Sat" + } + + if (dayCnt > 1) { + return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }) // "Sat 11/12" + } + + return createFormatter({ weekday: 'long' }) // "Saturday" +} diff --git a/fullcalendar-main/packages/core/src/component/CalendarDataProvider.ts b/fullcalendar-main/packages/core/src/component/CalendarDataProvider.ts new file mode 100644 index 0000000..567573e --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/CalendarDataProvider.ts @@ -0,0 +1,46 @@ +import { Component, ComponentChildren } from '../preact.js' +import { CalendarDataManager } from '../reducers/CalendarDataManager.js' +import { CalendarImpl } from '../api/CalendarImpl.js' +import { CalendarData } from '../reducers/data-types.js' + +export interface CalendarDataProviderProps { + optionOverrides: any + calendarApi: CalendarImpl + children?: (data: CalendarData) => ComponentChildren +} + +// TODO: move this to react plugin? +export class CalendarDataProvider extends Component<CalendarDataProviderProps, CalendarData> { + dataManager: CalendarDataManager + + constructor(props: CalendarDataProviderProps) { + super(props) + + this.dataManager = new CalendarDataManager({ + optionOverrides: props.optionOverrides, + calendarApi: props.calendarApi, + onData: this.handleData, + }) + } + + handleData = (data: CalendarData) => { + if (!this.dataManager) { // still within initial run, before assignment in constructor + // eslint-disable-next-line react/no-direct-mutation-state + this.state = data // can't use setState yet + } else { + this.setState(data) + } + } + + render() { + return this.props.children(this.state) + } + + componentDidUpdate(prevProps: CalendarDataProviderProps) { + let newOptionOverrides = this.props.optionOverrides + + if (newOptionOverrides !== prevProps.optionOverrides) { // prevent recursive handleData + this.dataManager.resetOptions(newOptionOverrides) + } + } +} diff --git a/fullcalendar-main/packages/core/src/component/DateComponent.ts b/fullcalendar-main/packages/core/src/component/DateComponent.ts new file mode 100644 index 0000000..508a8b5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/DateComponent.ts @@ -0,0 +1,64 @@ +import { BaseComponent } from '../vdom-util.js' +import { EventRenderRange } from './event-rendering.js' +import { EventInstanceHash } from '../structs/event-instance.js' +import { Hit } from '../interactions/hit.js' +import { elementClosest } from '../util/dom-manip.js' +import { guid } from '../util/misc.js' +import { Dictionary } from '../options.js' + +export type DateComponentHash = { [uid: string]: DateComponent<any, any> } + +// NOTE: for fg-events, eventRange.range is NOT sliced, +// thus, we need isStart/isEnd +export interface Seg { + component?: DateComponent<any, any> + isStart: boolean + isEnd: boolean + eventRange?: EventRenderRange + [otherProp: string]: any // TODO: remove this. extending Seg will handle this + el?: never + // NOTE: can sometimes have start/end, which are important values :( +} + +export interface EventSegUiInteractionState { + affectedInstances: EventInstanceHash + segs: Seg[] + isEvent: boolean +} + +/* +an INTERACTABLE date component + +PURPOSES: +- hook up to fg, fill, and mirror renderers +- interface for dragging and hits +*/ +export abstract class DateComponent<Props=Dictionary, State=Dictionary> extends BaseComponent<Props, State> { + uid = guid() + + // Hit System + // ----------------------------------------------------------------------------------------------------------------- + + prepareHits() { + } + + queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null { + return null // this should be abstract + } + + // Pointer Interaction Utils + // ----------------------------------------------------------------------------------------------------------------- + + isValidSegDownEl(el: HTMLElement) { + return !(this.props as any).eventDrag && // HACK + !(this.props as any).eventResize && // HACK + !elementClosest(el, '.fc-event-mirror') + } + + isValidDateDownEl(el: HTMLElement) { + return !elementClosest(el, '.fc-event:not(.fc-bg-event)') && + !elementClosest(el, '.fc-more-link') && // a "more.." link + !elementClosest(el, 'a[data-navlink]') && // a clickable nav link + !elementClosest(el, '.fc-popover') // hack + } +} diff --git a/fullcalendar-main/packages/core/src/component/date-rendering.ts b/fullcalendar-main/packages/core/src/component/date-rendering.ts new file mode 100644 index 0000000..7cf0fdf --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/date-rendering.ts @@ -0,0 +1,80 @@ +import { DateMarker, DAY_IDS } from '../datelib/marker.js' +import { rangeContainsMarker, DateRange } from '../datelib/date-range.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { Theme } from '../theme/Theme.js' + +export interface DateMeta { + dow: number + isDisabled: boolean + isOther: boolean // like, is it in the non-current "other" month + isToday: boolean + isPast: boolean + isFuture: boolean +} + +export function getDateMeta(date: DateMarker, todayRange?: DateRange, nowDate?: DateMarker, dateProfile?: DateProfile): DateMeta { + return { + dow: date.getUTCDay(), + isDisabled: Boolean(dateProfile && !rangeContainsMarker(dateProfile.activeRange, date)), + isOther: Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, date)), + isToday: Boolean(todayRange && rangeContainsMarker(todayRange, date)), + isPast: Boolean(nowDate ? (date < nowDate) : todayRange ? (date < todayRange.start) : false), + isFuture: Boolean(nowDate ? (date > nowDate) : todayRange ? (date >= todayRange.end) : false), + } +} + +export function getDayClassNames(meta: DateMeta, theme: Theme) { + let classNames: string[] = [ + 'fc-day', + `fc-day-${DAY_IDS[meta.dow]}`, + ] + + if (meta.isDisabled) { + classNames.push('fc-day-disabled') + } else { + if (meta.isToday) { + classNames.push('fc-day-today') + classNames.push(theme.getClass('today')) + } + + if (meta.isPast) { + classNames.push('fc-day-past') + } + + if (meta.isFuture) { + classNames.push('fc-day-future') + } + + if (meta.isOther) { + classNames.push('fc-day-other') + } + } + + return classNames +} + +export function getSlotClassNames(meta: DateMeta, theme: Theme) { + let classNames: string[] = [ + 'fc-slot', + `fc-slot-${DAY_IDS[meta.dow]}`, + ] + + if (meta.isDisabled) { + classNames.push('fc-slot-disabled') + } else { + if (meta.isToday) { + classNames.push('fc-slot-today') + classNames.push(theme.getClass('today')) + } + + if (meta.isPast) { + classNames.push('fc-slot-past') + } + + if (meta.isFuture) { + classNames.push('fc-slot-future') + } + } + + return classNames +} diff --git a/fullcalendar-main/packages/core/src/component/event-rendering.ts b/fullcalendar-main/packages/core/src/component/event-rendering.ts new file mode 100644 index 0000000..9ac0dc9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/event-rendering.ts @@ -0,0 +1,375 @@ +import { EventDef, EventDefHash } from '../structs/event-def.js' +import { EventTuple } from '../structs/event-parse.js' +import { EventStore } from '../structs/event-store.js' +import { DateRange, invertRanges, intersectRanges, rangeContainsMarker } from '../datelib/date-range.js' +import { Duration } from '../datelib/duration.js' +import { compareByFieldSpecs, OrderSpec } from '../util/misc.js' +import { computeVisibleDayRange } from '../util/date.js' +import { Seg } from './DateComponent.js' +import { EventImpl } from '../api/EventImpl.js' +import { EventUi, EventUiHash, combineEventUis } from './event-ui.js' +import { mapHash } from '../util/object.js' +import { ViewContext } from '../ViewContext.js' +import { DateFormatter } from '../datelib/DateFormatter.js' +import { addMs, DateMarker, startOfDay } from '../datelib/marker.js' +import { ViewApi } from '../api/ViewApi.js' +import { MountArg } from '../common/render-hook.js' +import { createAriaKeyboardAttrs } from '../util/dom-event.js' + +export interface EventRenderRange extends EventTuple { + ui: EventUi + range: DateRange + isStart: boolean + isEnd: boolean +} + +/* +Specifying nextDayThreshold signals that all-day ranges should be sliced. +*/ +export function sliceEventStore(eventStore: EventStore, eventUiBases: EventUiHash, framingRange: DateRange, nextDayThreshold?: Duration) { + let inverseBgByGroupId: { [groupId: string]: DateRange[] } = {} + let inverseBgByDefId: { [defId: string]: DateRange[] } = {} + let defByGroupId: { [groupId: string]: EventDef } = {} + let bgRanges: EventRenderRange[] = [] + let fgRanges: EventRenderRange[] = [] + let eventUis = compileEventUis(eventStore.defs, eventUiBases) + + for (let defId in eventStore.defs) { + let def = eventStore.defs[defId] + let ui = eventUis[def.defId] + + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId] = [] + + if (!defByGroupId[def.groupId]) { + defByGroupId[def.groupId] = def + } + } else { + inverseBgByDefId[defId] = [] + } + } + } + + for (let instanceId in eventStore.instances) { + let instance = eventStore.instances[instanceId] + let def = eventStore.defs[instance.defId] + let ui = eventUis[def.defId] + let origRange = instance.range + + let normalRange = (!def.allDay && nextDayThreshold) ? + computeVisibleDayRange(origRange, nextDayThreshold) : + origRange + + let slicedRange = intersectRanges(normalRange, framingRange) + + if (slicedRange) { + if (ui.display === 'inverse-background') { + if (def.groupId) { + inverseBgByGroupId[def.groupId].push(slicedRange) + } else { + inverseBgByDefId[instance.defId].push(slicedRange) + } + } else if (ui.display !== 'none') { + (ui.display === 'background' ? bgRanges : fgRanges).push({ + def, + ui, + instance, + range: slicedRange, + isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(), + isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf(), + }) + } + } + } + + for (let groupId in inverseBgByGroupId) { // BY GROUP + let ranges = inverseBgByGroupId[groupId] + let invertedRanges = invertRanges(ranges, framingRange) + + for (let invertedRange of invertedRanges) { + let def = defByGroupId[groupId] + let ui = eventUis[def.defId] + + bgRanges.push({ + def, + ui, + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }) + } + } + + for (let defId in inverseBgByDefId) { + let ranges = inverseBgByDefId[defId] + let invertedRanges = invertRanges(ranges, framingRange) + + for (let invertedRange of invertedRanges) { + bgRanges.push({ + def: eventStore.defs[defId], + ui: eventUis[defId], + instance: null, + range: invertedRange, + isStart: false, + isEnd: false, + }) + } + } + + return { bg: bgRanges, fg: fgRanges } +} + +export function hasBgRendering(def: EventDef) { + return def.ui.display === 'background' || def.ui.display === 'inverse-background' +} + +export function setElSeg(el: HTMLElement, seg: Seg) { + (el as any).fcSeg = seg +} + +export function getElSeg(el: HTMLElement): Seg | null { + return (el as any).fcSeg || + (el.parentNode as any).fcSeg || // for the harness + null +} + +// event ui computation + +export function compileEventUis(eventDefs: EventDefHash, eventUiBases: EventUiHash) { + return mapHash(eventDefs, (eventDef: EventDef) => compileEventUi(eventDef, eventUiBases)) +} + +export function compileEventUi(eventDef: EventDef, eventUiBases: EventUiHash) { + let uis = [] + + if (eventUiBases['']) { + uis.push(eventUiBases['']) + } + + if (eventUiBases[eventDef.defId]) { + uis.push(eventUiBases[eventDef.defId]) + } + + uis.push(eventDef.ui) + + return combineEventUis(uis) +} + +export function sortEventSegs(segs, eventOrderSpecs: OrderSpec<EventImpl>[]): Seg[] { + let objs = segs.map(buildSegCompareObj) + + objs.sort((obj0, obj1) => compareByFieldSpecs(obj0, obj1, eventOrderSpecs)) + + return objs.map((c) => c._seg) +} + +// returns a object with all primitive props that can be compared +export function buildSegCompareObj(seg: Seg) { + let { eventRange } = seg + let eventDef = eventRange.def + let range = eventRange.instance ? eventRange.instance.range : eventRange.range + let start = range.start ? range.start.valueOf() : 0 // TODO: better support for open-range events + let end = range.end ? range.end.valueOf() : 0 // " + + return { + ...eventDef.extendedProps, + ...eventDef, + id: eventDef.publicId, + start, + end, + duration: end - start, + allDay: Number(eventDef.allDay), + _seg: seg, // for later retrieval + } +} + +// other stuff + +export interface EventContentArg { // for *Content handlers + event: EventImpl + timeText: string + backgroundColor: string // TODO: add other EventUi props? + borderColor: string // + textColor: string // + isDraggable: boolean + isStartResizable: boolean + isEndResizable: boolean + isMirror: boolean + isStart: boolean + isEnd: boolean + isPast: boolean + isFuture: boolean + isToday: boolean + isSelected: boolean + isDragging: boolean + isResizing: boolean + view: ViewApi // specifically for the API +} + +export type EventMountArg = MountArg<EventContentArg> + +export function computeSegDraggable(seg: Seg, context: ViewContext) { + let { pluginHooks } = context + let transformers = pluginHooks.isDraggableTransformers + let { def, ui } = seg.eventRange + let val = ui.startEditable + + for (let transformer of transformers) { + val = transformer(val, def, ui, context) + } + + return val +} + +export function computeSegStartResizable(seg: Seg, context: ViewContext) { + return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart +} + +export function computeSegEndResizable(seg: Seg, context: ViewContext) { + return seg.isEnd && seg.eventRange.ui.durationEditable +} + +export function buildSegTimeText( + seg: Seg, + timeFormat: DateFormatter, + context: ViewContext, + defaultDisplayEventTime?: boolean, // defaults to true + defaultDisplayEventEnd?: boolean, // defaults to true + startOverride?: DateMarker, + endOverride?: DateMarker, +) { + let { dateEnv, options } = context + let { displayEventTime, displayEventEnd } = options + let eventDef = seg.eventRange.def + let eventInstance = seg.eventRange.instance + + if (displayEventTime == null) { displayEventTime = defaultDisplayEventTime !== false } + if (displayEventEnd == null) { displayEventEnd = defaultDisplayEventEnd !== false } + + let wholeEventStart = eventInstance.range.start + let wholeEventEnd = eventInstance.range.end + let segStart = startOverride || seg.start || seg.eventRange.range.start + let segEnd = endOverride || seg.end || seg.eventRange.range.end + let isStartDay = startOfDay(wholeEventStart).valueOf() === startOfDay(segStart).valueOf() + let isEndDay = startOfDay(addMs(wholeEventEnd, -1)).valueOf() === startOfDay(addMs(segEnd, -1)).valueOf() + + if (displayEventTime && !eventDef.allDay && (isStartDay || isEndDay)) { + segStart = isStartDay ? wholeEventStart : segStart + segEnd = isEndDay ? wholeEventEnd : segEnd + + if (displayEventEnd && eventDef.hasEnd) { + return dateEnv.formatRange(segStart, segEnd, timeFormat, { + forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooooooooooo, give tzo if same date + forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo, + }) + } + return dateEnv.format(segStart, timeFormat, { + forcedTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooo, same + }) + } + + return '' +} + +export function getSegMeta(seg: Seg, todayRange: DateRange, nowDate?: DateMarker) { // TODO: make arg order consistent with date util + let segRange = seg.eventRange.range + + return { + isPast: segRange.end <= (nowDate || todayRange.start), + isFuture: segRange.start >= (nowDate || todayRange.end), + isToday: todayRange && rangeContainsMarker(todayRange, segRange.start), + } +} + +export function getEventClassNames(props: EventContentArg) { // weird that we use this interface, but convenient + let classNames: string[] = ['fc-event'] + + if (props.isMirror) { + classNames.push('fc-event-mirror') + } + + if (props.isDraggable) { + classNames.push('fc-event-draggable') + } + + if (props.isStartResizable || props.isEndResizable) { + classNames.push('fc-event-resizable') + } + + if (props.isDragging) { + classNames.push('fc-event-dragging') + } + + if (props.isResizing) { + classNames.push('fc-event-resizing') + } + + if (props.isSelected) { + classNames.push('fc-event-selected') + } + + if (props.isStart) { + classNames.push('fc-event-start') + } + + if (props.isEnd) { + classNames.push('fc-event-end') + } + + if (props.isPast) { + classNames.push('fc-event-past') + } + + if (props.isToday) { + classNames.push('fc-event-today') + } + + if (props.isFuture) { + classNames.push('fc-event-future') + } + + return classNames +} + +export function buildEventRangeKey(eventRange: EventRenderRange) { + return eventRange.instance + ? eventRange.instance.instanceId + : `${eventRange.def.defId}:${eventRange.range.start.toISOString()}` + // inverse-background events don't have specific instances. TODO: better solution +} + +export function getSegAnchorAttrs(seg: Seg, context: ViewContext) { + let { def, instance } = seg.eventRange + let { url } = def + + if (url) { + return { href: url } + } + + let { emitter, options } = context + let { eventInteractive } = options + + if (eventInteractive == null) { + eventInteractive = def.interactive + if (eventInteractive == null) { + eventInteractive = Boolean(emitter.hasHandlers('eventClick')) + } + } + + // mock what happens in EventClicking + if (eventInteractive) { + // only attach keyboard-related handlers because click handler is already done in EventClicking + return createAriaKeyboardAttrs((ev: UIEvent) => { + emitter.trigger('eventClick', { + el: ev.target as HTMLElement, + event: new EventImpl(context, def, instance), + jsEvent: ev as MouseEvent, + view: context.viewApi, + }) + }) + } + + return {} +} diff --git a/fullcalendar-main/packages/core/src/component/event-splitting.ts b/fullcalendar-main/packages/core/src/component/event-splitting.ts new file mode 100644 index 0000000..cb17eea --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/event-splitting.ts @@ -0,0 +1,184 @@ +import { EventStore, createEmptyEventStore } from '../structs/event-store.js' +import { EventDef } from '../structs/event-def.js' +import { EventInteractionState } from '../interactions/event-interaction-state.js' +import { mapHash } from '../util/object.js' +import { memoize } from '../util/memoize.js' +import { EventUiHash, EventUi, combineEventUis } from './event-ui.js' +import { DateSpan } from '../structs/date-span.js' + +export interface SplittableProps { + businessHours: EventStore | null // is this really allowed to be null? + dateSelection: DateSpan | null + eventStore: EventStore + eventUiBases: EventUiHash + eventSelection: string + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null +} + +const EMPTY_EVENT_STORE = createEmptyEventStore() // for purecomponents. TODO: keep elsewhere + +export abstract class Splitter<PropsType extends SplittableProps = SplittableProps> { + private getKeysForEventDefs = memoize(this._getKeysForEventDefs) + private splitDateSelection = memoize(this._splitDateSpan) + private splitEventStore = memoize(this._splitEventStore) + private splitIndividualUi = memoize(this._splitIndividualUi) + private splitEventDrag = memoize(this._splitInteraction) + private splitEventResize = memoize(this._splitInteraction) + private eventUiBuilders = {} // TODO: typescript protection + + abstract getKeyInfo(props: PropsType): { [key: string]: { ui?: EventUi, businessHours?: EventStore } } + abstract getKeysForDateSpan(dateSpan: DateSpan): string[] + abstract getKeysForEventDef(eventDef: EventDef): string[] + + splitProps(props: PropsType): { [key: string]: SplittableProps } { + let keyInfos = this.getKeyInfo(props) + let defKeys = this.getKeysForEventDefs(props.eventStore) + let dateSelections = this.splitDateSelection(props.dateSelection) + let individualUi = this.splitIndividualUi(props.eventUiBases, defKeys) // the individual *bases* + let eventStores = this.splitEventStore(props.eventStore, defKeys) + let eventDrags = this.splitEventDrag(props.eventDrag) + let eventResizes = this.splitEventResize(props.eventResize) + let splitProps: { [key: string]: SplittableProps } = {} + + this.eventUiBuilders = mapHash(keyInfos, (info, key) => this.eventUiBuilders[key] || memoize(buildEventUiForKey)) + + for (let key in keyInfos) { + let keyInfo = keyInfos[key] + let eventStore = eventStores[key] || EMPTY_EVENT_STORE + let buildEventUi = this.eventUiBuilders[key] + + splitProps[key] = { + businessHours: keyInfo.businessHours || props.businessHours, + dateSelection: dateSelections[key] || null, + eventStore, + eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]), + eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '', + eventDrag: eventDrags[key] || null, + eventResize: eventResizes[key] || null, + } + } + + return splitProps + } + + private _splitDateSpan(dateSpan: DateSpan | null) { + let dateSpans = {} + + if (dateSpan) { + let keys = this.getKeysForDateSpan(dateSpan) + + for (let key of keys) { + dateSpans[key] = dateSpan + } + } + + return dateSpans + } + + private _getKeysForEventDefs(eventStore: EventStore) { + return mapHash(eventStore.defs, (eventDef: EventDef) => this.getKeysForEventDef(eventDef)) + } + + private _splitEventStore(eventStore: EventStore, defKeys): { [key: string]: EventStore } { + let { defs, instances } = eventStore + let splitStores = {} + + for (let defId in defs) { + for (let key of defKeys[defId]) { + if (!splitStores[key]) { + splitStores[key] = createEmptyEventStore() + } + + splitStores[key].defs[defId] = defs[defId] + } + } + + for (let instanceId in instances) { + let instance = instances[instanceId] + + for (let key of defKeys[instance.defId]) { + if (splitStores[key]) { // must have already been created + splitStores[key].instances[instanceId] = instance + } + } + } + + return splitStores + } + + private _splitIndividualUi(eventUiBases: EventUiHash, defKeys): { [key: string]: EventUiHash } { + let splitHashes: { [key: string]: EventUiHash } = {} + + for (let defId in eventUiBases) { + if (defId) { // not the '' key + for (let key of defKeys[defId]) { + if (!splitHashes[key]) { + splitHashes[key] = {} + } + + splitHashes[key][defId] = eventUiBases[defId] + } + } + } + + return splitHashes + } + + private _splitInteraction(interaction: EventInteractionState | null): { [key: string]: EventInteractionState } { + let splitStates: { [key: string]: EventInteractionState } = {} + + if (interaction) { + let affectedStores = this._splitEventStore( + interaction.affectedEvents, + this._getKeysForEventDefs(interaction.affectedEvents), // can't use cached. might be events from other calendar + ) + + // can't rely on defKeys because event data is mutated + let mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents) + let mutatedStores = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId) + + let populate = (key) => { + if (!splitStates[key]) { + splitStates[key] = { + affectedEvents: affectedStores[key] || EMPTY_EVENT_STORE, + mutatedEvents: mutatedStores[key] || EMPTY_EVENT_STORE, + isEvent: interaction.isEvent, + } + } + } + + for (let key in affectedStores) { + populate(key) + } + + for (let key in mutatedStores) { + populate(key) + } + } + + return splitStates + } +} + +function buildEventUiForKey(allUi: EventUi | null, eventUiForKey: EventUi | null, individualUi: EventUiHash | null) { + let baseParts = [] + + if (allUi) { + baseParts.push(allUi) + } + + if (eventUiForKey) { + baseParts.push(eventUiForKey) + } + + let stuff = { + '': combineEventUis(baseParts), + } + + if (individualUi) { + Object.assign(stuff, individualUi) + } + + return stuff +} diff --git a/fullcalendar-main/packages/core/src/component/event-ui.ts b/fullcalendar-main/packages/core/src/component/event-ui.ts new file mode 100644 index 0000000..0ae99a1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/component/event-ui.ts @@ -0,0 +1,98 @@ +import { Constraint, AllowFunc, normalizeConstraint } from '../structs/constraint.js' +import { parseClassNames } from '../util/html.js' +import { CalendarContext } from '../CalendarContext.js' +import { RawOptionsFromRefiners, RefinedOptionsFromRefiners, identity, Identity } from '../options.js' + +// TODO: better called "EventSettings" or "EventConfig" +// TODO: move this file into structs +// TODO: separate constraint/overlap/allow, because selection uses only that, not other props + +export const EVENT_UI_REFINERS = { + display: String, + editable: Boolean, + startEditable: Boolean, + durationEditable: Boolean, + constraint: identity as Identity<any>, // Identity<ConstraintInput>, // circular reference. ts dies. event->constraint->event + overlap: identity as Identity<boolean>, + allow: identity as Identity<AllowFunc>, + className: parseClassNames, // will both end up as array of strings + classNames: parseClassNames, // " + color: String, + backgroundColor: String, + borderColor: String, + textColor: String, +} + +const EMPTY_EVENT_UI: EventUi = { + display: null, + startEditable: null, + durationEditable: null, + constraints: [], + overlap: null, + allows: [], + backgroundColor: '', + borderColor: '', + textColor: '', + classNames: [], +} + +type BuiltInEventUiRefiners = typeof EVENT_UI_REFINERS + +interface EventUiRefiners extends BuiltInEventUiRefiners { + // to prevent circular reference (and give is the option for ambient modification for later) +} + +export type EventUiInput = RawOptionsFromRefiners<Required<EventUiRefiners>> // Required hack +export type EventUiRefined = RefinedOptionsFromRefiners<Required<EventUiRefiners>> // Required hack + +export interface EventUi { + display: string | null + startEditable: boolean | null + durationEditable: boolean | null + constraints: Constraint[] + overlap: boolean | null + allows: AllowFunc[] // crappy name to indicate plural + backgroundColor: string + borderColor: string + textColor: string, + classNames: string[] +} + +export type EventUiHash = { [defId: string]: EventUi } + +export function createEventUi(refined: EventUiRefined, context: CalendarContext): EventUi { + let constraint = normalizeConstraint(refined.constraint, context) + + return { + display: refined.display || null, + startEditable: refined.startEditable != null ? refined.startEditable : refined.editable, + durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable, + constraints: constraint != null ? [constraint] : [], + overlap: refined.overlap != null ? refined.overlap : null, + allows: refined.allow != null ? [refined.allow] : [], + backgroundColor: refined.backgroundColor || refined.color || '', + borderColor: refined.borderColor || refined.color || '', + textColor: refined.textColor || '', + classNames: (refined.className || []).concat(refined.classNames || []), // join singular and plural + } +} + +// TODO: prevent against problems with <2 args! +export function combineEventUis(uis: EventUi[]): EventUi { + return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI) +} + +function combineTwoEventUis(item0: EventUi, item1: EventUi): EventUi { // hash1 has higher precedence + return { + display: item1.display != null ? item1.display : item0.display, + startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable, + durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable, + constraints: item0.constraints.concat(item1.constraints), + overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap, + allows: item0.allows.concat(item1.allows), + backgroundColor: item1.backgroundColor || item0.backgroundColor, + borderColor: item1.borderColor || item0.borderColor, + textColor: item1.textColor || item0.textColor, + classNames: item0.classNames.concat(item1.classNames), + } +} diff --git a/fullcalendar-main/packages/core/src/content-inject/ContentContainer.ts b/fullcalendar-main/packages/core/src/content-inject/ContentContainer.ts new file mode 100644 index 0000000..ffcb169 --- /dev/null +++ b/fullcalendar-main/packages/core/src/content-inject/ContentContainer.ts @@ -0,0 +1,126 @@ +import { createElement, Component, FunctionalComponent, ComponentChildren } from '../preact.js' +import { ClassNamesGenerator } from '../common/render-hook.js' +import { + ContentInjector, + ContentGeneratorProps, + ElAttrsProps, + buildElAttrs, + ElProps, + ElAttrs, +} from './ContentInjector.js' +import { RenderId } from './RenderId.js' +import { setRef } from '../vdom-util.js' + +/* +The `children` prop is a function that defines inner wrappers (ex: ResourceCell) +*/ +export type ContentContainerProps<RenderProps> = + ElAttrsProps & + ContentGeneratorProps<RenderProps> & { + elTag?: string + classNameGenerator: ClassNamesGenerator<RenderProps> | undefined + didMount: ((renderProps: RenderProps & { el: HTMLElement }) => void) | undefined + willUnmount: ((renderProps: RenderProps & { el: HTMLElement }) => void) | undefined + children?: InnerContainerFunc<RenderProps> + } + +export class ContentContainer<RenderProps> extends Component<ContentContainerProps<RenderProps>> { + static contextType = RenderId + didMountMisfire?: boolean + context: number + el: HTMLElement + + InnerContent = InnerContentInjector.bind(undefined, this) + + render() { + const { props } = this + const generatedClassNames = generateClassNames(props.classNameGenerator, props.renderProps) + + if (props.children) { + const elAttrs = buildElAttrs(props, generatedClassNames, this.handleEl) + const children = props.children(this.InnerContent, props.renderProps, elAttrs) + + if (props.elTag) { + return createElement(props.elTag, elAttrs, children) + } else { + return children + } + } else { + return createElement(ContentInjector<RenderProps>, { + ...props, + elRef: this.handleEl, + elTag: props.elTag || 'div', + elClasses: (props.elClasses || []).concat(generatedClassNames), + renderId: this.context, + }) + } + } + + handleEl = (el: HTMLElement) => { + this.el = el + + if (this.props.elRef) { + setRef(this.props.elRef, el) + + if (el && this.didMountMisfire) { + this.componentDidMount() + } + } + } + + componentDidMount(): void { + if (this.el) { + this.props.didMount?.({ + ...this.props.renderProps, + el: this.el, + }) + } else { + this.didMountMisfire = true + } + } + + componentWillUnmount(): void { + this.props.willUnmount?.({ + ...this.props.renderProps, + el: this.el, + }) + } +} + +// Inner + +export type InnerContainerComponent = FunctionalComponent<ElProps> +export type InnerContainerFunc<RenderProps> = ( + InnerContainer: InnerContainerComponent, + renderProps: RenderProps, + elAttrs: ElAttrs, +) => ComponentChildren + +function InnerContentInjector<RenderProps>( + containerComponent: ContentContainer<RenderProps>, + props: ElProps, +) { + const parentProps = containerComponent.props + + return createElement(ContentInjector<RenderProps>, { + renderProps: parentProps.renderProps, + generatorName: parentProps.generatorName, + customGenerator: parentProps.customGenerator, + defaultGenerator: parentProps.defaultGenerator, + renderId: containerComponent.context, + ...props, + }) +} + +// Utils + +function generateClassNames<RenderProps>( + classNameGenerator: ClassNamesGenerator<RenderProps> | undefined, + renderProps: RenderProps, +): string[] { + const classNames = typeof classNameGenerator === 'function' ? + classNameGenerator(renderProps) : + classNameGenerator || [] + + return typeof classNames === 'string' ? [classNames] : classNames +} diff --git a/fullcalendar-main/packages/core/src/content-inject/ContentInjector.ts b/fullcalendar-main/packages/core/src/content-inject/ContentInjector.ts new file mode 100644 index 0000000..a552119 --- /dev/null +++ b/fullcalendar-main/packages/core/src/content-inject/ContentInjector.ts @@ -0,0 +1,209 @@ +import { createElement, ComponentChild, JSX, Ref, isValidElement } from '../preact.js' +import { CustomContentGenerator } from '../common/render-hook.js' +import { BaseComponent, setRef } from '../vdom-util.js' +import { guid } from '../util/misc.js' +import { isArraysEqual } from '../util/array.js' +import { removeElement } from '../util/dom-manip.js' +import { ViewOptions } from '../options.js' +import { isNonHandlerPropsEqual, isPropsEqual } from '../util/object.js' + +export type ElRef = Ref<HTMLElement> +export type ElAttrs = JSX.HTMLAttributes & JSX.SVGAttributes & { ref?: ElRef } & Record<string, any> + +export interface ElAttrsProps { + elRef?: ElRef + elClasses?: string[] + elStyle?: JSX.CSSProperties + elAttrs?: ElAttrs +} + +export interface ElProps extends ElAttrsProps { + elTag: string +} + +export interface ContentGeneratorProps<RenderProps> { + renderProps: RenderProps + generatorName: string | undefined // for informing UI-framework if `customGenerator` is undefined + customGenerator?: CustomContentGenerator<RenderProps> + defaultGenerator?: (renderProps: RenderProps) => ComponentChild +} + +export type ContentInjectorProps<RenderProps> = + ElProps & + ContentGeneratorProps<RenderProps> & + { renderId: number } + +export class ContentInjector<RenderProps> extends BaseComponent<ContentInjectorProps<RenderProps>> { + private id = guid() + private queuedDomNodes: Node[] = [] + private currentDomNodes: Node[] = [] + private currentGeneratorMeta: any + + render() { + const { props, context } = this + const { options } = context + const { customGenerator, defaultGenerator, renderProps } = props + const attrs = buildElAttrs(props, [], this.handleEl) + let useDefault = false + let innerContent: ComponentChild | undefined + let queuedDomNodes: Node[] = [] + let currentGeneratorMeta: any + + if (customGenerator != null) { + const customGeneratorRes = typeof customGenerator === 'function' ? + customGenerator(renderProps, createElement) : + customGenerator + + if (customGeneratorRes === true) { + useDefault = true + } else { + const isObject = customGeneratorRes && typeof customGeneratorRes === 'object' // non-null + + if (isObject && ('html' in customGeneratorRes)) { + attrs.dangerouslySetInnerHTML = { __html: customGeneratorRes.html } + } else if (isObject && ('domNodes' in customGeneratorRes)) { + queuedDomNodes = Array.prototype.slice.call(customGeneratorRes.domNodes) + } else if ( + isObject + ? isValidElement(customGeneratorRes) // vdom node + : typeof customGeneratorRes !== 'function' // primitive value (like string or number) + ) { + // use in vdom + innerContent = customGeneratorRes + } else { + // an exotic object for handleCustomRendering + currentGeneratorMeta = customGeneratorRes + } + } + } else { + useDefault = !hasCustomRenderingHandler(props.generatorName, options) + } + + if (useDefault && defaultGenerator) { + innerContent = defaultGenerator(renderProps) + } + + this.queuedDomNodes = queuedDomNodes + this.currentGeneratorMeta = currentGeneratorMeta + + return createElement(props.elTag, attrs, innerContent) + } + + componentDidMount(): void { + this.applyQueueudDomNodes() + this.triggerCustomRendering(true) + } + + componentDidUpdate(): void { + this.applyQueueudDomNodes() + this.triggerCustomRendering(true) + } + + componentWillUnmount(): void { + this.triggerCustomRendering(false) // TODO: different API for removal? + } + + private triggerCustomRendering(isActive: boolean) { + const { props, context } = this + const { handleCustomRendering, customRenderingMetaMap } = context.options + + if (handleCustomRendering) { + const generatorMeta = + this.currentGeneratorMeta ?? + customRenderingMetaMap?.[props.generatorName] + + if (generatorMeta) { + handleCustomRendering({ + id: this.id, + isActive, + containerEl: this.base as HTMLElement, + reportNewContainerEl: this.updateElRef, // front-end framework tells us about new container els + generatorMeta, + ...props, + elClasses: (props.elClasses || []).filter(isTruthy), + }) + } + } + } + + private handleEl = (el: HTMLElement | null) => { + const { options } = this.context + const { generatorName } = this.props + + if (!options.customRenderingReplaces || !hasCustomRenderingHandler(generatorName, options)) { + this.updateElRef(el) + } + } + + private updateElRef = (el: HTMLElement | null) => { + if (this.props.elRef) { + setRef(this.props.elRef, el) + } + } + + private applyQueueudDomNodes() { + const { queuedDomNodes, currentDomNodes } = this + const el = this.base + + if (!isArraysEqual(queuedDomNodes, currentDomNodes)) { + currentDomNodes.forEach(removeElement) + + for (let newNode of queuedDomNodes) { + el.appendChild(newNode) + } + + this.currentDomNodes = queuedDomNodes + } + } +} + +ContentInjector.addPropsEquality({ + elClasses: isArraysEqual, + elStyle: isPropsEqual, + elAttrs: isNonHandlerPropsEqual, + renderProps: isPropsEqual, +}) + +// Util + +/* +Does UI-framework provide custom way of rendering that does not use Preact VDOM +AND does the calendar's options define custom rendering? +AKA. Should we NOT render the default content? +*/ +export function hasCustomRenderingHandler( + generatorName: string | undefined, + options: ViewOptions, +): boolean { + return Boolean( + options.handleCustomRendering && + generatorName && + options.customRenderingMetaMap?.[generatorName], + ) +} + +export function buildElAttrs( + props: ElAttrsProps, + extraClassNames?: string[], + elRef?: ElRef, +): ElAttrs { + const attrs: ElAttrs = { ...props.elAttrs, ref: elRef as any } + + if (props.elClasses || extraClassNames) { + attrs.className = (props.elClasses || []) + .concat(extraClassNames || []) + .concat((attrs.className as (string | undefined)) || []) + .filter(Boolean) + .join(' ') + } + + if (props.elStyle) { + attrs.style = props.elStyle + } + + return attrs +} + +function isTruthy(val: any): boolean { + return Boolean(val) +} diff --git a/fullcalendar-main/packages/core/src/content-inject/CustomRenderingStore.ts b/fullcalendar-main/packages/core/src/content-inject/CustomRenderingStore.ts new file mode 100644 index 0000000..a55fe3a --- /dev/null +++ b/fullcalendar-main/packages/core/src/content-inject/CustomRenderingStore.ts @@ -0,0 +1,39 @@ +import { Store } from './Store.js' +import { ElProps } from './ContentInjector.js' + +export type CustomRenderingHandler<RenderProps> = (customRender: CustomRendering<RenderProps>) => void + +export interface CustomRendering<RenderProps> extends ElProps { + id: string // TODO: need this? Map can be responsible for storing key? + isActive: boolean + containerEl: HTMLElement + reportNewContainerEl: (el: HTMLElement | null) => void + generatorName: string + generatorMeta: any // could be as simple as boolean + renderProps: RenderProps +} + +/* +Subscribers will get a LIST of CustomRenderings +*/ +export class CustomRenderingStore<RenderProps> extends Store<Map<string, CustomRendering<RenderProps>>> { + private map = new Map<string, CustomRendering<RenderProps>>() + // for consistent order + + handle(customRendering: CustomRendering<RenderProps>): void { + const { map } = this + let updated = false + + if (customRendering.isActive) { + map.set(customRendering.id, customRendering as CustomRendering<RenderProps>) + updated = true + } else if (map.has(customRendering.id)) { + map.delete(customRendering.id) + updated = true + } + + if (updated) { + this.set(map) + } + } +} diff --git a/fullcalendar-main/packages/core/src/content-inject/RenderId.ts b/fullcalendar-main/packages/core/src/content-inject/RenderId.ts new file mode 100644 index 0000000..6bc13a3 --- /dev/null +++ b/fullcalendar-main/packages/core/src/content-inject/RenderId.ts @@ -0,0 +1,3 @@ +import { createContext } from '../preact.js' + +export const RenderId = createContext<number>(0) diff --git a/fullcalendar-main/packages/core/src/content-inject/Store.ts b/fullcalendar-main/packages/core/src/content-inject/Store.ts new file mode 100644 index 0000000..a234e83 --- /dev/null +++ b/fullcalendar-main/packages/core/src/content-inject/Store.ts @@ -0,0 +1,21 @@ + +export class Store<Value> { + private handlers: ((value: Value) => void)[] = [] + private currentValue: Value | undefined + + set(value: Value): void { + this.currentValue = value + + for (let handler of this.handlers) { + handler(value) + } + } + + subscribe(handler: (value: Value) => void) { + this.handlers.push(handler) + + if (this.currentValue !== undefined) { + handler(this.currentValue) + } + } +} diff --git a/fullcalendar-main/packages/core/src/datelib/DateFormatter.ts b/fullcalendar-main/packages/core/src/datelib/DateFormatter.ts new file mode 100644 index 0000000..ad0698f --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/DateFormatter.ts @@ -0,0 +1,50 @@ +import { DateMarker } from './marker.js' +import { CalendarSystem } from './calendar-system.js' +import { Locale } from './locale.js' +import { ZonedMarker, ExpandedZonedMarker, expandZonedMarker } from './zoned-marker.js' + +export interface VerboseFormattingArg { + date: ExpandedZonedMarker + start: ExpandedZonedMarker + end?: ExpandedZonedMarker + timeZone: string + localeCodes: string[], + defaultSeparator: string +} + +export function createVerboseFormattingArg( + start: ZonedMarker, + end: ZonedMarker, + context: DateFormattingContext, + betterDefaultSeparator?: string, +): VerboseFormattingArg { + let startInfo = expandZonedMarker(start, context.calendarSystem) + let endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null + + return { + date: startInfo, + start: startInfo, + end: endInfo, + timeZone: context.timeZone, + localeCodes: context.locale.codes, + defaultSeparator: betterDefaultSeparator || context.defaultSeparator, + } +} + +export type CmdFormatterFunc = (cmd: string, arg: VerboseFormattingArg) => string + +export interface DateFormattingContext { + timeZone: string, + locale: Locale, + calendarSystem: CalendarSystem + computeWeekNumber: (d: DateMarker) => number + weekText: string + weekTextLong: string + cmdFormatter?: CmdFormatterFunc + defaultSeparator: string +} + +export interface DateFormatter { + format(date: ZonedMarker, context: DateFormattingContext): string + formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string): string +} diff --git a/fullcalendar-main/packages/core/src/datelib/calendar-system.ts b/fullcalendar-main/packages/core/src/datelib/calendar-system.ts new file mode 100644 index 0000000..a0351a9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/calendar-system.ts @@ -0,0 +1,43 @@ +import { DateMarker, arrayToUtcDate, dateToUtcArray } from './marker.js' + +export interface CalendarSystem { + getMarkerYear(d: DateMarker): number + getMarkerMonth(d: DateMarker): number + getMarkerDay(d: DateMarker): number + arrayToMarker(arr: number[]): DateMarker + markerToArray(d: DateMarker): number[] +} + +let calendarSystemClassMap = {} + +export function registerCalendarSystem(name, theClass) { + calendarSystemClassMap[name] = theClass +} + +export function createCalendarSystem(name) { + return new calendarSystemClassMap[name]() +} + +class GregorianCalendarSystem implements CalendarSystem { + getMarkerYear(d: DateMarker) { + return d.getUTCFullYear() + } + + getMarkerMonth(d: DateMarker) { + return d.getUTCMonth() + } + + getMarkerDay(d: DateMarker) { + return d.getUTCDate() + } + + arrayToMarker(arr) { + return arrayToUtcDate(arr) + } + + markerToArray(marker) { + return dateToUtcArray(marker) + } +} + +registerCalendarSystem('gregory', GregorianCalendarSystem) diff --git a/fullcalendar-main/packages/core/src/datelib/date-range.ts b/fullcalendar-main/packages/core/src/datelib/date-range.ts new file mode 100644 index 0000000..279c253 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/date-range.ts @@ -0,0 +1,137 @@ +import { DateMarker } from './marker.js' +import { DateEnv, DateInput } from './env.js' + +export interface DateRangeInput { + start?: DateInput + end?: DateInput +} + +export interface OpenDateRange { + start: DateMarker | null + end: DateMarker | null +} + +export interface DateRange { + start: DateMarker + end: DateMarker +} + +export function parseRange(input: DateRangeInput, dateEnv: DateEnv): OpenDateRange { + let start = null + let end = null + + if (input.start) { + start = dateEnv.createMarker(input.start) + } + + if (input.end) { + end = dateEnv.createMarker(input.end) + } + + if (!start && !end) { + return null + } + + if (start && end && end < start) { + return null + } + + return { start, end } +} + +// SIDE-EFFECT: will mutate ranges. +// Will return a new array result. +export function invertRanges(ranges: DateRange[], constraintRange: DateRange): DateRange[] { + let invertedRanges: DateRange[] = [] + let { start } = constraintRange // the end of the previous range. the start of the new range + let i + let dateRange + + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges) + + for (i = 0; i < ranges.length; i += 1) { + dateRange = ranges[i] + + // add the span of time before the event (if there is any) + if (dateRange.start > start) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start, end: dateRange.start }) + } + + if (dateRange.end > start) { + start = dateRange.end + } + } + + // add the span of time after the last event (if there is any) + if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic) + invertedRanges.push({ start, end: constraintRange.end }) + } + + return invertedRanges +} + +function compareRanges(range0: DateRange, range1: DateRange) { + return range0.start.valueOf() - range1.start.valueOf() // earlier ranges go first +} + +export function intersectRanges(range0: OpenDateRange, range1: OpenDateRange): OpenDateRange { + let { start, end } = range0 + let newRange = null + + if (range1.start !== null) { + if (start === null) { + start = range1.start + } else { + start = new Date(Math.max(start.valueOf(), range1.start.valueOf())) + } + } + + if (range1.end != null) { + if (end === null) { + end = range1.end + } else { + end = new Date(Math.min(end.valueOf(), range1.end.valueOf())) + } + } + + if (start === null || end === null || start < end) { + newRange = { start, end } + } + + return newRange +} + +export function rangesEqual(range0: OpenDateRange, range1: OpenDateRange): boolean { + return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) && + (range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf()) +} + +export function rangesIntersect(range0: OpenDateRange, range1: OpenDateRange): boolean { + return (range0.end === null || range1.start === null || range0.end > range1.start) && + (range0.start === null || range1.end === null || range0.start < range1.end) +} + +export function rangeContainsRange(outerRange: OpenDateRange, innerRange: OpenDateRange): boolean { + return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) && + (outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end)) +} + +export function rangeContainsMarker(range: OpenDateRange, date: DateMarker | number): boolean { // date can be a millisecond time + return (range.start === null || date >= range.start) && + (range.end === null || date < range.end) +} + +// If the given date is not within the given range, move it inside. +// (If it's past the end, make it one millisecond before the end). +export function constrainMarkerToRange(date: DateMarker, range: DateRange): DateMarker { + if (range.start != null && date < range.start) { + return range.start + } + + if (range.end != null && date >= range.end) { + return new Date(range.end.valueOf() - 1) + } + + return date +} diff --git a/fullcalendar-main/packages/core/src/datelib/duration.ts b/fullcalendar-main/packages/core/src/datelib/duration.ts new file mode 100644 index 0000000..8bdaa9a --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/duration.ts @@ -0,0 +1,226 @@ +import { isInt } from '../util/misc.js' + +export type DurationInput = DurationObjectInput | string | number + +export interface DurationObjectInput { + years?: number + year?: number + months?: number + month?: number + weeks?: number + week?: number + days?: number + day?: number + hours?: number + hour?: number + minutes?: number + minute?: number + seconds?: number + second?: number + milliseconds?: number + millisecond?: number + ms?: number +} + +export interface Duration { + years: number + months: number + days: number + milliseconds: number + specifiedWeeks?: boolean +} + +const INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds'] +const PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/ + +// Parsing and Creation + +export function createDuration(input: DurationInput, unit?: string): Duration | null { + if (typeof input === 'string') { + return parseString(input) + } + + if (typeof input === 'object' && input) { // non-null object + return parseObject(input) + } + + if (typeof input === 'number') { + return parseObject({ [unit || 'milliseconds']: input }) + } + return null +} + +function parseString(s: string): Duration { + let m = PARSE_RE.exec(s) + if (m) { + let sign = m[1] ? -1 : 1 + return { + years: 0, + months: 0, + days: sign * (m[2] ? parseInt(m[2], 10) : 0), + milliseconds: sign * ( + (m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours + (m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes + (m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds + (m[6] ? parseInt(m[6], 10) : 0) // ms + ), + } + } + return null +} + +function parseObject(obj: DurationObjectInput): Duration { + let duration: Duration = { + years: obj.years || obj.year || 0, + months: obj.months || obj.month || 0, + days: obj.days || obj.day || 0, + milliseconds: + (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours + (obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes + (obj.seconds || obj.second || 0) * 1000 + // seconds + (obj.milliseconds || obj.millisecond || obj.ms || 0), // ms + } + + let weeks = obj.weeks || obj.week + if (weeks) { + duration.days += weeks * 7 + duration.specifiedWeeks = true + } + + return duration +} + +// Equality + +export function durationsEqual(d0: Duration, d1: Duration): boolean { + return d0.years === d1.years && + d0.months === d1.months && + d0.days === d1.days && + d0.milliseconds === d1.milliseconds +} + +export function asCleanDays(dur: Duration) { + if (!dur.years && !dur.months && !dur.milliseconds) { + return dur.days + } + return 0 +} + +// Simple Math + +export function addDurations(d0: Duration, d1: Duration) { + return { + years: d0.years + d1.years, + months: d0.months + d1.months, + days: d0.days + d1.days, + milliseconds: d0.milliseconds + d1.milliseconds, + } +} + +export function subtractDurations(d1: Duration, d0: Duration): Duration { + return { + years: d1.years - d0.years, + months: d1.months - d0.months, + days: d1.days - d0.days, + milliseconds: d1.milliseconds - d0.milliseconds, + } +} + +export function multiplyDuration(d: Duration, n: number) { + return { + years: d.years * n, + months: d.months * n, + days: d.days * n, + milliseconds: d.milliseconds * n, + } +} + +// Conversions +// "Rough" because they are based on average-case Gregorian months/years + +export function asRoughYears(dur: Duration) { + return asRoughDays(dur) / 365 +} + +export function asRoughMonths(dur: Duration) { + return asRoughDays(dur) / 30 +} + +export function asRoughDays(dur: Duration) { + return asRoughMs(dur) / 864e5 +} + +export function asRoughHours(dur: Duration) { + return asRoughMs(dur) / (1000 * 60 * 60) +} + +export function asRoughMinutes(dur: Duration) { + return asRoughMs(dur) / (1000 * 60) +} + +export function asRoughSeconds(dur: Duration) { + return asRoughMs(dur) / 1000 +} + +export function asRoughMs(dur: Duration) { + return dur.years * (365 * 864e5) + + dur.months * (30 * 864e5) + + dur.days * 864e5 + + dur.milliseconds +} + +// Advanced Math + +export function wholeDivideDurations(numerator: Duration, denominator: Duration): number { + let res = null + + for (let i = 0; i < INTERNAL_UNITS.length; i += 1) { + let unit = INTERNAL_UNITS[i] + + if (denominator[unit]) { + let localRes = numerator[unit] / denominator[unit] + + if (!isInt(localRes) || (res !== null && res !== localRes)) { + return null + } + + res = localRes + } else if (numerator[unit]) { + // needs to divide by something but can't! + return null + } + } + + return res +} + +export function greatestDurationDenominator(dur: Duration) { + let ms = dur.milliseconds + if (ms) { + if (ms % 1000 !== 0) { + return { unit: 'millisecond', value: ms } + } + if (ms % (1000 * 60) !== 0) { + return { unit: 'second', value: ms / 1000 } + } + if (ms % (1000 * 60 * 60) !== 0) { + return { unit: 'minute', value: ms / (1000 * 60) } + } + if (ms) { + return { unit: 'hour', value: ms / (1000 * 60 * 60) } + } + } + if (dur.days) { + if (dur.specifiedWeeks && dur.days % 7 === 0) { + return { unit: 'week', value: dur.days / 7 } + } + return { unit: 'day', value: dur.days } + } + if (dur.months) { + return { unit: 'month', value: dur.months } + } + if (dur.years) { + return { unit: 'year', value: dur.years } + } + return { unit: 'millisecond', value: 0 } +} diff --git a/fullcalendar-main/packages/core/src/datelib/env.ts b/fullcalendar-main/packages/core/src/datelib/env.ts new file mode 100644 index 0000000..6fa7c82 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/env.ts @@ -0,0 +1,463 @@ +import { + DateMarker, addMs, + diffHours, diffMinutes, diffSeconds, diffWholeWeeks, diffWholeDays, + startOfDay, startOfHour, startOfMinute, startOfSecond, + weekOfYear, arrayToUtcDate, dateToUtcArray, dateToLocalArray, arrayToLocalDate, timeAsMs, isValidDate, +} from './marker.js' +import { CalendarSystem, createCalendarSystem } from './calendar-system.js' +import { Locale } from './locale.js' +import { NamedTimeZoneImpl, NamedTimeZoneImplClass } from './timezone.js' +import { Duration, asRoughYears, asRoughMonths, asRoughDays, asRoughMs } from './duration.js' +import { DateFormatter, CmdFormatterFunc } from './DateFormatter.js' +import { buildIsoString } from './formatting-utils.js' +import { parse } from './parsing.js' +import { isInt } from '../util/misc.js' + +export type WeekNumberCalculation = 'local' | 'ISO' | ((m: Date) => number) + +export interface DateEnvSettings { + timeZone: string + namedTimeZoneImpl?: NamedTimeZoneImplClass + calendarSystem: string + locale: Locale + weekNumberCalculation?: WeekNumberCalculation + firstDay?: number, // will override what the locale wants + weekText?: string, + weekTextLong?: string + cmdFormatter?: CmdFormatterFunc + defaultSeparator?: string +} + +export type DateInput = Date | string | number | number[] + +export interface DateMarkerMeta { + marker: DateMarker + isTimeUnspecified: boolean + forcedTzo: number | null +} + +export class DateEnv { + timeZone: string + namedTimeZoneImpl: NamedTimeZoneImpl + canComputeOffset: boolean + + calendarSystem: CalendarSystem + locale: Locale + weekDow: number // which day begins the week + weekDoy: number // which day must be within the year, for computing the first week number + weekNumberFunc: any + weekText: string // DON'T LIKE how options are confused with local + weekTextLong: string + cmdFormatter?: CmdFormatterFunc + defaultSeparator: string + + constructor(settings: DateEnvSettings) { + let timeZone = this.timeZone = settings.timeZone + let isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC' + + if (settings.namedTimeZoneImpl && isNamedTimeZone) { + this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone) + } + + this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl) + + this.calendarSystem = createCalendarSystem(settings.calendarSystem) + this.locale = settings.locale + this.weekDow = settings.locale.week.dow + this.weekDoy = settings.locale.week.doy + + if (settings.weekNumberCalculation === 'ISO') { + this.weekDow = 1 + this.weekDoy = 4 + } + + if (typeof settings.firstDay === 'number') { + this.weekDow = settings.firstDay + } + + if (typeof settings.weekNumberCalculation === 'function') { + this.weekNumberFunc = settings.weekNumberCalculation + } + + this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText + this.weekTextLong = (settings.weekTextLong != null ? settings.weekTextLong : settings.locale.options.weekTextLong) || this.weekText + + this.cmdFormatter = settings.cmdFormatter + this.defaultSeparator = settings.defaultSeparator + } + + // Creating / Parsing + + createMarker(input: DateInput): DateMarker { + let meta = this.createMarkerMeta(input) + if (meta === null) { + return null + } + return meta.marker + } + + createNowMarker(): DateMarker { + if (this.canComputeOffset) { + return this.timestampToMarker(new Date().valueOf()) + } + // if we can't compute the current date val for a timezone, + // better to give the current local date vals than UTC + return arrayToUtcDate(dateToLocalArray(new Date())) + } + + createMarkerMeta(input: DateInput): DateMarkerMeta { + if (typeof input === 'string') { + return this.parse(input) + } + + let marker = null + + if (typeof input === 'number') { + marker = this.timestampToMarker(input) + } else if (input instanceof Date) { + input = input.valueOf() + + if (!isNaN(input)) { + marker = this.timestampToMarker(input) + } + } else if (Array.isArray(input)) { + marker = arrayToUtcDate(input) + } + + if (marker === null || !isValidDate(marker)) { + return null + } + + return { marker, isTimeUnspecified: false, forcedTzo: null } + } + + parse(s: string) { + let parts = parse(s) + if (parts === null) { + return null + } + + let { marker } = parts + let forcedTzo = null + + if (parts.timeZoneOffset !== null) { + if (this.canComputeOffset) { + marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000) + } else { + forcedTzo = parts.timeZoneOffset + } + } + + return { marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo } + } + + // Accessors + + getYear(marker: DateMarker): number { + return this.calendarSystem.getMarkerYear(marker) + } + + getMonth(marker: DateMarker): number { + return this.calendarSystem.getMarkerMonth(marker) + } + + getDay(marker: DateMarker): number { + return this.calendarSystem.getMarkerDay(marker) + } + + // Adding / Subtracting + + add(marker: DateMarker, dur: Duration): DateMarker { + let a = this.calendarSystem.markerToArray(marker) + a[0] += dur.years + a[1] += dur.months + a[2] += dur.days + a[6] += dur.milliseconds + return this.calendarSystem.arrayToMarker(a) + } + + subtract(marker: DateMarker, dur: Duration): DateMarker { + let a = this.calendarSystem.markerToArray(marker) + a[0] -= dur.years + a[1] -= dur.months + a[2] -= dur.days + a[6] -= dur.milliseconds + return this.calendarSystem.arrayToMarker(a) + } + + addYears(marker: DateMarker, n: number) { + let a = this.calendarSystem.markerToArray(marker) + a[0] += n + return this.calendarSystem.arrayToMarker(a) + } + + addMonths(marker: DateMarker, n: number) { + let a = this.calendarSystem.markerToArray(marker) + a[1] += n + return this.calendarSystem.arrayToMarker(a) + } + + // Diffing Whole Units + + diffWholeYears(m0: DateMarker, m1: DateMarker): number { + let { calendarSystem } = this + + if ( + timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) && + calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1) + ) { + return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0) + } + return null + } + + diffWholeMonths(m0: DateMarker, m1: DateMarker): number { + let { calendarSystem } = this + + if ( + timeAsMs(m0) === timeAsMs(m1) && + calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) + ) { + return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) + + (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12 + } + return null + } + + // Range / Duration + + greatestWholeUnit(m0: DateMarker, m1: DateMarker) { + let n = this.diffWholeYears(m0, m1) + + if (n !== null) { + return { unit: 'year', value: n } + } + + n = this.diffWholeMonths(m0, m1) + + if (n !== null) { + return { unit: 'month', value: n } + } + + n = diffWholeWeeks(m0, m1) + + if (n !== null) { + return { unit: 'week', value: n } + } + + n = diffWholeDays(m0, m1) + + if (n !== null) { + return { unit: 'day', value: n } + } + + n = diffHours(m0, m1) + + if (isInt(n)) { + return { unit: 'hour', value: n } + } + + n = diffMinutes(m0, m1) + + if (isInt(n)) { + return { unit: 'minute', value: n } + } + + n = diffSeconds(m0, m1) + + if (isInt(n)) { + return { unit: 'second', value: n } + } + + return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() } + } + + countDurationsBetween(m0: DateMarker, m1: DateMarker, d: Duration) { + // TODO: can use greatestWholeUnit + let diff + + if (d.years) { + diff = this.diffWholeYears(m0, m1) + if (diff !== null) { + return diff / asRoughYears(d) + } + } + + if (d.months) { + diff = this.diffWholeMonths(m0, m1) + if (diff !== null) { + return diff / asRoughMonths(d) + } + } + + if (d.days) { + diff = diffWholeDays(m0, m1) + if (diff !== null) { + return diff / asRoughDays(d) + } + } + + return (m1.valueOf() - m0.valueOf()) / asRoughMs(d) + } + + // Start-Of + // these DON'T return zoned-dates. only UTC start-of dates + + startOf(m: DateMarker, unit: string) { + if (unit === 'year') { + return this.startOfYear(m) + } + if (unit === 'month') { + return this.startOfMonth(m) + } + if (unit === 'week') { + return this.startOfWeek(m) + } + if (unit === 'day') { + return startOfDay(m) + } + if (unit === 'hour') { + return startOfHour(m) + } + if (unit === 'minute') { + return startOfMinute(m) + } + if (unit === 'second') { + return startOfSecond(m) + } + return null + } + + startOfYear(m: DateMarker): DateMarker { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + ]) + } + + startOfMonth(m: DateMarker): DateMarker { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + ]) + } + + startOfWeek(m: DateMarker): DateMarker { + return this.calendarSystem.arrayToMarker([ + this.calendarSystem.getMarkerYear(m), + this.calendarSystem.getMarkerMonth(m), + m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7), + ]) + } + + // Week Number + + computeWeekNumber(marker: DateMarker): number { + if (this.weekNumberFunc) { + return this.weekNumberFunc(this.toDate(marker)) + } + return weekOfYear(marker, this.weekDow, this.weekDoy) + } + + // TODO: choke on timeZoneName: long + format(marker: DateMarker, formatter: DateFormatter, dateOptions: { forcedTzo?: number } = {}) { + return formatter.format( + { + marker, + timeZoneOffset: dateOptions.forcedTzo != null ? + dateOptions.forcedTzo : + this.offsetForMarker(marker), + }, + this, + ) + } + + formatRange( + start: DateMarker, + end: DateMarker, + formatter: DateFormatter, + dateOptions: { forcedStartTzo?: number, forcedEndTzo?: number, isEndExclusive?: boolean, defaultSeparator?: string } = {}, + ) { + if (dateOptions.isEndExclusive) { + end = addMs(end, -1) + } + + return formatter.formatRange( + { + marker: start, + timeZoneOffset: dateOptions.forcedStartTzo != null ? + dateOptions.forcedStartTzo : + this.offsetForMarker(start), + }, + { + marker: end, + timeZoneOffset: dateOptions.forcedEndTzo != null ? + dateOptions.forcedEndTzo : + this.offsetForMarker(end), + }, + this, + dateOptions.defaultSeparator, + ) + } + + /* + DUMB: the omitTime arg is dumb. if we omit the time, we want to omit the timezone offset. and if we do that, + might as well use buildIsoString or some other util directly + */ + formatIso(marker: DateMarker, extraOptions: any = {}) { + let timeZoneOffset = null + + if (!extraOptions.omitTimeZoneOffset) { + if (extraOptions.forcedTzo != null) { + timeZoneOffset = extraOptions.forcedTzo + } else { + timeZoneOffset = this.offsetForMarker(marker) + } + } + + return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime) + } + + // TimeZone + + timestampToMarker(ms: number) { + if (this.timeZone === 'local') { + return arrayToUtcDate(dateToLocalArray(new Date(ms))) + } if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) { + return new Date(ms) + } + return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms)) + } + + offsetForMarker(m: DateMarker) { + if (this.timeZone === 'local') { + return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset() // convert "inverse" offset to "normal" offset + } if (this.timeZone === 'UTC') { + return 0 + } if (this.namedTimeZoneImpl) { + return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) + } + return null + } + + // Conversion + + toDate(m: DateMarker, forcedTzo?: number): Date { + if (this.timeZone === 'local') { + return arrayToLocalDate(dateToUtcArray(m)) + } + + if (this.timeZone === 'UTC') { + return new Date(m.valueOf()) // make sure it's a copy + } + + if (!this.namedTimeZoneImpl) { + return new Date(m.valueOf() - (forcedTzo || 0)) + } + + return new Date( + m.valueOf() - + this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60, // convert minutes -> ms + ) + } +} diff --git a/fullcalendar-main/packages/core/src/datelib/formatting-cmd.ts b/fullcalendar-main/packages/core/src/datelib/formatting-cmd.ts new file mode 100644 index 0000000..3e83ae3 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/formatting-cmd.ts @@ -0,0 +1,26 @@ +import { DateFormatter, DateFormattingContext, createVerboseFormattingArg } from './DateFormatter.js' +import { ZonedMarker } from './zoned-marker.js' + +/* +TODO: fix the terminology of "formatter" vs "formatting func" +*/ + +/* +At the time of instantiation, this object does not know which cmd-formatting system it will use. +It receives this at the time of formatting, as a setting. +*/ +export class CmdFormatter implements DateFormatter { + cmdStr: string + + constructor(cmdStr: string) { + this.cmdStr = cmdStr + } + + format(date: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, betterDefaultSeparator)) + } + + formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string) { + return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, betterDefaultSeparator)) + } +} diff --git a/fullcalendar-main/packages/core/src/datelib/formatting-func.ts b/fullcalendar-main/packages/core/src/datelib/formatting-func.ts new file mode 100644 index 0000000..97e8ee5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/formatting-func.ts @@ -0,0 +1,20 @@ +import { DateFormatter, DateFormattingContext, createVerboseFormattingArg, VerboseFormattingArg } from './DateFormatter.js' +import { ZonedMarker } from './zoned-marker.js' + +export type FuncFormatterFunc = (arg: VerboseFormattingArg) => string + +export class FuncFormatter implements DateFormatter { + func: FuncFormatterFunc + + constructor(func: FuncFormatterFunc) { + this.func = func + } + + format(date: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string) { + return this.func(createVerboseFormattingArg(date, null, context, betterDefaultSeparator)) + } + + formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string) { + return this.func(createVerboseFormattingArg(start, end, context, betterDefaultSeparator)) + } +} diff --git a/fullcalendar-main/packages/core/src/datelib/formatting-native.ts b/fullcalendar-main/packages/core/src/datelib/formatting-native.ts new file mode 100644 index 0000000..3a747f1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/formatting-native.ts @@ -0,0 +1,376 @@ +import { DateMarker, timeAsMs } from './marker.js' +import { CalendarSystem } from './calendar-system.js' +import { Locale } from './locale.js' +import { DateFormatter, DateFormattingContext } from './DateFormatter.js' +import { ZonedMarker } from './zoned-marker.js' +import { formatTimeZoneOffset } from './formatting-utils.js' +import { memoize } from '../util/memoize.js' + +const EXTENDED_SETTINGS_AND_SEVERITIES = { + week: 3, + separator: 0, // 0 = not applicable + omitZeroMinute: 0, + meridiem: 0, // like am/pm + omitCommas: 0, +} + +const STANDARD_DATE_PROP_SEVERITIES = { + timeZoneName: 7, + era: 6, + year: 5, + month: 4, + day: 2, + weekday: 2, + hour: 1, + minute: 1, + second: 1, +} + +const MERIDIEM_RE = /\s*([ap])\.?m\.?/i // eats up leading spaces too +const COMMA_RE = /,/g // we need re for globalness +const MULTI_SPACE_RE = /\s+/g +const LTR_RE = /\u200e/g // control character +const UTC_RE = /UTC|GMT/ + +export interface NativeFormatterOptions extends Intl.DateTimeFormatOptions { + week?: 'long' | 'short' | 'narrow' | 'numeric' + meridiem?: 'lowercase' | 'short' | 'narrow' | boolean + omitZeroMinute?: boolean + omitCommas?: boolean + separator?: string +} + +export class NativeFormatter implements DateFormatter { + standardDateProps: any + extendedSettings: any + severity: number + private buildFormattingFunc: typeof buildFormattingFunc // caching for efficiency with same date env + + constructor(formatSettings: NativeFormatterOptions) { + let standardDateProps: any = {} + let extendedSettings: any = {} + let severity = 0 + + for (let name in formatSettings) { + if (name in EXTENDED_SETTINGS_AND_SEVERITIES) { + extendedSettings[name] = formatSettings[name] + severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name], severity) + } else { + standardDateProps[name] = formatSettings[name] + + if (name in STANDARD_DATE_PROP_SEVERITIES) { // TODO: what about hour12? no severity + severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name], severity) + } + } + } + + this.standardDateProps = standardDateProps + this.extendedSettings = extendedSettings + this.severity = severity + + this.buildFormattingFunc = memoize(buildFormattingFunc) + } + + format(date: ZonedMarker, context: DateFormattingContext) { + return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date) + } + + formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, betterDefaultSeparator?: string) { + let { standardDateProps, extendedSettings } = this + + let diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem) + if (!diffSeverity) { + return this.format(start, context) + } + + let biggestUnitForPartial = diffSeverity + if ( + biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time + (standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') && + (standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') && + (standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit') + ) { + biggestUnitForPartial = 1 // make it look like the dates are only different in terms of time + } + + let full0 = this.format(start, context) + let full1 = this.format(end, context) + + if (full0 === full1) { + return full0 + } + + let partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial) + let partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context) + let partial0 = partialFormattingFunc(start) + let partial1 = partialFormattingFunc(end) + + let insertion = findCommonInsertion(full0, partial0, full1, partial1) + let separator = extendedSettings.separator || betterDefaultSeparator || context.defaultSeparator || '' + + if (insertion) { + return insertion.before + partial0 + separator + partial1 + insertion.after + } + + return full0 + separator + full1 + } + + getLargestUnit() { + switch (this.severity) { + case 7: + case 6: + case 5: + return 'year' + case 4: + return 'month' + case 3: + return 'week' + case 2: + return 'day' + default: + return 'time' // really? + } + } +} + +function buildFormattingFunc( + standardDateProps, + extendedSettings, + context: DateFormattingContext, +): (date: ZonedMarker) => string { + let standardDatePropCnt = Object.keys(standardDateProps).length + + if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') { + return (date: ZonedMarker) => ( + formatTimeZoneOffset(date.timeZoneOffset) + ) + } + + if (standardDatePropCnt === 0 && extendedSettings.week) { + return (date: ZonedMarker) => ( + formatWeekNumber( + context.computeWeekNumber(date.marker), + context.weekText, + context.weekTextLong, + context.locale, + extendedSettings.week, + ) + ) + } + + return buildNativeFormattingFunc(standardDateProps, extendedSettings, context) +} + +function buildNativeFormattingFunc( + standardDateProps, + extendedSettings, + context: DateFormattingContext, +): (date: ZonedMarker) => string { + standardDateProps = { ...standardDateProps } // copy + extendedSettings = { ...extendedSettings } // copy + + sanitizeSettings(standardDateProps, extendedSettings) + + standardDateProps.timeZone = 'UTC' // we leverage the only guaranteed timeZone for our UTC markers + + let normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps) + let zeroFormat // needed? + + if (extendedSettings.omitZeroMinute) { + let zeroProps = { ...standardDateProps } + delete zeroProps.minute // seconds and ms were already considered in sanitizeSettings + zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps) + } + + return (date: ZonedMarker) => { + let { marker } = date + let format + + if (zeroFormat && !marker.getUTCMinutes()) { + format = zeroFormat + } else { + format = normalFormat + } + + let s = format.format(marker) + + return postProcess(s, date, standardDateProps, extendedSettings, context) + } +} + +function sanitizeSettings(standardDateProps, extendedSettings) { + // deal with a browser inconsistency where formatting the timezone + // requires that the hour/minute be present. + if (standardDateProps.timeZoneName) { + if (!standardDateProps.hour) { + standardDateProps.hour = '2-digit' + } + if (!standardDateProps.minute) { + standardDateProps.minute = '2-digit' + } + } + + // only support short timezone names + if (standardDateProps.timeZoneName === 'long') { + standardDateProps.timeZoneName = 'short' + } + + // if requesting to display seconds, MUST display minutes + if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) { + delete extendedSettings.omitZeroMinute + } +} + +function postProcess(s: string, date: ZonedMarker, standardDateProps, extendedSettings, context: DateFormattingContext): string { + s = s.replace(LTR_RE, '') // remove left-to-right control chars. do first. good for other regexes + + if (standardDateProps.timeZoneName === 'short') { + s = injectTzoStr( + s, + (context.timeZone === 'UTC' || date.timeZoneOffset == null) ? + 'UTC' : // important to normalize for IE, which does "GMT" + formatTimeZoneOffset(date.timeZoneOffset), + ) + } + + if (extendedSettings.omitCommas) { + s = s.replace(COMMA_RE, '').trim() + } + + if (extendedSettings.omitZeroMinute) { + s = s.replace(':00', '') // zeroFormat doesn't always achieve this + } + + // ^ do anything that might create adjacent spaces before this point, + // because MERIDIEM_RE likes to eat up loading spaces + + if (extendedSettings.meridiem === false) { + s = s.replace(MERIDIEM_RE, '').trim() + } else if (extendedSettings.meridiem === 'narrow') { // a/p + s = s.replace(MERIDIEM_RE, (m0, m1) => m1.toLocaleLowerCase()) + } else if (extendedSettings.meridiem === 'short') { // am/pm + s = s.replace(MERIDIEM_RE, (m0, m1) => `${m1.toLocaleLowerCase()}m`) + } else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase + s = s.replace(MERIDIEM_RE, (m0) => m0.toLocaleLowerCase()) + } + + s = s.replace(MULTI_SPACE_RE, ' ') + s = s.trim() + + return s +} + +function injectTzoStr(s: string, tzoStr: string): string { + let replaced = false + + s = s.replace(UTC_RE, () => { + replaced = true + return tzoStr + }) + + // IE11 doesn't include UTC/GMT in the original string, so append to end + if (!replaced) { + s += ` ${tzoStr}` + } + + return s +} + +function formatWeekNumber( + num: number, + weekText: string, + weekTextLong: string, + locale: Locale, + display?: 'numeric' | 'narrow' | 'short' | 'long', +): string { + let parts = [] + + if (display === 'long') { + parts.push(weekTextLong) + } else if (display === 'short' || display === 'narrow') { + parts.push(weekText) + } + + if (display === 'long' || display === 'short') { + parts.push(' ') + } + + parts.push(locale.simpleNumberFormat.format(num)) + + if (locale.options.direction === 'rtl') { // TODO: use control characters instead? + parts.reverse() + } + + return parts.join('') +} + +// Range Formatting Utils + +// 0 = exactly the same +// 1 = different by time +// and bigger +function computeMarkerDiffSeverity(d0: DateMarker, d1: DateMarker, ca: CalendarSystem) { + if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) { + return 5 + } + if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) { + return 4 + } + if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) { + return 2 + } + if (timeAsMs(d0) !== timeAsMs(d1)) { + return 1 + } + return 0 +} + +function computePartialFormattingOptions(options, biggestUnit) { + let partialOptions = {} + + for (let name in options) { + if ( + !(name in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone) + STANDARD_DATE_PROP_SEVERITIES[name] <= biggestUnit + ) { + partialOptions[name] = options[name] + } + } + + return partialOptions +} + +function findCommonInsertion(full0, partial0, full1, partial1) { + let i0 = 0 + while (i0 < full0.length) { + let found0 = full0.indexOf(partial0, i0) + if (found0 === -1) { + break + } + + let before0 = full0.substr(0, found0) + i0 = found0 + partial0.length + let after0 = full0.substr(i0) + + let i1 = 0 + while (i1 < full1.length) { + let found1 = full1.indexOf(partial1, i1) + if (found1 === -1) { + break + } + + let before1 = full1.substr(0, found1) + i1 = found1 + partial1.length + let after1 = full1.substr(i1) + + if (before0 === before1 && after0 === after1) { + return { + before: before0, + after: after0, + } + } + } + } + + return null +} diff --git a/fullcalendar-main/packages/core/src/datelib/formatting-utils.ts b/fullcalendar-main/packages/core/src/datelib/formatting-utils.ts new file mode 100644 index 0000000..734b969 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/formatting-utils.ts @@ -0,0 +1,54 @@ +import { DateMarker } from './marker.js' +import { padStart } from '../util/misc.js' + +// timeZoneOffset is in minutes +export function buildIsoString(marker: DateMarker, timeZoneOffset?: number, stripZeroTime: boolean = false) { + let s = marker.toISOString() + + s = s.replace('.000', '') + + if (stripZeroTime) { + s = s.replace('T00:00:00Z', '') + } + + if (s.length > 10) { // time part wasn't stripped, can add timezone info + if (timeZoneOffset == null) { + s = s.replace('Z', '') + } else if (timeZoneOffset !== 0) { + s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true)) + } + // otherwise, its UTC-0 and we want to keep the Z + } + + return s +} + +// formats the date, but with no time part +// TODO: somehow merge with buildIsoString and stripZeroTime +// TODO: rename. omit "string" +export function formatDayString(marker: DateMarker) { + return marker.toISOString().replace(/T.*$/, '') +} + +export function formatIsoMonthStr(marker: DateMarker) { + return marker.toISOString().match(/^\d{4}-\d{2}/)[0] +} + +// TODO: use Date::toISOString and use everything after the T? +export function formatIsoTimeString(marker: DateMarker) { + return padStart(marker.getUTCHours(), 2) + ':' + + padStart(marker.getUTCMinutes(), 2) + ':' + + padStart(marker.getUTCSeconds(), 2) +} + +export function formatTimeZoneOffset(minutes: number, doIso = false) { + let sign = minutes < 0 ? '-' : '+' + let abs = Math.abs(minutes) + let hours = Math.floor(abs / 60) + let mins = Math.round(abs % 60) + + if (doIso) { + return `${sign + padStart(hours, 2)}:${padStart(mins, 2)}` + } + return `GMT${sign}${hours}${mins ? `:${padStart(mins, 2)}` : ''}` +} diff --git a/fullcalendar-main/packages/core/src/datelib/formatting.ts b/fullcalendar-main/packages/core/src/datelib/formatting.ts new file mode 100644 index 0000000..07d5618 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/formatting.ts @@ -0,0 +1,22 @@ +import { NativeFormatter, NativeFormatterOptions } from './formatting-native.js' +import { CmdFormatter } from './formatting-cmd.js' +import { FuncFormatter, FuncFormatterFunc } from './formatting-func.js' +import { DateFormatter } from './DateFormatter.js' + +export type FormatterInput = NativeFormatterOptions | string | FuncFormatterFunc + +export function createFormatter(input: FormatterInput): DateFormatter { + if (typeof input === 'object' && input) { // non-null object + return new NativeFormatter(input) + } + + if (typeof input === 'string') { + return new CmdFormatter(input) + } + + if (typeof input === 'function') { + return new FuncFormatter(input as FuncFormatterFunc) + } + + return null +} diff --git a/fullcalendar-main/packages/core/src/datelib/locale.ts b/fullcalendar-main/packages/core/src/datelib/locale.ts new file mode 100644 index 0000000..2e7efc8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/locale.ts @@ -0,0 +1,140 @@ +import { mergeProps } from '../util/object.js' +import { globalLocales } from '../global-locales.js' // weird to be importing this +import { CalendarOptions, CalendarOptionsRefined } from '../options.js' + +export type LocaleCodeArg = string | string[] +export type LocaleSingularArg = LocaleCodeArg | LocaleInput + +export interface Locale { + codeArg: LocaleCodeArg + codes: string[] + week: { dow: number, doy: number } + simpleNumberFormat: Intl.NumberFormat + options: CalendarOptionsRefined +} + +export interface LocaleInput extends CalendarOptions { + code: string +} + +export type LocaleInputMap = { [code: string]: LocaleInput } + +export interface RawLocaleInfo { + map: LocaleInputMap + defaultCode: string +} + +const MINIMAL_RAW_EN_LOCALE = { + code: 'en', + week: { + dow: 0, // Sunday is the first day of the week + doy: 4, // 4 days need to be within the year to be considered the first week + }, + direction: 'ltr' as ('ltr' | 'rtl'), // TODO: make a real type for this + buttonText: { + prev: 'prev', + next: 'next', + prevYear: 'prev year', + nextYear: 'next year', + year: 'year', + today: 'today', + month: 'month', + week: 'week', + day: 'day', + list: 'list', + }, + weekText: 'W', + weekTextLong: 'Week', + closeHint: 'Close', + timeHint: 'Time', + eventHint: 'Event', + allDayText: 'all-day', + moreLinkText: 'more', + noEventsText: 'No events to display', +} + +const RAW_EN_LOCALE = { + ...MINIMAL_RAW_EN_LOCALE, + // Includes things we don't want other locales to inherit, + // things that derive from other translatable strings. + buttonHints: { + prev: 'Previous $0', + next: 'Next $0', + today(buttonText, unit) { + return (unit === 'day') + ? 'Today' + : `This ${buttonText}` + }, + }, + viewHint: '$0 view', + navLinkHint: 'Go to $0', + moreLinkHint(eventCnt: number) { + return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}` + }, +} + +export function organizeRawLocales(explicitRawLocales: LocaleInput[]): RawLocaleInfo { + let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en' + let allRawLocales = globalLocales.concat(explicitRawLocales) + let rawLocaleMap: LocaleInputMap = { + en: RAW_EN_LOCALE, + } + + for (let rawLocale of allRawLocales) { + rawLocaleMap[rawLocale.code] = rawLocale + } + + return { + map: rawLocaleMap, + defaultCode, + } +} + +export function buildLocale(inputSingular: LocaleSingularArg, available: LocaleInputMap) { + if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) { + return parseLocale( + inputSingular.code, + [inputSingular.code], + inputSingular, + ) + } + return queryLocale(inputSingular, available) +} + +function queryLocale(codeArg: LocaleCodeArg, available: LocaleInputMap): Locale { + let codes = [].concat(codeArg || []) // will convert to array + let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE + + return parseLocale(codeArg, codes, raw) +} + +function queryRawLocale(codes: string[], available: LocaleInputMap): LocaleInput { + for (let i = 0; i < codes.length; i += 1) { + let parts = codes[i].toLocaleLowerCase().split('-') + + for (let j = parts.length; j > 0; j -= 1) { + let simpleId = parts.slice(0, j).join('-') + + if (available[simpleId]) { + return available[simpleId] + } + } + } + return null +} + +function parseLocale(codeArg: LocaleCodeArg, codes: string[], raw: LocaleInput): Locale { + let merged = mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']) + + delete merged.code // don't want this part of the options + let { week } = merged + delete merged.week + + return { + codeArg, + codes, + week, + simpleNumberFormat: new Intl.NumberFormat(codeArg), + options: merged, + } +} diff --git a/fullcalendar-main/packages/core/src/datelib/marker.ts b/fullcalendar-main/packages/core/src/datelib/marker.ts new file mode 100644 index 0000000..257646e --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/marker.ts @@ -0,0 +1,216 @@ +import { Duration } from './duration.js' + +export type DateMarker = Date + +export const DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + +// Adding + +export function addWeeks(m: DateMarker, n: number) { + let a = dateToUtcArray(m) + a[2] += n * 7 + return arrayToUtcDate(a) +} + +export function addDays(m: DateMarker, n: number) { + let a = dateToUtcArray(m) + a[2] += n + return arrayToUtcDate(a) +} + +export function addMs(m: DateMarker, n: number) { + let a = dateToUtcArray(m) + a[6] += n + return arrayToUtcDate(a) +} + +// Diffing (all return floats) +// TODO: why not use ranges? + +export function diffWeeks(m0, m1) { + return diffDays(m0, m1) / 7 +} + +export function diffDays(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24) +} + +export function diffHours(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60) +} + +export function diffMinutes(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / (1000 * 60) +} + +export function diffSeconds(m0, m1) { + return (m1.valueOf() - m0.valueOf()) / 1000 +} + +export function diffDayAndTime(m0: DateMarker, m1: DateMarker): Duration { + let m0day = startOfDay(m0) + let m1day = startOfDay(m1) + + return { + years: 0, + months: 0, + days: Math.round(diffDays(m0day, m1day)), + milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf()), + } +} + +// Diffing Whole Units + +export function diffWholeWeeks(m0: DateMarker, m1: DateMarker): number { + let d = diffWholeDays(m0, m1) + + if (d !== null && d % 7 === 0) { + return d / 7 + } + + return null +} + +export function diffWholeDays(m0: DateMarker, m1: DateMarker): number { + if (timeAsMs(m0) === timeAsMs(m1)) { + return Math.round(diffDays(m0, m1)) + } + return null +} + +// Start-Of + +export function startOfDay(m: DateMarker): DateMarker { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + ]) +} + +export function startOfHour(m: DateMarker) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + ]) +} + +export function startOfMinute(m: DateMarker) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes(), + ]) +} + +export function startOfSecond(m: DateMarker) { + return arrayToUtcDate([ + m.getUTCFullYear(), + m.getUTCMonth(), + m.getUTCDate(), + m.getUTCHours(), + m.getUTCMinutes(), + m.getUTCSeconds(), + ]) +} + +// Week Computation + +export function weekOfYear(marker, dow, doy) { + let y = marker.getUTCFullYear() + let w = weekOfGivenYear(marker, y, dow, doy) + + if (w < 1) { + return weekOfGivenYear(marker, y - 1, dow, doy) + } + + let nextW = weekOfGivenYear(marker, y + 1, dow, doy) + if (nextW >= 1) { + return Math.min(w, nextW) + } + + return w +} + +function weekOfGivenYear(marker, year, dow, doy) { + let firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]) + let dayStart = startOfDay(marker) + let days = Math.round(diffDays(firstWeekStart, dayStart)) + + return Math.floor(days / 7) + 1 // zero-indexed +} + +// start-of-first-week - start-of-year +function firstWeekOffset(year, dow, doy) { + // first-week day -- which january is always in the first week (4 for iso, 1 for other) + let fwd = 7 + dow - doy + + // first-week day local weekday -- which local weekday is fwd + let fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7 + + return -fwdlw + fwd - 1 +} + +// Array Conversion + +export function dateToLocalArray(date) { + return [ + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds(), + ] +} + +export function arrayToLocalDate(a) { + return new Date( + a[0], + a[1] || 0, + a[2] == null ? 1 : a[2], // day of month + a[3] || 0, + a[4] || 0, + a[5] || 0, + ) +} + +export function dateToUtcArray(date) { + return [ + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds(), + ] +} + +export function arrayToUtcDate(a) { + // according to web standards (and Safari), a month index is required. + // massage if only given a year. + if (a.length === 1) { + a = a.concat([0]) + } + + return new Date(Date.UTC(...(a as [any, any]))) +} + +// Other Utils + +export function isValidDate(m: DateMarker) { + return !isNaN(m.valueOf()) +} + +export function timeAsMs(m: DateMarker) { + return m.getUTCHours() * 1000 * 60 * 60 + + m.getUTCMinutes() * 1000 * 60 + + m.getUTCSeconds() * 1000 + + m.getUTCMilliseconds() +} diff --git a/fullcalendar-main/packages/core/src/datelib/parsing.ts b/fullcalendar-main/packages/core/src/datelib/parsing.ts new file mode 100644 index 0000000..2b6584a --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/parsing.ts @@ -0,0 +1,38 @@ +import { isValidDate } from './marker.js' + +const ISO_RE = /^\s*(\d{4})(-?(\d{2})(-?(\d{2})([T ](\d{2}):?(\d{2})(:?(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/ + +export function parse(str) { + let m = ISO_RE.exec(str) + + if (m) { + let marker = new Date(Date.UTC( + Number(m[1]), + m[3] ? Number(m[3]) - 1 : 0, + Number(m[5] || 1), + Number(m[7] || 0), + Number(m[8] || 0), + Number(m[10] || 0), + m[12] ? Number(`0.${m[12]}`) * 1000 : 0, + )) + + if (isValidDate(marker)) { + let timeZoneOffset = null + + if (m[13]) { + timeZoneOffset = (m[15] === '-' ? -1 : 1) * ( + Number(m[16] || 0) * 60 + + Number(m[18] || 0) + ) + } + + return { + marker, + isTimeUnspecified: !m[6], + timeZoneOffset, + } + } + } + + return null +} diff --git a/fullcalendar-main/packages/core/src/datelib/timezone.ts b/fullcalendar-main/packages/core/src/datelib/timezone.ts new file mode 100644 index 0000000..7c34d8a --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/timezone.ts @@ -0,0 +1,13 @@ +export abstract class NamedTimeZoneImpl { + timeZoneName: string + + constructor(timeZoneName: string) { + this.timeZoneName = timeZoneName + } + + abstract offsetForArray(a: number[]): number + + abstract timestampToArray(ms: number): number[] +} + +export type NamedTimeZoneImplClass = { new(timeZoneName: string): NamedTimeZoneImpl } diff --git a/fullcalendar-main/packages/core/src/datelib/zoned-marker.ts b/fullcalendar-main/packages/core/src/datelib/zoned-marker.ts new file mode 100644 index 0000000..4d7d62f --- /dev/null +++ b/fullcalendar-main/packages/core/src/datelib/zoned-marker.ts @@ -0,0 +1,35 @@ +import { DateMarker } from './marker.js' +import { CalendarSystem } from './calendar-system.js' + +export interface ZonedMarker { + marker: DateMarker, + timeZoneOffset: number +} + +export interface ExpandedZonedMarker extends ZonedMarker { + array: number[], + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + millisecond: number +} + +export function expandZonedMarker(dateInfo: ZonedMarker, calendarSystem: CalendarSystem): ExpandedZonedMarker { + let a = calendarSystem.markerToArray(dateInfo.marker) + + return { + marker: dateInfo.marker, + timeZoneOffset: dateInfo.timeZoneOffset, + array: a, + year: a[0], + month: a[1], + day: a[2], + hour: a[3], + minute: a[4], + second: a[5], + millisecond: a[6], + } +} diff --git a/fullcalendar-main/packages/core/src/dates-set.ts b/fullcalendar-main/packages/core/src/dates-set.ts new file mode 100644 index 0000000..bc665da --- /dev/null +++ b/fullcalendar-main/packages/core/src/dates-set.ts @@ -0,0 +1,13 @@ +import { DateProfile } from './DateProfileGenerator.js' +import { CalendarData } from './reducers/data-types.js' +import { RangeApiWithTimeZone, buildRangeApiWithTimeZone } from './structs/date-span.js' +import { ViewApi } from './api/ViewApi.js' + +export type DatesSetArg = RangeApiWithTimeZone & { view: ViewApi } + +export function handleDateProfile(dateProfile: DateProfile, context: CalendarData) { + context.emitter.trigger('datesSet', { + ...buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv), + view: context.viewApi, + }) +} diff --git a/fullcalendar-main/packages/core/src/event-crud.ts b/fullcalendar-main/packages/core/src/event-crud.ts new file mode 100644 index 0000000..ce41a37 --- /dev/null +++ b/fullcalendar-main/packages/core/src/event-crud.ts @@ -0,0 +1,40 @@ +import { EventStore } from './structs/event-store.js' +import { CalendarData } from './reducers/data-types.js' +import { EventImpl, buildEventApis } from './api/EventImpl.js' +import { Duration } from './datelib/duration.js' +import { ViewApi } from './index.js' + +export interface EventAddArg { + event: EventImpl + relatedEvents: EventImpl[] + revert: () => void +} + +export interface EventChangeArg { + oldEvent: EventImpl + event: EventImpl + relatedEvents: EventImpl[] + revert: () => void +} + +export interface EventDropArg extends EventChangeArg { // not best place. deals w/ UI + el: HTMLElement + delta: Duration + jsEvent: MouseEvent + view: ViewApi + // and other "transformed" things +} + +export interface EventRemoveArg { + event: EventImpl + relatedEvents: EventImpl[] + revert: () => void +} + +export function handleEventStore(eventStore: EventStore, context: CalendarData) { + let { emitter } = context + + if (emitter.hasHandlers('eventsSet')) { + emitter.trigger('eventsSet', buildEventApis(eventStore, context)) + } +} diff --git a/fullcalendar-main/packages/core/src/event-sources/array-event-source.ts b/fullcalendar-main/packages/core/src/event-sources/array-event-source.ts new file mode 100644 index 0000000..6de2e52 --- /dev/null +++ b/fullcalendar-main/packages/core/src/event-sources/array-event-source.ts @@ -0,0 +1,25 @@ +import { createPlugin } from '../plugin-system.js' +import { EventSourceDef } from '../structs/event-source-def.js' +import { EventInput } from '../structs/event-parse.js' + +let eventSourceDef: EventSourceDef<EventInput[]> = { + ignoreRange: true, + + parseMeta(refined) { + if (Array.isArray(refined.events)) { + return refined.events + } + return null + }, + + fetch(arg, successCallback) { + successCallback({ + rawEvents: arg.eventSource.meta, + }) + }, +} + +export const arrayEventSourcePlugin = createPlugin({ + name: 'array-event-source', + eventSourceDefs: [eventSourceDef], +}) diff --git a/fullcalendar-main/packages/core/src/event-sources/func-event-source.ts b/fullcalendar-main/packages/core/src/event-sources/func-event-source.ts new file mode 100644 index 0000000..63176c4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/event-sources/func-event-source.ts @@ -0,0 +1,48 @@ +import { EventSourceDef } from '../structs/event-source-def.js' +import { EventInput } from '../structs/event-parse.js' +import { createPlugin } from '../plugin-system.js' +import { buildRangeApiWithTimeZone } from '../structs/date-span.js' +import { unpromisify } from '../util/promise.js' + +export type EventSourceFuncArg = { + start: Date + end: Date + startStr: string + endStr: string + timeZone: string +} + +export type EventSourceFunc = + (( + arg: EventSourceFuncArg, + successCallback: (eventInputs: EventInput[]) => void, + failureCallback: (error: Error) => void, + ) => void) | + ((arg: EventSourceFuncArg) => Promise<EventInput[]>) + +let eventSourceDef: EventSourceDef<EventSourceFunc> = { + + parseMeta(refined) { + if (typeof refined.events === 'function') { + return refined.events + } + return null + }, + + fetch(arg, successCallback, errorCallback) { + const { dateEnv } = arg.context + const func = arg.eventSource.meta + + unpromisify( + func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), + (rawEvents) => successCallback({ rawEvents }), + errorCallback, + ) + }, + +} + +export const funcEventSourcePlugin = createPlugin({ + name: 'func-event-source', + eventSourceDefs: [eventSourceDef], +}) diff --git a/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source-refiners.ts b/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source-refiners.ts new file mode 100644 index 0000000..c93906f --- /dev/null +++ b/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source-refiners.ts @@ -0,0 +1,9 @@ +import { identity, Identity, Dictionary } from '../options.js' + +export const JSON_FEED_EVENT_SOURCE_REFINERS = { + method: String, + extraParams: identity as Identity<Dictionary | (() => Dictionary)>, + startParam: String, + endParam: String, + timeZoneParam: String, +} diff --git a/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source.ts b/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source.ts new file mode 100644 index 0000000..b3a1580 --- /dev/null +++ b/fullcalendar-main/packages/core/src/event-sources/json-feed-event-source.ts @@ -0,0 +1,102 @@ +import { requestJson } from '../util/requestJson.js' +import { CalendarContext } from '../CalendarContext.js' +import { EventSourceDef } from '../structs/event-source-def.js' +import { DateRange } from '../datelib/date-range.js' +import { createPlugin } from '../plugin-system.js' +import { JSON_FEED_EVENT_SOURCE_REFINERS } from './json-feed-event-source-refiners.js' +import { EventInput } from '../api/structs.js' + +interface JsonFeedMeta { + url: string + format: 'json' // for EventSourceImpl + method: string + extraParams?: any + startParam?: string + endParam?: string + timeZoneParam?: string +} + +let eventSourceDef: EventSourceDef<JsonFeedMeta> = { + + parseMeta(refined) { + if (refined.url && (refined.format === 'json' || !refined.format)) { + return { + url: refined.url, + format: 'json', + method: (refined.method || 'GET').toUpperCase(), + extraParams: refined.extraParams, + startParam: refined.startParam, + endParam: refined.endParam, + timeZoneParam: refined.timeZoneParam, + } + } + return null + }, + + fetch(arg, successCallback, errorCallback) { + const { meta } = arg.eventSource + const requestParams = buildRequestParams(meta, arg.range, arg.context) + + requestJson( + meta.method, + meta.url, + requestParams, + ).then( + ([rawEvents, response]: [EventInput[], Response]) => { + successCallback({ rawEvents, response }) + }, + errorCallback, + ) + }, + +} + +export const jsonFeedEventSourcePlugin = createPlugin({ + name: 'json-event-source', + eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS, + eventSourceDefs: [eventSourceDef], +}) + +function buildRequestParams(meta: JsonFeedMeta, range: DateRange, context: CalendarContext) { + let { dateEnv, options } = context + let startParam + let endParam + let timeZoneParam + let customRequestParams + let params = {} + + startParam = meta.startParam + if (startParam == null) { + startParam = options.startParam + } + + endParam = meta.endParam + if (endParam == null) { + endParam = options.endParam + } + + timeZoneParam = meta.timeZoneParam + if (timeZoneParam == null) { + timeZoneParam = options.timeZoneParam + } + + // retrieve any outbound GET/POST data from the options + if (typeof meta.extraParams === 'function') { + // supplied as a function that returns a key/value object + customRequestParams = meta.extraParams() + } else { + // probably supplied as a straight key/value object + customRequestParams = meta.extraParams || {} + } + + Object.assign(params, customRequestParams) + + params[startParam] = dateEnv.formatIso(range.start) + params[endParam] = dateEnv.formatIso(range.end) + + if (dateEnv.timeZone !== 'local') { + params[timeZoneParam] = dateEnv.timeZone + } + + return params +} diff --git a/fullcalendar-main/packages/core/src/formatting-api.ts b/fullcalendar-main/packages/core/src/formatting-api.ts new file mode 100644 index 0000000..a88c7c7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/formatting-api.ts @@ -0,0 +1,65 @@ +import { DateEnv } from './datelib/env.js' +import { createFormatter } from './datelib/formatting.js' +import { NativeFormatterOptions } from './datelib/formatting-native.js' +import { organizeRawLocales, buildLocale } from './datelib/locale.js' +import { BASE_OPTION_DEFAULTS } from './options.js' + +// public +import { DateInput } from './api/structs.js' + +export interface FormatDateOptions extends NativeFormatterOptions { + locale?: string +} + +export interface FormatRangeOptions extends FormatDateOptions { + separator?: string + isEndExclusive?: boolean +} + +export function formatDate(dateInput: DateInput, options: FormatDateOptions = {}) { + let dateEnv = buildDateEnv(options) + let formatter = createFormatter(options) + let dateMeta = dateEnv.createMarkerMeta(dateInput) + + if (!dateMeta) { // TODO: warning? + return '' + } + + return dateEnv.format(dateMeta.marker, formatter, { + forcedTzo: dateMeta.forcedTzo, + }) +} + +export function formatRange( + startInput: DateInput, + endInput: DateInput, + options: FormatRangeOptions, // mixture of env and formatter settings +) { + let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}) // pass in if non-null object + let formatter = createFormatter(options) + let startMeta = dateEnv.createMarkerMeta(startInput) + let endMeta = dateEnv.createMarkerMeta(endInput) + + if (!startMeta || !endMeta) { // TODO: warning? + return '' + } + + return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { + forcedStartTzo: startMeta.forcedTzo, + forcedEndTzo: endMeta.forcedTzo, + isEndExclusive: options.isEndExclusive, + defaultSeparator: BASE_OPTION_DEFAULTS.defaultRangeSeparator, + }) +} + +// TODO: more DRY and optimized +function buildDateEnv(settings: FormatRangeOptions) { + let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map) // TODO: don't hardcode 'en' everywhere + + return new DateEnv({ + timeZone: BASE_OPTION_DEFAULTS.timeZone, + calendarSystem: 'gregory', + ...settings, + locale, + }) +} diff --git a/fullcalendar-main/packages/core/src/global-config.ts b/fullcalendar-main/packages/core/src/global-config.ts new file mode 100644 index 0000000..7f54e1f --- /dev/null +++ b/fullcalendar-main/packages/core/src/global-config.ts @@ -0,0 +1,3 @@ +// TODO: get rid of this in favor of options system, +// tho it's really easy to access this globally rather than pass thru options. +export const config = {} as any diff --git a/fullcalendar-main/packages/core/src/global-locales.ts b/fullcalendar-main/packages/core/src/global-locales.ts new file mode 100644 index 0000000..90c0089 --- /dev/null +++ b/fullcalendar-main/packages/core/src/global-locales.ts @@ -0,0 +1,3 @@ +import { LocaleInput } from './datelib/locale.js' + +export const globalLocales: LocaleInput[] = [] diff --git a/fullcalendar-main/packages/core/src/global-plugins.ts b/fullcalendar-main/packages/core/src/global-plugins.ts new file mode 100644 index 0000000..05e601f --- /dev/null +++ b/fullcalendar-main/packages/core/src/global-plugins.ts @@ -0,0 +1,33 @@ +import { PluginDef } from './plugin-system-struct.js' +import { createPlugin } from './plugin-system.js' +import { arrayEventSourcePlugin } from './event-sources/array-event-source.js' +import { funcEventSourcePlugin } from './event-sources/func-event-source.js' +import { jsonFeedEventSourcePlugin } from './event-sources/json-feed-event-source.js' +import { simpleRecurringEventsPlugin } from './structs/recurring-event-simple.js' +import { changeHandlerPlugin } from './option-change-handlers.js' +import { handleDateProfile } from './dates-set.js' +import { handleEventStore } from './event-crud.js' +import { computeEventSourcesLoading } from './reducers/eventSources.js' +import { CalendarDataManagerState } from './reducers/data-types.js' + +/* +this array is exposed on the root namespace so that UMD plugins can add to it. +see the rollup-bundles script. +*/ +export const globalPlugins: PluginDef[] = [ + arrayEventSourcePlugin, + funcEventSourcePlugin, + jsonFeedEventSourcePlugin, + simpleRecurringEventsPlugin, + changeHandlerPlugin, + createPlugin({ + name: 'misc', + isLoadingFuncs: [ + (state: CalendarDataManagerState) => computeEventSourcesLoading(state.eventSources), + ], + propSetHandlers: { + dateProfile: handleDateProfile, + eventStore: handleEventStore, + }, + }), +] diff --git a/fullcalendar-main/packages/core/src/index.css b/fullcalendar-main/packages/core/src/index.css new file mode 100644 index 0000000..93e5ab8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/index.css @@ -0,0 +1,21 @@ + +@import './styles/mixins'; +@import './styles/vars'; + +@import './styles/page-root'; +@import './styles/calendar-root'; + +@import './styles/icons'; +@import './styles/button'; +@import './styles/button-group'; +@import './styles/toolbar'; +@import './styles/scroller'; +@import './styles/scroller-harness'; +@import './styles/scrollgrid'; +@import './styles/sticky'; +@import './styles/view-harness'; +@import './styles/col-header'; +@import './styles/bg'; +@import './styles/event'; +@import './styles/h-event'; +@import './styles/popover'; diff --git a/fullcalendar-main/packages/core/src/index.global.ts b/fullcalendar-main/packages/core/src/index.global.ts new file mode 100644 index 0000000..e8d7b42 --- /dev/null +++ b/fullcalendar-main/packages/core/src/index.global.ts @@ -0,0 +1,5 @@ +import * as Internal from './internal.js' +import * as Preact from './preact.js' + +export * from './index.js' +export { Internal, Preact } diff --git a/fullcalendar-main/packages/core/src/index.ts b/fullcalendar-main/packages/core/src/index.ts new file mode 100644 index 0000000..94ec1f0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/index.ts @@ -0,0 +1,21 @@ +import './index.css' + +export { Calendar } from './Calendar.js' + +export { CalendarApi } from './api/CalendarApi.js' +export { ViewApi } from './api/ViewApi.js' +export { EventSourceApi } from './api/EventSourceApi.js' +export { EventApi } from './api/EventApi.js' +export * from './api/structs.js' + +export { FormatDateOptions, FormatRangeOptions } from './formatting-api.js' +export { formatDate, formatRange } from './formatting-api.js' +export { createPlugin } from './plugin-system.js' +export { sliceEvents } from './View.js' +export { EventRenderRange } from './component/event-rendering.js' // for sliceEvents +export { JsonRequestError } from './util/requestJson.js' + +export { globalLocales } from './global-locales.js' +export { globalPlugins } from './global-plugins.js' + +export const version: string = '<%= pkgVersion %>' diff --git a/fullcalendar-main/packages/core/src/interactions/ElementDragging.ts b/fullcalendar-main/packages/core/src/interactions/ElementDragging.ts new file mode 100644 index 0000000..833967f --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/ElementDragging.ts @@ -0,0 +1,42 @@ +import { Emitter } from '../common/Emitter.js' + +/* +An abstraction for a dragging interaction originating on an event. +Does higher-level things than PointerDragger, such as possibly: +- a "mirror" that moves with the pointer +- a minimum number of pixels or other criteria for a true drag to begin + +subclasses must emit: +- pointerdown +- dragstart +- dragmove +- pointerup +- dragend +*/ +export abstract class ElementDragging { // TODO: rename to *Interface? + emitter: Emitter<any> + + constructor(el: HTMLElement, selector?: string) { + this.emitter = new Emitter() + } + + destroy() { + } + + // if given true, should prevent dragstart+dragmove+dragend from firing + abstract setIgnoreMove(bool: boolean): void + + setMirrorIsVisible(bool: boolean) { + // optional if subclass doesn't want to support a mirror + } + + setMirrorNeedsRevert(bool: boolean) { + // optional if subclass doesn't want to support a mirror + } + + setAutoScrollEnabled(bool: boolean) { + // optional + } +} + +export type ElementDraggingClass = { new(el: HTMLElement, selector?: string): ElementDragging } diff --git a/fullcalendar-main/packages/core/src/interactions/EventClicking.ts b/fullcalendar-main/packages/core/src/interactions/EventClicking.ts new file mode 100644 index 0000000..1c4bfb5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/EventClicking.ts @@ -0,0 +1,60 @@ +import { listenBySelector } from '../util/dom-event.js' +import { EventImpl } from '../api/EventImpl.js' +import { elementClosest } from '../util/dom-manip.js' +import { getElSeg } from '../component/event-rendering.js' +import { Interaction, InteractionSettings } from './interaction.js' +import { ViewApi } from '../api/ViewApi.js' + +export interface EventClickArg { + el: HTMLElement + event: EventImpl + jsEvent: MouseEvent + view: ViewApi +} + +/* +Detects when the user clicks on an event within a DateComponent +*/ +export class EventClicking extends Interaction { + constructor(settings: InteractionSettings) { + super(settings) + + this.destroy = listenBySelector( + settings.el, + 'click', + '.fc-event', // on both fg and bg events + this.handleSegClick, + ) + } + + handleSegClick = (ev: Event, segEl: HTMLElement) => { + let { component } = this + let { context } = component + let seg = getElSeg(segEl) + + if ( + seg && // might be the <div> surrounding the more link + component.isValidSegDownEl(ev.target as HTMLElement) + ) { + // our way to simulate a link click for elements that can't be <a> tags + // grab before trigger fired in case trigger trashes DOM thru rerendering + let hasUrlContainer = elementClosest(ev.target as HTMLElement, '.fc-event-forced-url') + let url = hasUrlContainer ? (hasUrlContainer.querySelector('a[href]') as any).href : '' + + context.emitter.trigger('eventClick', { + el: segEl, + event: new EventImpl( + component.context, + seg.eventRange.def, + seg.eventRange.instance, + ), + jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655 + view: context.viewApi, + } as EventClickArg) + + if (url && !ev.defaultPrevented) { + window.location.href = url + } + } + } +} diff --git a/fullcalendar-main/packages/core/src/interactions/EventHovering.ts b/fullcalendar-main/packages/core/src/interactions/EventHovering.ts new file mode 100644 index 0000000..fdd889d --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/EventHovering.ts @@ -0,0 +1,77 @@ +import { listenToHoverBySelector } from '../util/dom-event.js' +import { EventImpl } from '../api/EventImpl.js' +import { getElSeg } from '../component/event-rendering.js' +import { Interaction, InteractionSettings } from './interaction.js' +import { ViewApi } from '../api/ViewApi.js' + +export interface EventHoveringArg { + el: HTMLElement + event: EventImpl + jsEvent: MouseEvent + view: ViewApi +} + +/* +Triggers events and adds/removes core classNames when the user's pointer +enters/leaves event-elements of a component. +*/ +export class EventHovering extends Interaction { + removeHoverListeners: () => void + + currentSegEl: HTMLElement + + constructor(settings: InteractionSettings) { + super(settings) + + this.removeHoverListeners = listenToHoverBySelector( + settings.el, + '.fc-event', // on both fg and bg events + this.handleSegEnter, + this.handleSegLeave, + ) + } + + destroy() { + this.removeHoverListeners() + } + + // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it + handleEventElRemove = (el: HTMLElement) => { + if (el === this.currentSegEl) { + this.handleSegLeave(null, this.currentSegEl) + } + } + + handleSegEnter = (ev: Event, segEl: HTMLElement) => { + if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper + this.currentSegEl = segEl + this.triggerEvent('eventMouseEnter', ev, segEl) + } + } + + handleSegLeave = (ev: Event | null, segEl: HTMLElement) => { + if (this.currentSegEl) { + this.currentSegEl = null + this.triggerEvent('eventMouseLeave', ev, segEl) + } + } + + triggerEvent(publicEvName: 'eventMouseEnter' | 'eventMouseLeave', ev: Event | null, segEl: HTMLElement) { + let { component } = this + let { context } = component + let seg = getElSeg(segEl)! + + if (!ev || component.isValidSegDownEl(ev.target as HTMLElement)) { + context.emitter.trigger(publicEvName, { + el: segEl, + event: new EventImpl( + context, + seg.eventRange.def, + seg.eventRange.instance, + ), + jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655 + view: context.viewApi, + } as EventHoveringArg) + } + } +} diff --git a/fullcalendar-main/packages/core/src/interactions/date-selecting.ts b/fullcalendar-main/packages/core/src/interactions/date-selecting.ts new file mode 100644 index 0000000..5e7fb4c --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/date-selecting.ts @@ -0,0 +1,3 @@ +import { Hit } from './hit.js' + +export type dateSelectionJoinTransformer = (hit0: Hit, hit1: Hit) => any diff --git a/fullcalendar-main/packages/core/src/interactions/event-dragging.ts b/fullcalendar-main/packages/core/src/interactions/event-dragging.ts new file mode 100644 index 0000000..5d5a747 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/event-dragging.ts @@ -0,0 +1,10 @@ +import { EventMutation } from '../structs/event-mutation.js' +import { Hit } from './hit.js' +import { EventDef } from '../structs/event-def.js' +import { EventUi } from '../component/event-ui.js' +import { CalendarContext } from '../CalendarContext.js' +import { Dictionary } from '../options.js' + +export type eventDragMutationMassager = (mutation: EventMutation, hit0: Hit, hit1: Hit) => void +export type EventDropTransformers = (mutation: EventMutation, context: CalendarContext) => Dictionary +export type eventIsDraggableTransformer = (val: boolean, eventDef: EventDef, eventUi: EventUi, context: CalendarContext) => boolean diff --git a/fullcalendar-main/packages/core/src/interactions/event-interaction-state.ts b/fullcalendar-main/packages/core/src/interactions/event-interaction-state.ts new file mode 100644 index 0000000..6ea99e2 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/event-interaction-state.ts @@ -0,0 +1,7 @@ +import { EventStore } from '../structs/event-store.js' + +export interface EventInteractionState { // is this ever used alone? + affectedEvents: EventStore + mutatedEvents: EventStore + isEvent: boolean +} diff --git a/fullcalendar-main/packages/core/src/interactions/external-element-dragging.ts b/fullcalendar-main/packages/core/src/interactions/external-element-dragging.ts new file mode 100644 index 0000000..195f4a3 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/external-element-dragging.ts @@ -0,0 +1,4 @@ +import { DateSpan } from '../structs/date-span.js' +import { DragMeta } from '../structs/drag-meta.js' + +export type ExternalDefTransform = (dateSpan: DateSpan, dragMeta: DragMeta) => any diff --git a/fullcalendar-main/packages/core/src/interactions/hit.ts b/fullcalendar-main/packages/core/src/interactions/hit.ts new file mode 100644 index 0000000..618d4d8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/hit.ts @@ -0,0 +1,15 @@ +import { DateProfile } from '../DateProfileGenerator.js' +import { DateSpan } from '../structs/date-span.js' +import { Rect } from '../util/geom.js' +import { ViewContext } from '../ViewContext.js' + +export interface Hit { + componentId?: string // will be set by HitDragging + context?: ViewContext // will be set by HitDragging + dateProfile: DateProfile + dateSpan: DateSpan + dayEl: HTMLElement + rect: Rect + layer: number + largeUnit?: string // TODO: have timeline set this! +} diff --git a/fullcalendar-main/packages/core/src/interactions/interaction.ts b/fullcalendar-main/packages/core/src/interactions/interaction.ts new file mode 100644 index 0000000..de0ccb9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/interaction.ts @@ -0,0 +1,50 @@ +import { DateComponent } from '../component/DateComponent.js' +import { Hit } from './hit.js' + +export abstract class Interaction { + component: DateComponent<any> + isHitComboAllowed: ((hit0: Hit, hit1: Hit) => boolean) | null + + constructor(settings: InteractionSettings) { + this.component = settings.component + this.isHitComboAllowed = settings.isHitComboAllowed || null + } + + destroy() { + } +} + +export type InteractionClass = { new(settings: InteractionSettings): Interaction } + +export interface InteractionSettingsInput { + el: HTMLElement + useEventCenter?: boolean + isHitComboAllowed?: (hit0: Hit, hit1: Hit) => boolean +} + +export interface InteractionSettings { + component: DateComponent<any> + el: HTMLElement + useEventCenter: boolean + isHitComboAllowed: ((hit0: Hit, hit1: Hit) => boolean) | null +} + +export type InteractionSettingsStore = { [componenUid: string]: InteractionSettings } + +export function parseInteractionSettings(component: DateComponent<any>, input: InteractionSettingsInput): InteractionSettings { + return { + component, + el: input.el, + useEventCenter: input.useEventCenter != null ? input.useEventCenter : true, + isHitComboAllowed: input.isHitComboAllowed || null, + } +} + +export function interactionSettingsToStore(settings: InteractionSettings) { + return { + [settings.component.uid]: settings, + } +} + +// global state +export const interactionSettingsStore: InteractionSettingsStore = {} diff --git a/fullcalendar-main/packages/core/src/interactions/pointer.ts b/fullcalendar-main/packages/core/src/interactions/pointer.ts new file mode 100644 index 0000000..459ab06 --- /dev/null +++ b/fullcalendar-main/packages/core/src/interactions/pointer.ts @@ -0,0 +1,9 @@ +export interface PointerDragEvent { + origEvent: UIEvent + isTouch: boolean + subjectEl: EventTarget + pageX: number + pageY: number + deltaX: number + deltaY: number +} diff --git a/fullcalendar-main/packages/core/src/internal.ts b/fullcalendar-main/packages/core/src/internal.ts new file mode 100644 index 0000000..0603f54 --- /dev/null +++ b/fullcalendar-main/packages/core/src/internal.ts @@ -0,0 +1,273 @@ +import './index.css' + +export { + Identity, Dictionary, + BaseOptionRefiners, BaseOptionsRefined, + ViewOptionsRefined, RawOptionsFromRefiners, RefinedOptionsFromRefiners, + CalendarListenerRefiners, + BASE_OPTION_DEFAULTS, identity, refineProps, +} from './options.js' + +export type { EventDef, EventDefHash } from './structs/event-def.js' +export type { EventInstance, EventInstanceHash } from './structs/event-instance.js' +export { createEventInstance } from './structs/event-instance.js' +export type { EventRefined, EventTuple, EventRefiners } from './structs/event-parse.js' +export { parseEventDef, refineEventDef } from './structs/event-parse.js' +export { parseBusinessHours } from './structs/business-hours.js' + +export type { OrderSpec } from './util/misc.js' +export { + padStart, + isInt, + parseFieldSpecs, + compareByFieldSpecs, + flexibleCompare, + preventSelection, allowSelection, preventContextMenu, allowContextMenu, + compareNumbers, enableCursor, disableCursor, + guid, +} from './util/misc.js' + +export { + computeVisibleDayRange, + isMultiDayRange, + diffDates, +} from './util/date.js' + +export { + removeExact, + isArraysEqual, +} from './util/array.js' + +export type { MemoizeHashFunc, MemoiseArrayFunc } from './util/memoize.js' +export { memoize, memoizeObjArg, memoizeArraylike, memoizeHashlike } from './util/memoize.js' + +export type { Rect, Point } from './util/geom.js' +export { + intersectRects, + pointInsideRect, + constrainPoint, + getRectCenter, diffPoints, + translateRect, +} from './util/geom.js' + +export { mapHash, filterHash, isPropsEqual, compareObjs, collectFromHash } from './util/object.js' + +export { + findElements, + findDirectChildren, + removeElement, + applyStyle, + elementMatches, + elementClosest, + getEventTargetViaRoot, + getUniqueDomId, +} from './util/dom-manip.js' +export { parseClassNames } from './util/html.js' + +export { getCanVGrowWithinCell } from './util/table-styling.js' + +export type { EventStore } from './structs/event-store.js' +export { + createEmptyEventStore, + mergeEventStores, + getRelevantEvents, + eventTupleToStore, +} from './structs/event-store.js' +export type { EventUiHash, EventUi } from './component/event-ui.js' +export { combineEventUis, createEventUi } from './component/event-ui.js' +export type { SplittableProps } from './component/event-splitting.js' +export { Splitter } from './component/event-splitting.js' +export { getDayClassNames, getDateMeta, getSlotClassNames } from './component/date-rendering.js' +export { buildNavLinkAttrs } from './common/nav-link.js' + +export { + preventDefault, + whenTransitionDone, +} from './util/dom-event.js' + +export { + computeInnerRect, + computeEdges, + getClippingParents, + computeRect, +} from './util/dom-geom.js' + +export { unpromisify } from './util/promise.js' + +export { Emitter } from './common/Emitter.js' +export type { DateRange } from './datelib/date-range.js' +export { rangeContainsMarker, intersectRanges, rangesEqual, rangesIntersect, rangeContainsRange } from './datelib/date-range.js' +export { PositionCache } from './common/PositionCache.js' +export { ScrollController, ElementScrollController, WindowScrollController } from './common/scroll-controller.js' +export { Theme } from './theme/Theme.js' +export type { ViewContext } from './ViewContext.js' +export { ViewContextType } from './ViewContext.js' +export type { Seg, EventSegUiInteractionState } from './component/DateComponent.js' +export { DateComponent } from './component/DateComponent.js' +export type { CalendarData } from './reducers/data-types.js' +export type { ViewProps } from './View.js' + +export type { DateProfile } from './DateProfileGenerator.js' +export { DateProfileGenerator } from './DateProfileGenerator.js' +export type { ViewSpec } from './structs/view-spec.js' +export type { DateSpan } from './structs/date-span.js' +export { isDateSpansEqual } from './structs/date-span.js' + +export type { DateMarker } from './datelib/marker.js' +export { + addDays, + startOfDay, + addMs, + addWeeks, + diffWeeks, + diffWholeWeeks, + diffWholeDays, + diffDayAndTime, + diffDays, + isValidDate, +} from './datelib/marker.js' +export { + createDuration, + asCleanDays, multiplyDuration, addDurations, + asRoughMinutes, asRoughSeconds, asRoughMs, + wholeDivideDurations, greatestDurationDenominator, +} from './datelib/duration.js' +export { DateEnv } from './datelib/env.js' + +export { createFormatter } from './datelib/formatting.js' +export type { DateFormatter, VerboseFormattingArg } from './datelib/DateFormatter.js' +export { + formatIsoTimeString, + formatDayString, + buildIsoString, + formatIsoMonthStr, +} from './datelib/formatting-utils.js' +export { NamedTimeZoneImpl } from './datelib/timezone.js' +export { parse as parseMarker } from './datelib/parsing.js' + +export type { EventSourceDef } from './structs/event-source-def.js' +export type { EventSourceRefined } from './structs/event-source-parse.js' +export { EventSourceRefiners } from './structs/event-source-parse.js' + +export type { SegSpan, SegRect, SegEntry, SegInsertion, SegEntryGroup } from './seg-hierarchy.js' +export { + SegHierarchy, buildEntryKey, getEntrySpanEnd, binarySearch, groupIntersectingEntries, + intersectSpans, +} from './seg-hierarchy.js' + +export type { InteractionSettings, InteractionSettingsStore } from './interactions/interaction.js' +export { + Interaction, + interactionSettingsToStore, + interactionSettingsStore, +} from './interactions/interaction.js' +export type { PointerDragEvent } from './interactions/pointer.js' +export type { Hit } from './interactions/hit.js' +export type { dateSelectionJoinTransformer } from './interactions/date-selecting.js' +export type { eventDragMutationMassager, EventDropTransformers } from './interactions/event-dragging.js' +export { ElementDragging } from './interactions/ElementDragging.js' + +export { config } from './global-config.js' + +export type { RecurringType } from './structs/recurring-event.js' + +export type { DragMetaInput, DragMeta } from './structs/drag-meta.js' +export { parseDragMeta } from './structs/drag-meta.js' + +export type { ViewPropsTransformer, PluginDef } from './plugin-system-struct.js' +export type { Action } from './reducers/Action.js' +export type { CalendarContext } from './CalendarContext.js' +export type { CalendarContentProps } from './CalendarContent.js' +export { CalendarRoot } from './CalendarRoot.js' + +export { DayHeader } from './common/DayHeader.js' +export { computeFallbackHeaderFormat } from './common/table-utils.js' +export { TableDateCell } from './common/TableDateCell.js' +export { TableDowCell } from './common/TableDowCell.js' + +export { DaySeriesModel } from './common/DaySeriesModel.js' + +export type { EventInteractionState } from './interactions/event-interaction-state.js' +export { + sliceEventStore, hasBgRendering, getElSeg, + buildSegTimeText, + sortEventSegs, + getSegMeta, buildEventRangeKey, + getSegAnchorAttrs, +} from './component/event-rendering.js' + +export type { DayTableCell } from './common/DayTableModel.js' +export { DayTableModel } from './common/DayTableModel.js' + +export type { SlicedProps } from './common/slicing-utils.js' +export { Slicer } from './common/slicing-utils.js' + +export type { EventMutation } from './structs/event-mutation.js' +export { applyMutationToEventStore } from './structs/event-mutation.js' +export type { Constraint } from './structs/constraint.js' +export { isPropsValid, isInteractionValid, isDateSelectionValid } from './validation.js' + +export { requestJson } from './util/requestJson.js' + +export { BaseComponent, setRef } from './vdom-util.js' +export { DelayedRunner } from './util/DelayedRunner.js' + +export type { + ScrollGridProps, + ScrollGridSectionConfig, + ColGroupConfig, + ScrollGridChunkConfig, +} from './scrollgrid/ScrollGridImpl.js' +export type { SimpleScrollGridSection } from './scrollgrid/SimpleScrollGrid.js' +export { SimpleScrollGrid } from './scrollgrid/SimpleScrollGrid.js' +export type { + ScrollerLike, ColProps, ChunkContentCallbackArgs, + ChunkConfigRowContent, ChunkConfigContent, +} from './scrollgrid/util.js' +export { + hasShrinkWidth, renderMicroColGroup, + getScrollGridClassNames, getSectionClassNames, getSectionHasLiquidHeight, getAllowYScrolling, renderChunkContent, computeShrinkWidth, + sanitizeShrinkWidth, + isColPropsEqual, + renderScrollShim, + getStickyFooterScrollbar, + getStickyHeaderDates, +} from './scrollgrid/util.js' +export type { OverflowValue } from './scrollgrid/Scroller.js' +export { Scroller } from './scrollgrid/Scroller.js' +export { getScrollbarWidths } from './util/scrollbar-width.js' +export { RefMap } from './util/RefMap.js' +export { getIsRtlScrollbarOnLeft } from './util/scrollbar-side.js' + +export { NowTimer } from './NowTimer.js' +export type { ScrollRequest } from './ScrollResponder.js' +export { ScrollResponder } from './ScrollResponder.js' +export type { + CustomContentGenerator, DidMountHandler, WillUnmountHandler, MountArg, +} from './common/render-hook.js' +export { StandardEvent } from './common/StandardEvent.js' +export { NowIndicatorContainer } from './common/NowIndicatorContainer.js' + +export { DayCellContainer, hasCustomDayCellContent } from './common/DayCellContainer.js' +export type { MinimalEventProps } from './common/EventContainer.js' +export { EventContainer } from './common/EventContainer.js' +export { renderFill, BgEvent } from './common/bg-fill.js' +export { WeekNumberContainerProps, WeekNumberContainer } from './common/WeekNumberContainer.js' +export { MoreLinkContainer, computeEarliestSegStart } from './common/MoreLinkContainer.js' + +export type { ViewContainerProps } from './common/ViewContainer.js' +export { ViewContainer } from './common/ViewContainer.js' +export type { DatePointTransform, DateSpanTransform } from './calendar-utils.js' +export { triggerDateSelect, getDefaultEventEnd } from './calendar-utils.js' + +export { injectStyles } from './styleUtils.js' + +export { CalendarImpl } from './api/CalendarImpl.js' +export { EventImpl, buildEventApis } from './api/EventImpl.js' + +export type { ElProps } from './content-inject/ContentInjector.js' +export { buildElAttrs } from './content-inject/ContentInjector.js' +export type { InnerContainerFunc } from './content-inject/ContentContainer.js' +export { ContentContainer } from './content-inject/ContentContainer.js' +export type { CustomRendering } from './content-inject/CustomRenderingStore.js' +export { CustomRenderingStore } from './content-inject/CustomRenderingStore.js' diff --git a/fullcalendar-main/packages/core/src/locales-all.global.ts b/fullcalendar-main/packages/core/src/locales-all.global.ts new file mode 100644 index 0000000..8b0e5d7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales-all.global.ts @@ -0,0 +1,4 @@ +import { globalLocales } from './index.js' +import localesAll from './locales-all.js' + +globalLocales.push(...localesAll) diff --git a/fullcalendar-main/packages/core/src/locales-all.js.tpl b/fullcalendar-main/packages/core/src/locales-all.js.tpl new file mode 100644 index 0000000..35d0d22 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales-all.js.tpl @@ -0,0 +1,7 @@ +{{#each localeCodes}} +import l{{@index}} from './locales/{{this}}.js' +{{/each}} + +export default [ + {{#each localeCodes}}l{{@index}}, {{/each}} +] diff --git a/fullcalendar-main/packages/core/src/locales-all.ts b/fullcalendar-main/packages/core/src/locales-all.ts new file mode 100644 index 0000000..085728a --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales-all.ts @@ -0,0 +1,5 @@ +import { LocaleInput } from './index.js' + +declare const allLocales: LocaleInput[] + +export default allLocales diff --git a/fullcalendar-main/packages/core/src/locales/af.ts b/fullcalendar-main/packages/core/src/locales/af.ts new file mode 100644 index 0000000..80026a0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/af.ts @@ -0,0 +1,22 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'af', + week: { + dow: 1, // Maandag is die eerste dag van die week. + doy: 4, // Die week wat die 4de Januarie bevat is die eerste week van die jaar. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Agenda', + }, + allDayText: 'Heeldag', + moreLinkText: 'Addisionele', + noEventsText: 'Daar is geen gebeurtenisse nie', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-dz.ts b/fullcalendar-main/packages/core/src/locales/ar-dz.ts new file mode 100644 index 0000000..ad2bcd4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-dz.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-dz', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 4, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-kw.ts b/fullcalendar-main/packages/core/src/locales/ar-kw.ts new file mode 100644 index 0000000..75f302c --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-kw.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-kw', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-ly.ts b/fullcalendar-main/packages/core/src/locales/ar-ly.ts new file mode 100644 index 0000000..3e0f3bc --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-ly.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-ly', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-ma.ts b/fullcalendar-main/packages/core/src/locales/ar-ma.ts new file mode 100644 index 0000000..2d9f304 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-ma.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-ma', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-sa.ts b/fullcalendar-main/packages/core/src/locales/ar-sa.ts new file mode 100644 index 0000000..1685d2a --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-sa.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-sa', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar-tn.ts b/fullcalendar-main/packages/core/src/locales/ar-tn.ts new file mode 100644 index 0000000..3f56ab7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar-tn.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar-tn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ar.ts b/fullcalendar-main/packages/core/src/locales/ar.ts new file mode 100644 index 0000000..abca90b --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ar.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ar', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'السابق', + next: 'التالي', + today: 'اليوم', + year: 'سنة', + month: 'شهر', + week: 'أسبوع', + day: 'يوم', + list: 'أجندة', + }, + weekText: 'أسبوع', + allDayText: 'اليوم كله', + moreLinkText: 'أخرى', + noEventsText: 'أي أحداث لعرض', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/az.ts b/fullcalendar-main/packages/core/src/locales/az.ts new file mode 100644 index 0000000..21007b6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/az.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'az', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Əvvəl', + next: 'Sonra', + today: 'Bu Gün', + year: 'Il', + month: 'Ay', + week: 'Həftə', + day: 'Gün', + list: 'Gündəm', + }, + weekText: 'Həftə', + allDayText: 'Bütün Gün', + moreLinkText(n) { + return '+ daha çox ' + n + }, + noEventsText: 'Göstərmək üçün hadisə yoxdur', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/bg.ts b/fullcalendar-main/packages/core/src/locales/bg.ts new file mode 100644 index 0000000..8f4e495 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/bg.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'bg', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'назад', + next: 'напред', + today: 'днес', + year: 'година', + month: 'Месец', + week: 'Седмица', + day: 'Ден', + list: 'График', + }, + allDayText: 'Цял ден', + moreLinkText(n) { + return '+още ' + n + }, + noEventsText: 'Няма събития за показване', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/bn.ts b/fullcalendar-main/packages/core/src/locales/bn.ts new file mode 100644 index 0000000..28e8b4f --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/bn.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'bn', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'পেছনে', + next: 'সামনে', + today: 'আজ', + year: 'বছর', + month: 'মাস', + week: 'সপ্তাহ', + day: 'দিন', + list: 'তালিকা', + }, + weekText: 'সপ্তাহ', + allDayText: 'সারাদিন', + moreLinkText(n) { + return '+অন্যান্য ' + n + }, + noEventsText: 'কোনো ইভেন্ট নেই', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/bs.ts b/fullcalendar-main/packages/core/src/locales/bs.ts new file mode 100644 index 0000000..0bc42dd --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/bs.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'bs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prošli', + next: 'Sljedeći', + today: 'Danas', + year: 'Godina', + month: 'Mjesec', + week: 'Sedmica', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Sed', + allDayText: 'Cijeli dan', + moreLinkText(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikazivanje', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ca.ts b/fullcalendar-main/packages/core/src/locales/ca.ts new file mode 100644 index 0000000..48aad92 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ca.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ca', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Següent', + today: 'Avui', + year: 'Any', + month: 'Mes', + week: 'Setmana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Set', + allDayText: 'Tot el dia', + moreLinkText: 'més', + noEventsText: 'No hi ha esdeveniments per mostrar', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/cs.ts b/fullcalendar-main/packages/core/src/locales/cs.ts new file mode 100644 index 0000000..707b2ef --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/cs.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'cs', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Dříve', + next: 'Později', + today: 'Nyní', + year: 'Rok', + month: 'Měsíc', + week: 'Týden', + day: 'Den', + list: 'Agenda', + }, + weekText: 'Týd', + allDayText: 'Celý den', + moreLinkText(n) { + return '+další: ' + n + }, + noEventsText: 'Žádné akce k zobrazení', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/cy.ts b/fullcalendar-main/packages/core/src/locales/cy.ts new file mode 100644 index 0000000..bb9c8e4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/cy.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'cy', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Blaenorol', + next: 'Nesaf', + today: 'Heddiw', + year: 'Blwyddyn', + month: 'Mis', + week: 'Wythnos', + day: 'Dydd', + list: 'Rhestr', + }, + weekText: 'Wythnos', + allDayText: 'Trwy\'r dydd', + moreLinkText: 'Mwy', + noEventsText: 'Dim digwyddiadau', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/da.ts b/fullcalendar-main/packages/core/src/locales/da.ts new file mode 100644 index 0000000..e374937 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/da.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'da', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Næste', + today: 'I dag', + year: 'År', + month: 'Måned', + week: 'Uge', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uge', + allDayText: 'Hele dagen', + moreLinkText: 'flere', + noEventsText: 'Ingen arrangementer at vise', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/de-at.ts b/fullcalendar-main/packages/core/src/locales/de-at.ts new file mode 100644 index 0000000..f797edf --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/de-at.ts @@ -0,0 +1,60 @@ +import { LocaleInput } from '../index.js' + +function affix(buttonText: 'Tag' | 'Woche' | 'Monat' | 'Jahr'): string { + return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' : + buttonText === 'Jahr' ? 's' : '' +} + +export default { + code: 'de-at', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + weekTextLong: 'Woche', + allDayText: 'Ganztägig', + moreLinkText(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + buttonHints: { + prev(buttonText) { + return `Vorherige${affix(buttonText)} ${buttonText}` + }, + next(buttonText) { + return `Nächste${affix(buttonText)} ${buttonText}` + }, + today(buttonText) { + // → Heute, Diese Woche, Dieser Monat, Dieses Jahr + if (buttonText === 'Tag') { + return 'Heute' + } + return `Diese${affix(buttonText)} ${buttonText}` + }, + }, + viewHint(buttonText) { + // → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht + const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es' + return buttonText + glue + 'ansicht' + }, + navLinkHint: 'Gehe zu $0', + moreLinkHint(eventCnt: number) { + return 'Zeige ' + (eventCnt === 1 ? + 'ein weiteres Ereignis' : + eventCnt + ' weitere Ereignisse') + }, + closeHint: 'Schließen', + timeHint: 'Uhrzeit', + eventHint: 'Ereignis', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/de.ts b/fullcalendar-main/packages/core/src/locales/de.ts new file mode 100644 index 0000000..768d003 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/de.ts @@ -0,0 +1,60 @@ +import { LocaleInput } from '../index.js' + +function affix(buttonText: 'Tag' | 'Woche' | 'Monat' | 'Jahr'): string { + return (buttonText === 'Tag' || buttonText === 'Monat') ? 'r' : + buttonText === 'Jahr' ? 's' : '' +} + +export default { + code: 'de', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zurück', + next: 'Vor', + today: 'Heute', + year: 'Jahr', + month: 'Monat', + week: 'Woche', + day: 'Tag', + list: 'Terminübersicht', + }, + weekText: 'KW', + weekTextLong: 'Woche', + allDayText: 'Ganztägig', + moreLinkText(n) { + return '+ weitere ' + n + }, + noEventsText: 'Keine Ereignisse anzuzeigen', + buttonHints: { + prev(buttonText) { + return `Vorherige${affix(buttonText)} ${buttonText}` + }, + next(buttonText) { + return `Nächste${affix(buttonText)} ${buttonText}` + }, + today(buttonText) { + // → Heute, Diese Woche, Dieser Monat, Dieses Jahr + if (buttonText === 'Tag') { + return 'Heute' + } + return `Diese${affix(buttonText)} ${buttonText}` + }, + }, + viewHint(buttonText) { + // → Tagesansicht, Wochenansicht, Monatsansicht, Jahresansicht + const glue = buttonText === 'Woche' ? 'n' : buttonText === 'Monat' ? 's' : 'es' + return buttonText + glue + 'ansicht' + }, + navLinkHint: 'Gehe zu $0', + moreLinkHint(eventCnt: number) { + return 'Zeige ' + (eventCnt === 1 ? + 'ein weiteres Ereignis' : + eventCnt + ' weitere Ereignisse') + }, + closeHint: 'Schließen', + timeHint: 'Uhrzeit', + eventHint: 'Ereignis', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/el.ts b/fullcalendar-main/packages/core/src/locales/el.ts new file mode 100644 index 0000000..9b8cc98 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/el.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'el', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4st is the first week of the year. + }, + buttonText: { + prev: 'Προηγούμενος', + next: 'Επόμενος', + today: 'Σήμερα', + year: 'Ετος', + month: 'Μήνας', + week: 'Εβδομάδα', + day: 'Ημέρα', + list: 'Ατζέντα', + }, + weekText: 'Εβδ', + allDayText: 'Ολοήμερο', + moreLinkText: 'περισσότερα', + noEventsText: 'Δεν υπάρχουν γεγονότα προς εμφάνιση', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/en-au.ts b/fullcalendar-main/packages/core/src/locales/en-au.ts new file mode 100644 index 0000000..220fde0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/en-au.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'en-au', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonHints: { + prev: 'Previous $0', + next: 'Next $0', + today: 'This $0', + }, + viewHint: '$0 view', + navLinkHint: 'Go to $0', + moreLinkHint(eventCnt: number) { + return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}` + }, +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/en-gb.ts b/fullcalendar-main/packages/core/src/locales/en-gb.ts new file mode 100644 index 0000000..391e342 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/en-gb.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'en-gb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonHints: { + prev: 'Previous $0', + next: 'Next $0', + today: 'This $0', + }, + viewHint: '$0 view', + navLinkHint: 'Go to $0', + moreLinkHint(eventCnt: number) { + return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}` + }, +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/en-nz.ts b/fullcalendar-main/packages/core/src/locales/en-nz.ts new file mode 100644 index 0000000..5ede023 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/en-nz.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'en-nz', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonHints: { + prev: 'Previous $0', + next: 'Next $0', + today: 'This $0', + }, + viewHint: '$0 view', + navLinkHint: 'Go to $0', + moreLinkHint(eventCnt: number) { + return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}` + }, +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/eo.ts b/fullcalendar-main/packages/core/src/locales/eo.ts new file mode 100644 index 0000000..da9e90c --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/eo.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'eo', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Antaŭa', + next: 'Sekva', + today: 'Hodiaŭ', + year: 'Jaro', + month: 'Monato', + week: 'Semajno', + day: 'Tago', + list: 'Tagordo', + }, + weekText: 'Sm', + allDayText: 'Tuta tago', + moreLinkText: 'pli', + noEventsText: 'Neniuj eventoj por montri', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/es-us.ts b/fullcalendar-main/packages/core/src/locales/es-us.ts new file mode 100644 index 0000000..de8164a --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/es-us.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'es', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + year: 'Año', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Todo el día', + moreLinkText: 'más', + noEventsText: 'No hay eventos para mostrar', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/es.ts b/fullcalendar-main/packages/core/src/locales/es.ts new file mode 100644 index 0000000..433cd80 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/es.ts @@ -0,0 +1,42 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'es', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Sig', + today: 'Hoy', + year: 'Año', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Agenda', + }, + buttonHints: { + prev: '$0 antes', + next: '$0 siguiente', + today(buttonText) { + return (buttonText === 'Día') ? 'Hoy' : + ((buttonText === 'Semana') ? 'Esta' : 'Este') + ' ' + buttonText.toLocaleLowerCase() + }, + }, + viewHint(buttonText) { + return 'Vista ' + (buttonText === 'Semana' ? 'de la' : 'del') + ' ' + buttonText.toLocaleLowerCase() + }, + weekText: 'Sm', + weekTextLong: 'Semana', + allDayText: 'Todo el día', + moreLinkText: 'más', + moreLinkHint(eventCnt) { + return `Mostrar ${eventCnt} eventos más` + }, + noEventsText: 'No hay eventos para mostrar', + navLinkHint: 'Ir al $0', + closeHint: 'Cerrar', + timeHint: 'La hora', + eventHint: 'Evento', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/et.ts b/fullcalendar-main/packages/core/src/locales/et.ts new file mode 100644 index 0000000..1aaecf7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/et.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'et', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Eelnev', + next: 'Järgnev', + today: 'Täna', + year: 'Aasta', + month: 'Kuu', + week: 'Nädal', + day: 'Päev', + list: 'Päevakord', + }, + weekText: 'näd', + allDayText: 'Kogu päev', + moreLinkText(n) { + return '+ veel ' + n + }, + noEventsText: 'Kuvamiseks puuduvad sündmused', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/eu.ts b/fullcalendar-main/packages/core/src/locales/eu.ts new file mode 100644 index 0000000..7561527 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/eu.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'eu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Aur', + next: 'Hur', + today: 'Gaur', + year: 'Urtea', + month: 'Hilabetea', + week: 'Astea', + day: 'Eguna', + list: 'Agenda', + }, + weekText: 'As', + allDayText: 'Egun osoa', + moreLinkText: 'gehiago', + noEventsText: 'Ez dago ekitaldirik erakusteko', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/fa.ts b/fullcalendar-main/packages/core/src/locales/fa.ts new file mode 100644 index 0000000..b4a2dcb --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/fa.ts @@ -0,0 +1,26 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'fa', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'قبلی', + next: 'بعدی', + today: 'امروز', + year: 'سال', + month: 'ماه', + week: 'هفته', + day: 'روز', + list: 'برنامه', + }, + weekText: 'هف', + allDayText: 'تمام روز', + moreLinkText(n) { + return 'بیش از ' + n + }, + noEventsText: 'هیچ رویدادی به نمایش', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/fi.ts b/fullcalendar-main/packages/core/src/locales/fi.ts new file mode 100644 index 0000000..f77f401 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/fi.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'fi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Edellinen', + next: 'Seuraava', + today: 'Tänään', + year: 'Vuosi', + month: 'Kuukausi', + week: 'Viikko', + day: 'Päivä', + list: 'Tapahtumat', + }, + weekText: 'Vk', + allDayText: 'Koko päivä', + moreLinkText: 'lisää', + noEventsText: 'Ei näytettäviä tapahtumia', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/fr-ca.ts b/fullcalendar-main/packages/core/src/locales/fr-ca.ts new file mode 100644 index 0000000..85864e6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/fr-ca.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'fr', + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Aujourd\'hui', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sem.', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun évènement à afficher', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/fr-ch.ts b/fullcalendar-main/packages/core/src/locales/fr-ch.ts new file mode 100644 index 0000000..52a8293 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/fr-ch.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'fr-ch', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Courant', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Mon planning', + }, + weekText: 'Sm', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun évènement à afficher', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/fr.ts b/fullcalendar-main/packages/core/src/locales/fr.ts new file mode 100644 index 0000000..7204925 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/fr.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'fr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Précédent', + next: 'Suivant', + today: 'Aujourd\'hui', + year: 'Année', + month: 'Mois', + week: 'Semaine', + day: 'Jour', + list: 'Planning', + }, + weekText: 'Sem.', + weekTextLong: 'Semaine', + allDayText: 'Toute la journée', + moreLinkText: 'en plus', + noEventsText: 'Aucun évènement à afficher', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/gl.ts b/fullcalendar-main/packages/core/src/locales/gl.ts new file mode 100644 index 0000000..ef7b914 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/gl.ts @@ -0,0 +1,42 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'gl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Ant', + next: 'Seg', + today: 'Hoxe', + year: 'Ano', + month: 'Mes', + week: 'Semana', + day: 'Día', + list: 'Axenda', + }, + buttonHints: { + prev: '$0 antes', + next: '$0 seguinte', + today(buttonText) { + return (buttonText === 'Día') ? 'Hoxe' : + ((buttonText === 'Semana') ? 'Esta' : 'Este') + ' ' + buttonText.toLocaleLowerCase() + }, + }, + viewHint(buttonText) { + return 'Vista ' + (buttonText === 'Semana' ? 'da' : 'do') + ' ' + buttonText.toLocaleLowerCase() + }, + weekText: 'Sm', + weekTextLong: 'Semana', + allDayText: 'Todo o día', + moreLinkText: 'máis', + moreLinkHint(eventCnt) { + return `Amosar ${eventCnt} eventos máis` + }, + noEventsText: 'Non hai eventos para amosar', + navLinkHint: 'Ir ao $0', + closeHint: 'Pechar', + timeHint: 'A hora', + eventHint: 'Evento', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/global.js.tpl b/fullcalendar-main/packages/core/src/locales/global.js.tpl new file mode 100644 index 0000000..58cbc3b --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/global.js.tpl @@ -0,0 +1,4 @@ +import { globalLocales } from '../index.js' +import locale from './{{localeCode}}.js' + +globalLocales.push(locale); diff --git a/fullcalendar-main/packages/core/src/locales/he.ts b/fullcalendar-main/packages/core/src/locales/he.ts new file mode 100644 index 0000000..f65dcdc --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/he.ts @@ -0,0 +1,20 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'he', + direction: 'rtl', + buttonText: { + prev: 'הקודם', + next: 'הבא', + today: 'היום', + year: 'שנה', + month: 'חודש', + week: 'שבוע', + day: 'יום', + list: 'סדר יום', + }, + allDayText: 'כל היום', + moreLinkText: 'נוספים', + noEventsText: 'אין אירועים להצגה', + weekText: 'שבוע', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/hi.ts b/fullcalendar-main/packages/core/src/locales/hi.ts new file mode 100644 index 0000000..3b5cedb --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/hi.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'hi', + week: { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'पिछला', + next: 'अगला', + today: 'आज', + year: 'वर्ष', + month: 'महीना', + week: 'सप्ताह', + day: 'दिन', + list: 'कार्यसूची', + }, + weekText: 'हफ्ता', + allDayText: 'सभी दिन', + moreLinkText(n) { + return '+अधिक ' + n + }, + noEventsText: 'कोई घटनाओं को प्रदर्शित करने के लिए', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/hr.ts b/fullcalendar-main/packages/core/src/locales/hr.ts new file mode 100644 index 0000000..e8af9b2 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/hr.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'hr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prijašnji', + next: 'Sljedeći', + today: 'Danas', + year: 'Godina', + month: 'Mjesec', + week: 'Tjedan', + day: 'Dan', + list: 'Raspored', + }, + weekText: 'Tje', + allDayText: 'Cijeli dan', + moreLinkText(n) { + return '+ još ' + n + }, + noEventsText: 'Nema događaja za prikaz', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/hu.ts b/fullcalendar-main/packages/core/src/locales/hu.ts new file mode 100644 index 0000000..2d2248e --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/hu.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'hu', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'vissza', + next: 'előre', + today: 'ma', + year: 'Év', + month: 'Hónap', + week: 'Hét', + day: 'Nap', + list: 'Lista', + }, + weekText: 'Hét', + allDayText: 'Egész nap', + moreLinkText: 'további', + noEventsText: 'Nincs megjeleníthető esemény', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/hy-am.ts b/fullcalendar-main/packages/core/src/locales/hy-am.ts new file mode 100644 index 0000000..d4a6338 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/hy-am.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'hy-am', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Նախորդ', + next: 'Հաջորդ', + today: 'Այսօր', + year: 'Տարի', + month: 'Ամիս', + week: 'Շաբաթ', + day: 'Օր', + list: 'Օրվա ցուցակ', + }, + weekText: 'Շաբ', + allDayText: 'Ամբողջ օր', + moreLinkText(n) { + return '+ ևս ' + n + }, + noEventsText: 'Բացակայում է իրադարձությունը ցուցադրելու', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/id.ts b/fullcalendar-main/packages/core/src/locales/id.ts new file mode 100644 index 0000000..33bcdb9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/id.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'id', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'mundur', + next: 'maju', + today: 'hari ini', + year: 'Tahun', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sehari penuh', + moreLinkText: 'lebih', + noEventsText: 'Tidak ada acara untuk ditampilkan', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/is.ts b/fullcalendar-main/packages/core/src/locales/is.ts new file mode 100644 index 0000000..d88b782 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/is.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'is', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Fyrri', + next: 'Næsti', + today: 'Í dag', + year: 'Ár', + month: 'Mánuður', + week: 'Vika', + day: 'Dagur', + list: 'Dagskrá', + }, + weekText: 'Vika', + allDayText: 'Allan daginn', + moreLinkText: 'meira', + noEventsText: 'Engir viðburðir til að sýna', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/it.ts b/fullcalendar-main/packages/core/src/locales/it.ts new file mode 100644 index 0000000..546d47c --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/it.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'it', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Prec', + next: 'Succ', + today: 'Oggi', + year: 'Anno', + month: 'Mese', + week: 'Settimana', + day: 'Giorno', + list: 'Agenda', + }, + weekText: 'Sm', + allDayText: 'Tutto il giorno', + moreLinkText(n) { + return '+altri ' + n + }, + noEventsText: 'Non ci sono eventi da visualizzare', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ja.ts b/fullcalendar-main/packages/core/src/locales/ja.ts new file mode 100644 index 0000000..05401c6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ja.ts @@ -0,0 +1,21 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ja', + buttonText: { + prev: '前', + next: '次', + today: '今日', + year: '年', + month: '月', + week: '週', + day: '日', + list: '予定リスト', + }, + weekText: '週', + allDayText: '終日', + moreLinkText(n) { + return '他 ' + n + ' 件' + }, + noEventsText: '表示する予定はありません', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ka.ts b/fullcalendar-main/packages/core/src/locales/ka.ts new file mode 100644 index 0000000..9c4c798 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ka.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ka', + week: { + dow: 1, + doy: 7, + }, + buttonText: { + prev: 'წინა', + next: 'შემდეგი', + today: 'დღეს', + year: 'წელიწადი', + month: 'თვე', + week: 'კვირა', + day: 'დღე', + list: 'დღის წესრიგი', + }, + weekText: 'კვ', + allDayText: 'მთელი დღე', + moreLinkText(n) { + return '+ კიდევ ' + n + }, + noEventsText: 'ღონისძიებები არ არის', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/kk.ts b/fullcalendar-main/packages/core/src/locales/kk.ts new file mode 100644 index 0000000..4780550 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/kk.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'kk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Алдыңғы', + next: 'Келесі', + today: 'Бүгін', + year: 'Жыл', + month: 'Ай', + week: 'Апта', + day: 'Күн', + list: 'Күн тәртібі', + }, + weekText: 'Не', + allDayText: 'Күні бойы', + moreLinkText(n) { + return '+ тағы ' + n + }, + noEventsText: 'Көрсету үшін оқиғалар жоқ', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/km.ts b/fullcalendar-main/packages/core/src/locales/km.ts new file mode 100644 index 0000000..ba33d48 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/km.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'km', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'មុន', + next: 'បន្ទាប់', + today: 'ថ្ងៃនេះ', + year: 'ឆ្នាំ', + month: 'ខែ', + week: 'សប្តាហ៍', + day: 'ថ្ងៃ', + list: 'បញ្ជី', + }, + weekText: 'សប្តាហ៍', + allDayText: 'ពេញមួយថ្ងៃ', + moreLinkText: 'ច្រើនទៀត', + noEventsText: 'គ្មានព្រឹត្តិការណ៍ត្រូវបង្ហាញ', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ko.ts b/fullcalendar-main/packages/core/src/locales/ko.ts new file mode 100644 index 0000000..6329f39 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ko.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ko', + buttonText: { + prev: '이전달', + next: '다음달', + today: '오늘', + year: '년도', + month: '월', + week: '주', + day: '일', + list: '일정목록', + }, + weekText: '주', + allDayText: '종일', + moreLinkText: '개', + noEventsText: '일정이 없습니다', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ku.ts b/fullcalendar-main/packages/core/src/locales/ku.ts new file mode 100644 index 0000000..07312c1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ku.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ku', + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12, // The week that contains Jan 1st is the first week of the year. + }, + direction: 'rtl', + buttonText: { + prev: 'پێشتر', + next: 'دواتر', + today: 'ئەمڕو', + year: 'ساڵ', + month: 'مانگ', + week: 'هەفتە', + day: 'ڕۆژ', + list: 'بەرنامە', + }, + weekText: 'هەفتە', + allDayText: 'هەموو ڕۆژەکە', + moreLinkText: 'زیاتر', + noEventsText: 'هیچ ڕووداوێك نیە', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/lb.ts b/fullcalendar-main/packages/core/src/locales/lb.ts new file mode 100644 index 0000000..1005132 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/lb.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'lb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Zréck', + next: 'Weider', + today: 'Haut', + year: 'Joer', + month: 'Mount', + week: 'Woch', + day: 'Dag', + list: 'Terminiwwersiicht', + }, + weekText: 'W', + allDayText: 'Ganzen Dag', + moreLinkText: 'méi', + noEventsText: 'Nee Evenementer ze affichéieren', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/lt.ts b/fullcalendar-main/packages/core/src/locales/lt.ts new file mode 100644 index 0000000..5a87f2c --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/lt.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'lt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Atgal', + next: 'Pirmyn', + today: 'Šiandien', + year: 'Metai', + month: 'Mėnuo', + week: 'Savaitė', + day: 'Diena', + list: 'Darbotvarkė', + }, + weekText: 'SAV', + allDayText: 'Visą dieną', + moreLinkText: 'daugiau', + noEventsText: 'Nėra įvykių rodyti', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/lv.ts b/fullcalendar-main/packages/core/src/locales/lv.ts new file mode 100644 index 0000000..21f8d71 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/lv.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'lv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Iepr.', + next: 'Nāk.', + today: 'Šodien', + year: 'Gads', + month: 'Mēnesis', + week: 'Nedēļa', + day: 'Diena', + list: 'Dienas kārtība', + }, + weekText: 'Ned.', + allDayText: 'Visu dienu', + moreLinkText(n) { + return '+vēl ' + n + }, + noEventsText: 'Nav notikumu', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/mk.ts b/fullcalendar-main/packages/core/src/locales/mk.ts new file mode 100644 index 0000000..c25039f --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/mk.ts @@ -0,0 +1,21 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'mk', + buttonText: { + prev: 'претходно', + next: 'следно', + today: 'Денес', + year: 'година', + month: 'Месец', + week: 'Недела', + day: 'Ден', + list: 'График', + }, + weekText: 'Сед', + allDayText: 'Цел ден', + moreLinkText(n) { + return '+повеќе ' + n + }, + noEventsText: 'Нема настани за прикажување', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ms.ts b/fullcalendar-main/packages/core/src/locales/ms.ts new file mode 100644 index 0000000..df3e25a --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ms.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ms', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Sebelum', + next: 'Selepas', + today: 'hari ini', + year: 'Tahun', + month: 'Bulan', + week: 'Minggu', + day: 'Hari', + list: 'Agenda', + }, + weekText: 'Mg', + allDayText: 'Sepanjang hari', + moreLinkText(n) { + return 'masih ada ' + n + ' acara' + }, + noEventsText: 'Tiada peristiwa untuk dipaparkan', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/nb.ts b/fullcalendar-main/packages/core/src/locales/nb.ts new file mode 100644 index 0000000..29bfc93 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/nb.ts @@ -0,0 +1,34 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'nb', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Forrige', + next: 'Neste', + today: 'I dag', + year: 'År', + month: 'Måned', + week: 'Uke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Uke', + weekTextLong: 'Uke', + allDayText: 'Hele dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', + buttonHints: { + prev: 'Forrige $0', + next: 'Neste $0', + today: 'Nåværende $0', + }, + viewHint: '$0 visning', + navLinkHint: 'Gå til $0', + moreLinkHint(eventCnt: number) { + return `Vis ${eventCnt} flere hendelse${eventCnt === 1 ? '' : 'r'}` + }, +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ne.ts b/fullcalendar-main/packages/core/src/locales/ne.ts new file mode 100644 index 0000000..4461fed --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ne.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ne', // code for nepal + week: { + dow: 7, // Sunday is the first day of the week. + doy: 1, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'अघिल्लो', + next: 'अर्को', + today: 'आज', + year: 'वर्ष', + month: 'महिना', + week: 'हप्ता', + day: 'दिन', + list: 'सूची', + }, + weekText: 'हप्ता', + allDayText: 'दिनभरि', + moreLinkText: 'थप लिंक', + noEventsText: 'देखाउनको लागि कुनै घटनाहरू छैनन्', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/nl.ts b/fullcalendar-main/packages/core/src/locales/nl.ts new file mode 100644 index 0000000..58e6779 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/nl.ts @@ -0,0 +1,22 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'nl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Vorige', + next: 'Volgende', + today: 'Vandaag', + year: 'Jaar', + month: 'Maand', + week: 'Week', + day: 'Dag', + list: 'Lijst', + }, + allDayText: 'Hele dag', + moreLinkText: 'extra', + noEventsText: 'Geen evenementen om te laten zien', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/nn.ts b/fullcalendar-main/packages/core/src/locales/nn.ts new file mode 100644 index 0000000..a8edf1c --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/nn.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'nn', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Førre', + next: 'Neste', + today: 'I dag', + year: 'År', + month: 'Månad', + week: 'Veke', + day: 'Dag', + list: 'Agenda', + }, + weekText: 'Veke', + allDayText: 'Heile dagen', + moreLinkText: 'til', + noEventsText: 'Ingen hendelser å vise', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/pl.ts b/fullcalendar-main/packages/core/src/locales/pl.ts new file mode 100644 index 0000000..bfa83ba --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/pl.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'pl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Poprzedni', + next: 'Następny', + today: 'Dziś', + year: 'Rok', + month: 'Miesiąc', + week: 'Tydzień', + day: 'Dzień', + list: 'Plan dnia', + }, + weekText: 'Tydz', + allDayText: 'Cały dzień', + moreLinkText: 'więcej', + noEventsText: 'Brak wydarzeń do wyświetlenia', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/pt-br.ts b/fullcalendar-main/packages/core/src/locales/pt-br.ts new file mode 100644 index 0000000..01894d9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/pt-br.ts @@ -0,0 +1,42 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'pt-br', + buttonText: { + prev: 'Anterior', + next: 'Próximo', + prevYear: 'Ano anterior', + nextYear: 'Próximo ano', + year: 'Ano', + today: 'Hoje', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Lista', + }, + buttonHints: { + prev: '$0 Anterior', + next: 'Próximo $0', + today(buttonText) { + return (buttonText === 'Dia') ? 'Hoje' : + ((buttonText === 'Semana') ? 'Esta' : 'Este') + ' ' + buttonText.toLocaleLowerCase() + }, + }, + viewHint(buttonText) { + return 'Visualizar ' + (buttonText === 'Semana' ? 'a' : 'o') + ' ' + buttonText.toLocaleLowerCase() + }, + weekText: 'Sm', + weekTextLong: 'Semana', + allDayText: 'dia inteiro', + moreLinkText(n) { + return 'mais +' + n + }, + moreLinkHint(eventCnt) { + return `Mostrar mais ${eventCnt} eventos` + }, + noEventsText: 'Não há eventos para mostrar', + navLinkHint: 'Ir para $0', + closeHint: 'Fechar', + timeHint: 'A hora', + eventHint: 'Evento', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/pt.ts b/fullcalendar-main/packages/core/src/locales/pt.ts new file mode 100644 index 0000000..75b1f92 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/pt.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'pt', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Anterior', + next: 'Seguinte', + today: 'Hoje', + year: 'Ano', + month: 'Mês', + week: 'Semana', + day: 'Dia', + list: 'Agenda', + }, + weekText: 'Sem', + allDayText: 'Todo o dia', + moreLinkText: 'mais', + noEventsText: 'Não há eventos para mostrar', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ro.ts b/fullcalendar-main/packages/core/src/locales/ro.ts new file mode 100644 index 0000000..92c7438 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ro.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ro', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'precedentă', + next: 'următoare', + today: 'Azi', + year: 'An', + month: 'Lună', + week: 'Săptămână', + day: 'Zi', + list: 'Agendă', + }, + weekText: 'Săpt', + allDayText: 'Toată ziua', + moreLinkText(n) { + return '+alte ' + n + }, + noEventsText: 'Nu există evenimente de afișat', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ru.ts b/fullcalendar-main/packages/core/src/locales/ru.ts new file mode 100644 index 0000000..dfe113d --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ru.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ru', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Пред', + next: 'След', + today: 'Сегодня', + year: 'Год', + month: 'Месяц', + week: 'Неделя', + day: 'День', + list: 'Повестка дня', + }, + weekText: 'Нед', + allDayText: 'Весь день', + moreLinkText(n) { + return '+ ещё ' + n + }, + noEventsText: 'Нет событий для отображения', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/si-lk.ts b/fullcalendar-main/packages/core/src/locales/si-lk.ts new file mode 100644 index 0000000..539c02f --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/si-lk.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'si-lk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'පෙර', + next: 'පසු', + today: 'අද', + year: 'අවුරුදු', + month: 'මාසය', + week: 'සතිය', + day: 'දවස', + list: 'ලැයිස්තුව', + }, + weekText: 'සති', + allDayText: 'සියලු', + moreLinkText: 'තවත්', + noEventsText: 'මුකුත් නැත', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sk.ts b/fullcalendar-main/packages/core/src/locales/sk.ts new file mode 100644 index 0000000..80cdb34 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sk.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Predchádzajúci', + next: 'Nasledujúci', + today: 'Dnes', + year: 'Rok', + month: 'Mesiac', + week: 'Týždeň', + day: 'Deň', + list: 'Rozvrh', + }, + weekText: 'Ty', + allDayText: 'Celý deň', + moreLinkText(n) { + return '+ďalšie: ' + n + }, + noEventsText: 'Žiadne akcie na zobrazenie', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sl.ts b/fullcalendar-main/packages/core/src/locales/sl.ts new file mode 100644 index 0000000..551f501 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sl.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prejšnji', + next: 'Naslednji', + today: 'Trenutni', + year: 'Leto', + month: 'Mesec', + week: 'Teden', + day: 'Dan', + list: 'Dnevni red', + }, + weekText: 'Teden', + allDayText: 'Ves dan', + moreLinkText: 'več', + noEventsText: 'Ni dogodkov za prikaz', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sm.ts b/fullcalendar-main/packages/core/src/locales/sm.ts new file mode 100644 index 0000000..747c36d --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sm.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sm', + buttonText: { + prev: 'Talu ai', + next: 'Mulimuli atu', + today: 'Aso nei', + year: 'Tausaga', + month: 'Masina', + week: 'Vaiaso', + day: 'Aso', + list: 'Faasologa', + }, + weekText: 'Vaiaso', + allDayText: 'Aso atoa', + moreLinkText: 'sili atu', + noEventsText: 'Leai ni mea na tutupu', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sq.ts b/fullcalendar-main/packages/core/src/locales/sq.ts new file mode 100644 index 0000000..a30abc0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sq.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sq', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'mbrapa', + next: 'Përpara', + today: 'Sot', + year: 'Viti', + month: 'Muaj', + week: 'Javë', + day: 'Ditë', + list: 'Listë', + }, + weekText: 'Ja', + allDayText: 'Gjithë ditën', + moreLinkText(n) { + return '+më tepër ' + n + }, + noEventsText: 'Nuk ka evente për të shfaqur', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sr-cyrl.ts b/fullcalendar-main/packages/core/src/locales/sr-cyrl.ts new file mode 100644 index 0000000..3086f61 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sr-cyrl.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sr-cyrl', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Претходна', + next: 'следећи', + today: 'Данас', + year: 'Година', + month: 'Месец', + week: 'Недеља', + day: 'Дан', + list: 'Планер', + }, + weekText: 'Сед', + allDayText: 'Цео дан', + moreLinkText(n) { + return '+ још ' + n + }, + noEventsText: 'Нема догађаја за приказ', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sr.ts b/fullcalendar-main/packages/core/src/locales/sr.ts new file mode 100644 index 0000000..6e1cafc --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sr.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Prethodna', + next: 'Sledeći', + today: 'Danas', + year: 'Godina', + month: 'Mеsеc', + week: 'Nеdеlja', + day: 'Dan', + list: 'Planеr', + }, + weekText: 'Sed', + allDayText: 'Cеo dan', + moreLinkText(n) { + return '+ još ' + n + }, + noEventsText: 'Nеma događaja za prikaz', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/sv.ts b/fullcalendar-main/packages/core/src/locales/sv.ts new file mode 100644 index 0000000..a6f26fa --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/sv.ts @@ -0,0 +1,43 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'sv', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Förra', + next: 'Nästa', + today: 'Idag', + year: 'År', + month: 'Månad', + week: 'Vecka', + day: 'Dag', + list: 'Program', + }, + buttonHints: { + prev(buttonText) { + return `Föregående ${buttonText.toLocaleLowerCase()}` + }, + next(buttonText) { + return `Nästa ${buttonText.toLocaleLowerCase()}` + }, + today(buttonText) { + return (buttonText === 'Program' ? 'Detta' : 'Denna') + ' ' + buttonText.toLocaleLowerCase() + }, + }, + viewHint: '$0 vy', + navLinkHint: 'Gå till $0', + moreLinkHint(eventCnt: number) { + return `Visa ytterligare ${eventCnt} händelse${eventCnt === 1 ? '' : 'r'}` + }, + weekText: 'v.', + weekTextLong: 'Vecka', + allDayText: 'Heldag', + moreLinkText: 'till', + noEventsText: 'Inga händelser att visa', + closeHint: 'Stäng', + timeHint: 'Klockan', + eventHint: 'Händelse', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ta-in.ts b/fullcalendar-main/packages/core/src/locales/ta-in.ts new file mode 100644 index 0000000..ee3a479 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ta-in.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ta-in', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'முந்தைய', + next: 'அடுத்தது', + today: 'இன்று', + year: 'ஆண்டு', + month: 'மாதம்', + week: 'வாரம்', + day: 'நாள்', + list: 'தினசரி அட்டவணை', + }, + weekText: 'வாரம்', + allDayText: 'நாள் முழுவதும்', + moreLinkText(n) { + return '+ மேலும் ' + n + }, + noEventsText: 'காண்பிக்க நிகழ்வுகள் இல்லை', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/th.ts b/fullcalendar-main/packages/core/src/locales/th.ts new file mode 100644 index 0000000..4c3bb1d --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/th.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'th', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'ก่อนหน้า', + next: 'ถัดไป', + prevYear: 'ปีก่อนหน้า', + nextYear: 'ปีถัดไป', + year: 'ปี', + today: 'วันนี้', + month: 'เดือน', + week: 'สัปดาห์', + day: 'วัน', + list: 'กำหนดการ', + }, + weekText: 'สัปดาห์', + allDayText: 'ตลอดวัน', + moreLinkText: 'เพิ่มเติม', + noEventsText: 'ไม่มีกิจกรรมที่จะแสดง', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/tr.ts b/fullcalendar-main/packages/core/src/locales/tr.ts new file mode 100644 index 0000000..f02dc59 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/tr.ts @@ -0,0 +1,23 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'tr', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'geri', + next: 'ileri', + today: 'bugün', + year: 'Yıl', + month: 'Ay', + week: 'Hafta', + day: 'Gün', + list: 'Ajanda', + }, + weekText: 'Hf', + allDayText: 'Tüm gün', + moreLinkText: 'daha fazla', + noEventsText: 'Gösterilecek etkinlik yok', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/ug.ts b/fullcalendar-main/packages/core/src/locales/ug.ts new file mode 100644 index 0000000..83213dc --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/ug.ts @@ -0,0 +1,16 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'ug', + buttonText: { + prev: 'ئالدىنقى', + next: 'كېيىنكى', + today: 'بۈگۈن', + year: 'يىل', + month: 'ئاي', + week: 'ھەپتە', + day: 'كۈن', + list: 'كۈنتەرتىپ', + }, + allDayText: 'پۈتۈن كۈن', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/uk.ts b/fullcalendar-main/packages/core/src/locales/uk.ts new file mode 100644 index 0000000..f7a72a8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/uk.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'uk', + week: { + dow: 1, // Monday is the first day of the week. + doy: 7, // The week that contains Jan 1st is the first week of the year. + }, + buttonText: { + prev: 'Попередній', + next: 'далі', + today: 'Сьогодні', + year: 'рік', + month: 'Місяць', + week: 'Тиждень', + day: 'День', + list: 'Порядок денний', + }, + weekText: 'Тиж', + allDayText: 'Увесь день', + moreLinkText(n) { + return '+ще ' + n + '...' + }, + noEventsText: 'Немає подій для відображення', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/uz-cy.ts b/fullcalendar-main/packages/core/src/locales/uz-cy.ts new file mode 100644 index 0000000..944acd2 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/uz-cy.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'uz-cy', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Олин', + next: 'Кейин', + today: 'Бугун', + month: 'Ой', + week: 'Ҳафта', + day: 'Кун', + list: 'Кун тартиби', + }, + weekText: 'Ҳафта', + allDayText: 'Кун бўйича', + moreLinkText(n) { + return '+ яна ' + n + }, + noEventsText: 'Кўрсатиш учун воқеалар йўқ', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/uz.ts b/fullcalendar-main/packages/core/src/locales/uz.ts new file mode 100644 index 0000000..394e2ad --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/uz.ts @@ -0,0 +1,24 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'uz', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Oldingi', + next: 'Keyingi', + today: 'Bugun', + year: 'Yil', + month: 'Oy', + week: 'Xafta', + day: 'Kun', + list: 'Kun tartibi', + }, + allDayText: 'Kun bo\'yi', + moreLinkText(n) { + return '+ yana ' + n + }, + noEventsText: 'Ko\'rsatish uchun voqealar yo\'q', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/vi.ts b/fullcalendar-main/packages/core/src/locales/vi.ts new file mode 100644 index 0000000..01eca81 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/vi.ts @@ -0,0 +1,25 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'vi', + week: { + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: 'Trước', + next: 'Tiếp', + today: 'Hôm nay', + year: 'Năm', + month: 'Tháng', + week: 'Tuần', + day: 'Ngày', + list: 'Lịch biểu', + }, + weekText: 'Tu', + allDayText: 'Cả ngày', + moreLinkText(n) { + return '+ thêm ' + n + }, + noEventsText: 'Không có sự kiện để hiển thị', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/zh-cn.ts b/fullcalendar-main/packages/core/src/locales/zh-cn.ts new file mode 100644 index 0000000..4b489ad --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/zh-cn.ts @@ -0,0 +1,26 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'zh-cn', + week: { + // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 + dow: 1, // Monday is the first day of the week. + doy: 4, // The week that contains Jan 4th is the first week of the year. + }, + buttonText: { + prev: '上月', + next: '下月', + today: '今天', + year: '年', + month: '月', + week: '周', + day: '日', + list: '日程', + }, + weekText: '周', + allDayText: '全天', + moreLinkText(n) { + return '另外 ' + n + ' 个' + }, + noEventsText: '没有事件显示', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/locales/zh-tw.ts b/fullcalendar-main/packages/core/src/locales/zh-tw.ts new file mode 100644 index 0000000..c3d6c91 --- /dev/null +++ b/fullcalendar-main/packages/core/src/locales/zh-tw.ts @@ -0,0 +1,19 @@ +import { LocaleInput } from '../index.js' + +export default { + code: 'zh-tw', + buttonText: { + prev: '上個', + next: '下個', + today: '今天', + year: '年', + month: '月', + week: '週', + day: '天', + list: '活動列表', + }, + weekText: '週', + allDayText: '整天', + moreLinkText: '顯示更多', + noEventsText: '沒有任何活動', +} as LocaleInput diff --git a/fullcalendar-main/packages/core/src/option-change-handlers.ts b/fullcalendar-main/packages/core/src/option-change-handlers.ts new file mode 100644 index 0000000..94c9a3e --- /dev/null +++ b/fullcalendar-main/packages/core/src/option-change-handlers.ts @@ -0,0 +1,64 @@ +import { createPlugin } from './plugin-system.js' +import { hashValuesToArray } from './util/object.js' +import { EventSource } from './structs/event-source.js' +import { CalendarContext } from './CalendarContext.js' + +export const changeHandlerPlugin = createPlugin({ + name: 'change-handler', + optionChangeHandlers: { + events(events, context) { + handleEventSources([events], context) + }, + eventSources: handleEventSources, + }, +}) + +/* +BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out +*/ +function handleEventSources(inputs, context: CalendarContext) { + let unfoundSources: EventSource<any>[] = hashValuesToArray(context.getCurrentData().eventSources) + + if ( + unfoundSources.length === 1 && + inputs.length === 1 && + Array.isArray(unfoundSources[0]._raw) && + Array.isArray(inputs[0]) + ) { + context.dispatch({ + type: 'RESET_RAW_EVENTS', + sourceId: unfoundSources[0].sourceId, + rawEvents: inputs[0], + }) + return + } + + let newInputs = [] + + for (let input of inputs) { + let inputFound = false + + for (let i = 0; i < unfoundSources.length; i += 1) { + if (unfoundSources[i]._raw === input) { + unfoundSources.splice(i, 1) // delete + inputFound = true + break + } + } + + if (!inputFound) { + newInputs.push(input) + } + } + + for (let unfoundSource of unfoundSources) { + context.dispatch({ + type: 'REMOVE_EVENT_SOURCE', + sourceId: unfoundSource.sourceId, + }) + } + + for (let newInput of newInputs) { + context.calendarApi.addEventSource(newInput) + } +} diff --git a/fullcalendar-main/packages/core/src/options.ts b/fullcalendar-main/packages/core/src/options.ts new file mode 100644 index 0000000..dae37d5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/options.ts @@ -0,0 +1,514 @@ +import { createDuration } from './datelib/duration.js' +import { mergeProps, isPropsEqual } from './util/object.js' +import { isArraysEqual } from './util/array.js' +import { createFormatter } from './datelib/formatting.js' +import { parseFieldSpecs } from './util/misc.js' +import { DateProfileGeneratorClass } from './DateProfileGenerator.js' +import { CalendarApi } from './api/CalendarApi.js' +import { ViewApi } from './api/ViewApi.js' +import { EventApi } from './api/EventApi.js' +import { + CssDimValue, + DateInput, + DateRangeInput, + BusinessHoursInput, + EventSourceInput, + LocaleSingularArg, LocaleInput, + EventInput, EventInputTransformer, + OverlapFunc, ConstraintInput, AllowFunc, + PluginDef, + ViewComponentType, + SpecificViewContentArg, SpecificViewMountArg, + ClassNamesGenerator, CustomContentGenerator, DidMountHandler, WillUnmountHandler, + NowIndicatorContentArg, NowIndicatorMountArg, + WeekNumberContentArg, WeekNumberMountArg, + SlotLaneContentArg, SlotLaneMountArg, + SlotLabelContentArg, SlotLabelMountArg, + AllDayContentArg, AllDayMountArg, + DayHeaderContentArg, DayHeaderMountArg, + DayCellContentArg, DayCellMountArg, + ViewContentArg, ViewMountArg, + EventClickArg, + EventHoveringArg, + DateSelectArg, DateUnselectArg, + WeekNumberCalculation, + FormatterInput, + ToolbarInput, CustomButtonInput, ButtonIconsInput, ButtonTextCompoundInput, + EventContentArg, EventMountArg, + DatesSetArg, + EventAddArg, EventChangeArg, EventRemoveArg, + MoreLinkContentArg, + MoreLinkMountArg, + MoreLinkAction, + ButtonHintCompoundInput, + CustomRenderingHandler, +} from './api/structs.js' + +// base options +// ------------ + +export const BASE_OPTION_REFINERS = { + navLinkDayClick: identity as Identity<string | ((this: CalendarApi, date: Date, jsEvent: UIEvent) => void)>, + navLinkWeekClick: identity as Identity<string | ((this: CalendarApi, weekStart: Date, jsEvent: UIEvent) => void)>, + duration: createDuration, + bootstrapFontAwesome: identity as Identity<ButtonIconsInput | false>, // TODO: move to bootstrap plugin + buttonIcons: identity as Identity<ButtonIconsInput | false>, + customButtons: identity as Identity<{ [name: string]: CustomButtonInput }>, + defaultAllDayEventDuration: createDuration, + defaultTimedEventDuration: createDuration, + nextDayThreshold: createDuration, + scrollTime: createDuration, + scrollTimeReset: Boolean, + slotMinTime: createDuration, + slotMaxTime: createDuration, + dayPopoverFormat: createFormatter, + slotDuration: createDuration, + snapDuration: createDuration, + headerToolbar: identity as Identity<ToolbarInput | false>, + footerToolbar: identity as Identity<ToolbarInput | false>, + defaultRangeSeparator: String, + titleRangeSeparator: String, + forceEventDuration: Boolean, + + dayHeaders: Boolean, + dayHeaderFormat: createFormatter, + dayHeaderClassNames: identity as Identity<ClassNamesGenerator<DayHeaderContentArg>>, + dayHeaderContent: identity as Identity<CustomContentGenerator<DayHeaderContentArg>>, + dayHeaderDidMount: identity as Identity<DidMountHandler<DayHeaderMountArg>>, + dayHeaderWillUnmount: identity as Identity<WillUnmountHandler<DayHeaderMountArg>>, + + dayCellClassNames: identity as Identity<ClassNamesGenerator<DayCellContentArg>>, + dayCellContent: identity as Identity<CustomContentGenerator<DayCellContentArg>>, + dayCellDidMount: identity as Identity<DidMountHandler<DayCellMountArg>>, + dayCellWillUnmount: identity as Identity<WillUnmountHandler<DayCellMountArg>>, + + initialView: String, + aspectRatio: Number, + weekends: Boolean, + + weekNumberCalculation: identity as Identity<WeekNumberCalculation>, + weekNumbers: Boolean, + weekNumberClassNames: identity as Identity<ClassNamesGenerator<WeekNumberContentArg>>, + weekNumberContent: identity as Identity<CustomContentGenerator<WeekNumberContentArg>>, + weekNumberDidMount: identity as Identity<DidMountHandler<WeekNumberMountArg>>, + weekNumberWillUnmount: identity as Identity<WillUnmountHandler<WeekNumberMountArg>>, + + editable: Boolean, + + viewClassNames: identity as Identity<ClassNamesGenerator<ViewContentArg>>, + viewDidMount: identity as Identity<DidMountHandler<ViewMountArg>>, + viewWillUnmount: identity as Identity<WillUnmountHandler<ViewMountArg>>, + + nowIndicator: Boolean, + nowIndicatorClassNames: identity as Identity<ClassNamesGenerator<NowIndicatorContentArg>>, + nowIndicatorContent: identity as Identity<CustomContentGenerator<NowIndicatorContentArg>>, + nowIndicatorDidMount: identity as Identity<DidMountHandler<NowIndicatorMountArg>>, + nowIndicatorWillUnmount: identity as Identity<WillUnmountHandler<NowIndicatorMountArg>>, + + showNonCurrentDates: Boolean, + lazyFetching: Boolean, + startParam: String, + endParam: String, + timeZoneParam: String, + timeZone: String, + locales: identity as Identity<LocaleInput[]>, + locale: identity as Identity<LocaleSingularArg>, + themeSystem: String as Identity<'standard' | string>, + dragRevertDuration: Number, + dragScroll: Boolean, + allDayMaintainDuration: Boolean, + unselectAuto: Boolean, + dropAccept: identity as Identity<string | ((this: CalendarApi, draggable: any) => boolean)>, // TODO: type draggable + eventOrder: parseFieldSpecs, + eventOrderStrict: Boolean, + + handleWindowResize: Boolean, + windowResizeDelay: Number, + longPressDelay: Number, + eventDragMinDistance: Number, + expandRows: Boolean, + height: identity as Identity<CssDimValue>, + contentHeight: identity as Identity<CssDimValue>, + direction: String as Identity<'ltr' | 'rtl'>, + weekNumberFormat: createFormatter, + eventResizableFromStart: Boolean, + displayEventTime: Boolean, + displayEventEnd: Boolean, + weekText: String, // the short version + weekTextLong: String, // falls back to weekText + progressiveEventRendering: Boolean, + businessHours: identity as Identity<BusinessHoursInput>, + initialDate: identity as Identity<DateInput>, + now: identity as Identity<DateInput | ((this: CalendarApi) => DateInput)>, + eventDataTransform: identity as Identity<EventInputTransformer>, + stickyHeaderDates: identity as Identity<boolean | 'auto'>, + stickyFooterScrollbar: identity as Identity<boolean | 'auto'>, + viewHeight: identity as Identity<CssDimValue>, + defaultAllDay: Boolean, + eventSourceFailure: identity as Identity<(this: CalendarApi, error: any) => void>, + eventSourceSuccess: identity as Identity<(this: CalendarApi, eventsInput: EventInput[], response?: Response) => EventInput[] | void>, + + eventDisplay: String, // TODO: give more specific + eventStartEditable: Boolean, + eventDurationEditable: Boolean, + eventOverlap: identity as Identity<boolean | OverlapFunc>, + eventConstraint: identity as Identity<ConstraintInput>, + eventAllow: identity as Identity<AllowFunc>, + eventBackgroundColor: String, + eventBorderColor: String, + eventTextColor: String, + eventColor: String, + eventClassNames: identity as Identity<ClassNamesGenerator<EventContentArg>>, + eventContent: identity as Identity<CustomContentGenerator<EventContentArg>>, + eventDidMount: identity as Identity<DidMountHandler<EventMountArg>>, + eventWillUnmount: identity as Identity<WillUnmountHandler<EventMountArg>>, + + selectConstraint: identity as Identity<ConstraintInput>, + selectOverlap: identity as Identity<boolean | OverlapFunc>, + selectAllow: identity as Identity<AllowFunc>, + + droppable: Boolean, + unselectCancel: String, + + slotLabelFormat: identity as Identity<FormatterInput | FormatterInput[]>, + + slotLaneClassNames: identity as Identity<ClassNamesGenerator<SlotLaneContentArg>>, + slotLaneContent: identity as Identity<CustomContentGenerator<SlotLaneContentArg>>, + slotLaneDidMount: identity as Identity<DidMountHandler<SlotLaneMountArg>>, + slotLaneWillUnmount: identity as Identity<WillUnmountHandler<SlotLaneMountArg>>, + + slotLabelClassNames: identity as Identity<ClassNamesGenerator<SlotLabelContentArg>>, + slotLabelContent: identity as Identity<CustomContentGenerator<SlotLabelContentArg>>, + slotLabelDidMount: identity as Identity<DidMountHandler<SlotLabelMountArg>>, + slotLabelWillUnmount: identity as Identity<WillUnmountHandler<SlotLabelMountArg>>, + + dayMaxEvents: identity as Identity<boolean | number>, + dayMaxEventRows: identity as Identity<boolean | number>, + dayMinWidth: Number, + slotLabelInterval: createDuration, + + allDayText: String, + allDayClassNames: identity as Identity<ClassNamesGenerator<AllDayContentArg>>, + allDayContent: identity as Identity<CustomContentGenerator<AllDayContentArg>>, + allDayDidMount: identity as Identity<DidMountHandler<AllDayMountArg>>, + allDayWillUnmount: identity as Identity<WillUnmountHandler<AllDayMountArg>>, + + slotMinWidth: Number, // move to timeline? + navLinks: Boolean, + eventTimeFormat: createFormatter, + rerenderDelay: Number, // TODO: move to @fullcalendar/core right? nah keep here + moreLinkText: identity as Identity<string | ((this: CalendarApi, num: number) => string)>, // this not enforced :( check others too + moreLinkHint: identity as Identity<string | ((this: CalendarApi, num: number) => string)>, + selectMinDistance: Number, + selectable: Boolean, + selectLongPressDelay: Number, + eventLongPressDelay: Number, + + selectMirror: Boolean, + eventMaxStack: Number, + eventMinHeight: Number, + eventMinWidth: Number, + eventShortHeight: Number, + slotEventOverlap: Boolean, + plugins: identity as Identity<PluginDef[]>, + firstDay: Number, + dayCount: Number, + dateAlignment: String, + dateIncrement: createDuration, + hiddenDays: identity as Identity<number[]>, + fixedWeekCount: Boolean, + validRange: identity as Identity<DateRangeInput | ((this: CalendarApi, nowDate: Date) => DateRangeInput)>, // `this` works? + visibleRange: identity as Identity<DateRangeInput | ((this: CalendarApi, currentDate: Date) => DateRangeInput)>, // `this` works? + titleFormat: identity as Identity<FormatterInput>, // DONT parse just yet. we need to inject titleSeparator + + eventInteractive: Boolean, + + // only used by list-view, but languages define the value, so we need it in base options + noEventsText: String, + + viewHint: identity as Identity<string | ((...args: any[]) => string)>, + navLinkHint: identity as Identity<string | ((...args: any[]) => string)>, + closeHint: String, + timeHint: String, + eventHint: String, + + moreLinkClick: identity as Identity<MoreLinkAction>, + moreLinkClassNames: identity as Identity<ClassNamesGenerator<MoreLinkContentArg>>, + moreLinkContent: identity as Identity<CustomContentGenerator<MoreLinkContentArg>>, + moreLinkDidMount: identity as Identity<DidMountHandler<MoreLinkMountArg>>, + moreLinkWillUnmount: identity as Identity<WillUnmountHandler<MoreLinkMountArg>>, + + monthStartFormat: createFormatter, + + // for connectors + // (can't be part of plugin system b/c must be provided at runtime) + handleCustomRendering: identity as Identity<CustomRenderingHandler<any>>, + customRenderingMetaMap: identity as Identity<{ [optionName: string]: any }>, + customRenderingReplaces: Boolean, +} + +type BuiltInBaseOptionRefiners = typeof BASE_OPTION_REFINERS + +export interface BaseOptionRefiners extends BuiltInBaseOptionRefiners { + // for ambient-extending +} + +export type BaseOptions = RawOptionsFromRefiners< // as RawOptions + Required<BaseOptionRefiners> // Required is a hack for "Index signature is missing" +> + +// do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results. +// raw values. +export const BASE_OPTION_DEFAULTS = { + eventDisplay: 'auto', + defaultRangeSeparator: ' - ', + titleRangeSeparator: ' \u2013 ', // en dash + defaultTimedEventDuration: '01:00:00', + defaultAllDayEventDuration: { day: 1 }, + forceEventDuration: false, + nextDayThreshold: '00:00:00', + dayHeaders: true, + initialView: '', + aspectRatio: 1.35, + headerToolbar: { + start: 'title', + center: '', + end: 'today prev,next', + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'local' as WeekNumberCalculation, + editable: false, + nowIndicator: false, + scrollTime: '06:00:00', + scrollTimeReset: true, + slotMinTime: '00:00:00', + slotMaxTime: '24:00:00', + showNonCurrentDates: true, + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timeZoneParam: 'timeZone', + timeZone: 'local', // TODO: throw error if given falsy value? + locales: [], + locale: '', // blank values means it will compute based off locales[] + themeSystem: 'standard', + dragRevertDuration: 500, + dragScroll: true, + allDayMaintainDuration: false, + unselectAuto: true, + dropAccept: '*', + eventOrder: 'start,-duration,allDay,title', + dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + handleWindowResize: true, + windowResizeDelay: 100, // milliseconds before an updateSize happens + longPressDelay: 1000, + eventDragMinDistance: 5, // only applies to mouse + expandRows: false, + navLinks: false, + selectable: false, + eventMinHeight: 15, + eventMinWidth: 30, + eventShortHeight: 30, + monthStartFormat: { month: 'long', day: 'numeric' }, +} + +export type BaseOptionsRefined = DefaultedRefinedOptions< + RefinedOptionsFromRefiners<Required<BaseOptionRefiners>>, // Required is a hack for "Index signature is missing" + keyof typeof BASE_OPTION_DEFAULTS +> + +// calendar listeners +// ------------------ + +export const CALENDAR_LISTENER_REFINERS = { + datesSet: identity as Identity<(arg: DatesSetArg) => void>, + eventsSet: identity as Identity<(events: EventApi[]) => void>, + eventAdd: identity as Identity<(arg: EventAddArg) => void>, + eventChange: identity as Identity<(arg: EventChangeArg) => void>, + eventRemove: identity as Identity<(arg: EventRemoveArg) => void>, + windowResize: identity as Identity<(arg: { view: ViewApi }) => void>, + eventClick: identity as Identity<(arg: EventClickArg) => void>, // TODO: resource for scheduler???? + eventMouseEnter: identity as Identity<(arg: EventHoveringArg) => void>, + eventMouseLeave: identity as Identity<(arg: EventHoveringArg) => void>, + select: identity as Identity<(arg: DateSelectArg) => void>, // resource for scheduler???? + unselect: identity as Identity<(arg: DateUnselectArg) => void>, + loading: identity as Identity<(isLoading: boolean) => void>, + + // internal + _unmount: identity as Identity<() => void>, + _beforeprint: identity as Identity<() => void>, + _afterprint: identity as Identity<() => void>, + _noEventDrop: identity as Identity<() => void>, + _noEventResize: identity as Identity<() => void>, + _resize: identity as Identity<(forced: boolean) => void>, + _scrollRequest: identity as Identity<(arg: any) => void>, +} + +type BuiltInCalendarListenerRefiners = typeof CALENDAR_LISTENER_REFINERS + +export interface CalendarListenerRefiners extends BuiltInCalendarListenerRefiners { + // for ambient extending +} + +type CalendarListenersLoose = RefinedOptionsFromRefiners<Required<CalendarListenerRefiners>> // Required hack +export type CalendarListeners = Required<CalendarListenersLoose> // much more convenient for Emitters and whatnot + +// calendar-specific options +// ------------------------- + +export const CALENDAR_OPTION_REFINERS = { // does not include base nor calendar listeners + buttonText: identity as Identity<ButtonTextCompoundInput>, + buttonHints: identity as Identity<ButtonHintCompoundInput>, + views: identity as Identity<{ [viewId: string]: ViewOptions }>, + plugins: identity as Identity<PluginDef[]>, + initialEvents: identity as Identity<EventSourceInput>, + events: identity as Identity<EventSourceInput>, + eventSources: identity as Identity<EventSourceInput[]>, +} + +type BuiltInCalendarOptionRefiners = typeof CALENDAR_OPTION_REFINERS + +export interface CalendarOptionRefiners extends BuiltInCalendarOptionRefiners { + // for ambient-extending +} + +export type CalendarOptions = + BaseOptions & + CalendarListenersLoose & + RawOptionsFromRefiners<Required<CalendarOptionRefiners>> // Required hack https://github.com/microsoft/TypeScript/issues/15300 + +export type CalendarOptionsRefined = + BaseOptionsRefined & + CalendarListenersLoose & + RefinedOptionsFromRefiners<Required<CalendarOptionRefiners>> // Required hack + +export const COMPLEX_OPTION_COMPARATORS: { + [optionName in keyof CalendarOptions]: (a: CalendarOptions[optionName], b: CalendarOptions[optionName]) => boolean +} = { + headerToolbar: isMaybeObjectsEqual, + footerToolbar: isMaybeObjectsEqual, + buttonText: isMaybeObjectsEqual, + buttonHints: isMaybeObjectsEqual, + buttonIcons: isMaybeObjectsEqual, + dateIncrement: isMaybeObjectsEqual, + plugins: isMaybeArraysEqual, + events: isMaybeArraysEqual, + eventSources: isMaybeArraysEqual, + ['resources' as any]: isMaybeArraysEqual, +} + +export function isMaybeObjectsEqual(a, b) { + if (typeof a === 'object' && typeof b === 'object' && a && b) { // both non-null objects + return isPropsEqual(a, b) + } + return a === b +} + +function isMaybeArraysEqual(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + return isArraysEqual(a, b) + } + return a === b +} + +// view-specific options +// --------------------- + +export const VIEW_OPTION_REFINERS: { + [name: string]: any +} = { + type: String, + component: identity as Identity<ViewComponentType>, + buttonText: String, + buttonTextKey: String, // internal only + dateProfileGeneratorClass: identity as Identity<DateProfileGeneratorClass>, + usesMinMaxTime: Boolean, // internal only + classNames: identity as Identity<ClassNamesGenerator<SpecificViewContentArg>>, + content: identity as Identity<CustomContentGenerator<SpecificViewContentArg>>, + didMount: identity as Identity<DidMountHandler<SpecificViewMountArg>>, + willUnmount: identity as Identity<WillUnmountHandler<SpecificViewMountArg>>, +} + +type BuiltInViewOptionRefiners = typeof VIEW_OPTION_REFINERS + +export interface ViewOptionRefiners extends BuiltInViewOptionRefiners { + // for ambient-extending +} + +export type ViewOptions = + BaseOptions & + CalendarListenersLoose & + RawOptionsFromRefiners<Required<ViewOptionRefiners>> // Required hack + +export type ViewOptionsRefined = + BaseOptionsRefined & + CalendarListenersLoose & + RefinedOptionsFromRefiners<Required<ViewOptionRefiners>> // Required hack + +// util funcs +// ---------------------------------------------------------------------------------------------------- + +export function mergeRawOptions(optionSets: Dictionary[]) { + return mergeProps(optionSets, COMPLEX_OPTION_COMPARATORS) +} + +export function refineProps<Refiners extends GenericRefiners, Raw extends RawOptionsFromRefiners<Refiners>>( + input: Raw, + refiners: Refiners, +): { + refined: RefinedOptionsFromRefiners<Refiners>, + extra: Dictionary, +} { + let refined = {} as any + let extra = {} as any + + for (let propName in refiners) { + if (propName in input) { + refined[propName] = refiners[propName](input[propName]) + } + } + + for (let propName in input) { + if (!(propName in refiners)) { + extra[propName] = input[propName] + } + } + + return { refined, extra } +} + +// definition utils +// ---------------------------------------------------------------------------------------------------- + +export type GenericRefiners = { + [propName: string]: (input: any) => any +} + +export type GenericListenerRefiners = { + [listenerName: string]: Identity<(this: CalendarApi, ...args: any[]) => void> +} + +export type RawOptionsFromRefiners<Refiners extends GenericRefiners> = { + [Prop in keyof Refiners]?: // all optional + Refiners[Prop] extends ((input: infer RawType) => infer RefinedType) + ? (any extends RawType ? RefinedType : RawType) // if input type `any`, use output (for Boolean/Number/String) + : never +} + +export type RefinedOptionsFromRefiners<Refiners extends GenericRefiners> = { + [Prop in keyof Refiners]?: // all optional + Refiners[Prop] extends ((input: any) => infer RefinedType) ? RefinedType : never +} + +export type DefaultedRefinedOptions<RefinedOptions extends Dictionary, DefaultKey extends keyof RefinedOptions> = + Required<Pick<RefinedOptions, DefaultKey>> & + Partial<Omit<RefinedOptions, DefaultKey>> + +export type Dictionary = Record<string, any> + +export type Identity<T = any> = (raw: T) => T + +export function identity<T>(raw: T): T { + return raw +} diff --git a/fullcalendar-main/packages/core/src/plugin-system-struct.ts b/fullcalendar-main/packages/core/src/plugin-system-struct.ts new file mode 100644 index 0000000..a54e535 --- /dev/null +++ b/fullcalendar-main/packages/core/src/plugin-system-struct.ts @@ -0,0 +1,113 @@ +import { ReducerFunc } from './reducers/CalendarDataManager.js' +import { EventDefMemberAdder } from './structs/event-parse.js' +import { eventDefMutationApplier } from './structs/event-mutation.js' +import { DatePointTransform, DateSpanTransform, CalendarInteractionClass, OptionChangeHandlerMap } from './calendar-utils.js' +import { ViewConfigInputHash } from './structs/view-config.js' +import { ViewProps } from './View.js' +import { CalendarContentProps } from './CalendarContent.js' +import { CalendarContext } from './CalendarContext.js' +import { isPropsValidTester } from './structs/constraint.js' +import { eventDragMutationMassager, eventIsDraggableTransformer, EventDropTransformers } from './interactions/event-dragging.js' +import { dateSelectionJoinTransformer } from './interactions/date-selecting.js' +import { ExternalDefTransform } from './interactions/external-element-dragging.js' +import { InteractionClass } from './interactions/interaction.js' +import { ThemeClass } from './theme/Theme.js' +import { EventSourceDef } from './structs/event-source-def.js' +import { CmdFormatterFunc } from './datelib/DateFormatter.js' +import { RecurringType } from './structs/recurring-event.js' +import { NamedTimeZoneImplClass } from './datelib/timezone.js' +import { ElementDraggingClass } from './interactions/ElementDragging.js' +import { ComponentChildren } from './preact.js' +import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl.js' +import { GenericRefiners, GenericListenerRefiners, Dictionary } from './options.js' +import { CalendarData } from './reducers/data-types.js' + +// TODO: easier way to add new hooks? need to update a million things + +export interface PluginDefInput { + name: string + premiumReleaseDate?: string + deps?: PluginDef[] + reducers?: ReducerFunc[] + isLoadingFuncs?: ((state: Dictionary) => boolean)[] + contextInit?: (context: CalendarContext) => void + eventRefiners?: GenericRefiners // why not an array like others? + eventDefMemberAdders?: EventDefMemberAdder[] + eventSourceRefiners?: GenericRefiners + isDraggableTransformers?: eventIsDraggableTransformer[] + eventDragMutationMassagers?: eventDragMutationMassager[] + eventDefMutationAppliers?: eventDefMutationApplier[] + dateSelectionTransformers?: dateSelectionJoinTransformer[] + datePointTransforms?: DatePointTransform[] + dateSpanTransforms?: DateSpanTransform[] + views?: ViewConfigInputHash + viewPropsTransformers?: ViewPropsTransformerClass[] + isPropsValid?: isPropsValidTester + externalDefTransforms?: ExternalDefTransform[] + viewContainerAppends?: ViewContainerAppend[] + eventDropTransformers?: EventDropTransformers[] + componentInteractions?: InteractionClass[] + calendarInteractions?: CalendarInteractionClass[] + themeClasses?: { [themeSystemName: string]: ThemeClass } + eventSourceDefs?: EventSourceDef<any>[] + cmdFormatter?: CmdFormatterFunc + recurringTypes?: RecurringType<any>[] + namedTimeZonedImpl?: NamedTimeZoneImplClass + initialView?: string + elementDraggingImpl?: ElementDraggingClass + optionChangeHandlers?: OptionChangeHandlerMap + scrollGridImpl?: ScrollGridImpl + listenerRefiners?: GenericListenerRefiners + optionRefiners?: GenericRefiners + propSetHandlers?: { [propName: string]: (val: any, context: CalendarData) => void } // TODO: make better types +} + +export interface PluginHooks { + premiumReleaseDate: Date | undefined + reducers: ReducerFunc[] + isLoadingFuncs: ((state: Dictionary) => boolean)[] + contextInit: ((context: CalendarContext) => void)[] + eventRefiners: GenericRefiners + eventDefMemberAdders: EventDefMemberAdder[] + eventSourceRefiners: GenericRefiners + isDraggableTransformers: eventIsDraggableTransformer[] + eventDragMutationMassagers: eventDragMutationMassager[] + eventDefMutationAppliers: eventDefMutationApplier[] + dateSelectionTransformers: dateSelectionJoinTransformer[] + datePointTransforms: DatePointTransform[] + dateSpanTransforms: DateSpanTransform[] + views: ViewConfigInputHash // TODO: parse before gets to this step? + viewPropsTransformers: ViewPropsTransformerClass[] + isPropsValid: isPropsValidTester | null + externalDefTransforms: ExternalDefTransform[] + viewContainerAppends: ViewContainerAppend[] + eventDropTransformers: EventDropTransformers[] + componentInteractions: InteractionClass[] + calendarInteractions: CalendarInteractionClass[] + themeClasses: { [themeSystemName: string]: ThemeClass } + eventSourceDefs: EventSourceDef<any>[] + cmdFormatter?: CmdFormatterFunc + recurringTypes: RecurringType<any>[] + namedTimeZonedImpl?: NamedTimeZoneImplClass + initialView: string + elementDraggingImpl?: ElementDraggingClass + optionChangeHandlers: OptionChangeHandlerMap + scrollGridImpl: ScrollGridImpl | null + listenerRefiners: GenericListenerRefiners + optionRefiners: GenericRefiners + propSetHandlers: { [propName: string]: (val: any, context: CalendarData) => void } +} + +export interface PluginDef extends PluginHooks { + id: string + name: string + deps: PluginDef[] +} + +export type ViewPropsTransformerClass = new() => ViewPropsTransformer + +export interface ViewPropsTransformer { + transform(viewProps: ViewProps, calendarProps: CalendarContentProps): any +} + +export type ViewContainerAppend = (context: CalendarContext) => ComponentChildren diff --git a/fullcalendar-main/packages/core/src/plugin-system.ts b/fullcalendar-main/packages/core/src/plugin-system.ts new file mode 100644 index 0000000..048b4f6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/plugin-system.ts @@ -0,0 +1,175 @@ +import { guid } from './util/misc.js' +import { PluginDefInput, PluginDef, PluginHooks } from './plugin-system-struct.js' +import { isArraysEqual } from './util/array.js' + +// TODO: easier way to add new hooks? need to update a million things + +export function createPlugin(input: PluginDefInput): PluginDef { + return { + id: guid(), + name: input.name, + premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate): undefined, + deps: input.deps || [], + reducers: input.reducers || [], + isLoadingFuncs: input.isLoadingFuncs || [], + contextInit: [].concat(input.contextInit || []), + eventRefiners: input.eventRefiners || {}, + eventDefMemberAdders: input.eventDefMemberAdders || [], + eventSourceRefiners: input.eventSourceRefiners || {}, + isDraggableTransformers: input.isDraggableTransformers || [], + eventDragMutationMassagers: input.eventDragMutationMassagers || [], + eventDefMutationAppliers: input.eventDefMutationAppliers || [], + dateSelectionTransformers: input.dateSelectionTransformers || [], + datePointTransforms: input.datePointTransforms || [], + dateSpanTransforms: input.dateSpanTransforms || [], + views: input.views || {}, + viewPropsTransformers: input.viewPropsTransformers || [], + isPropsValid: input.isPropsValid || null, + externalDefTransforms: input.externalDefTransforms || [], + viewContainerAppends: input.viewContainerAppends || [], + eventDropTransformers: input.eventDropTransformers || [], + componentInteractions: input.componentInteractions || [], + calendarInteractions: input.calendarInteractions || [], + themeClasses: input.themeClasses || {}, + eventSourceDefs: input.eventSourceDefs || [], + cmdFormatter: input.cmdFormatter, + recurringTypes: input.recurringTypes || [], + namedTimeZonedImpl: input.namedTimeZonedImpl, + initialView: input.initialView || '', + elementDraggingImpl: input.elementDraggingImpl, + optionChangeHandlers: input.optionChangeHandlers || {}, + scrollGridImpl: input.scrollGridImpl || null, + listenerRefiners: input.listenerRefiners || {}, + optionRefiners: input.optionRefiners || {}, + propSetHandlers: input.propSetHandlers || {}, + } +} + +function buildPluginHooks(pluginDefs: PluginDef[], globalDefs: PluginDef[]): PluginHooks { + let currentPluginIds: { [pluginName: string]: string } = {} + let hooks: PluginHooks = { + premiumReleaseDate: undefined, + reducers: [], + isLoadingFuncs: [], + contextInit: [], + eventRefiners: {}, + eventDefMemberAdders: [], + eventSourceRefiners: {}, + isDraggableTransformers: [], + eventDragMutationMassagers: [], + eventDefMutationAppliers: [], + dateSelectionTransformers: [], + datePointTransforms: [], + dateSpanTransforms: [], + views: {}, + viewPropsTransformers: [], + isPropsValid: null, + externalDefTransforms: [], + viewContainerAppends: [], + eventDropTransformers: [], + componentInteractions: [], + calendarInteractions: [], + themeClasses: {}, + eventSourceDefs: [], + cmdFormatter: null, + recurringTypes: [], + namedTimeZonedImpl: null, + initialView: '', + elementDraggingImpl: null, + optionChangeHandlers: {}, + scrollGridImpl: null, + listenerRefiners: {}, + optionRefiners: {}, + propSetHandlers: {}, + } + + function addDefs(defs: PluginDef[]) { + for (let def of defs) { + const pluginName = def.name + const currentId = currentPluginIds[pluginName] + + if (currentId === undefined) { + currentPluginIds[pluginName] = def.id + addDefs(def.deps) + hooks = combineHooks(hooks, def) + } else if (currentId !== def.id) { + // different ID than the one already added + console.warn(`Duplicate plugin '${pluginName}'`) + } + } + } + + if (pluginDefs) { + addDefs(pluginDefs) + } + + addDefs(globalDefs) + + return hooks +} + +export function buildBuildPluginHooks() { // memoizes + let currentOverrideDefs: PluginDef[] = [] + let currentGlobalDefs: PluginDef[] = [] + let currentHooks: PluginHooks + + return (overrideDefs: PluginDef[], globalDefs: PluginDef[]) => { + if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) { + currentHooks = buildPluginHooks(overrideDefs, globalDefs) + } + currentOverrideDefs = overrideDefs + currentGlobalDefs = globalDefs + return currentHooks + } +} + +function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks { + return { + premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate), + reducers: hooks0.reducers.concat(hooks1.reducers), + isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs), + contextInit: hooks0.contextInit.concat(hooks1.contextInit), + eventRefiners: { ...hooks0.eventRefiners, ...hooks1.eventRefiners }, + eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders), + eventSourceRefiners: { ...hooks0.eventSourceRefiners, ...hooks1.eventSourceRefiners }, + isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers), + eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers), + eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers), + dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers), + datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms), + dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms), + views: { ...hooks0.views, ...hooks1.views }, + viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers), + isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid, + externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms), + viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends), + eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers), + calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions), + componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions), + themeClasses: { ...hooks0.themeClasses, ...hooks1.themeClasses }, + eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs), + cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter, + recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes), + namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl, + initialView: hooks0.initialView || hooks1.initialView, // put earlier plugins FIRST + elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // " + optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers }, + scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl, + listenerRefiners: { ...hooks0.listenerRefiners, ...hooks1.listenerRefiners }, + optionRefiners: { ...hooks0.optionRefiners, ...hooks1.optionRefiners }, + propSetHandlers: { ...hooks0.propSetHandlers, ...hooks1.propSetHandlers }, + } +} + +function compareOptionalDates( + date0: Date | undefined, + date1: Date | undefined, +): Date | undefined { + if (date0 === undefined) { + return date1 + } + if (date1 === undefined) { + return date0 + } + return new Date(Math.max(date0.valueOf(), date1.valueOf())) +} diff --git a/fullcalendar-main/packages/core/src/preact.ts b/fullcalendar-main/packages/core/src/preact.ts new file mode 100644 index 0000000..d136dd4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/preact.ts @@ -0,0 +1,70 @@ +import * as preact from 'preact' +export * from 'preact' +export { createPortal } from 'preact/compat' + +/* +NOTE: this can be a public API, especially createElement for hooks. +See examples/typescript-scheduler/src/index.ts +*/ + +export function flushSync(runBeforeFlush) { + runBeforeFlush() + + let oldDebounceRendering = preact.options.debounceRendering // orig + let callbackQ = [] + + function execCallbackSync(callback) { + callbackQ.push(callback) + } + + preact.options.debounceRendering = execCallbackSync + preact.render(preact.createElement(FakeComponent, {}), document.createElement('div')) + + while (callbackQ.length) { + callbackQ.shift()() + } + + preact.options.debounceRendering = oldDebounceRendering +} + +class FakeComponent extends preact.Component { + render() { return preact.createElement('div', {}) } + componentDidMount() { this.setState({}) } +} + +// TODO: use preact/compat instead? +export function createContext<T>(defaultValue: T) { + let ContextType = preact.createContext<T>(defaultValue) + let origProvider = ContextType.Provider + + ContextType.Provider = function () { // eslint-disable-line func-names + let isNew = !this.getChildContext + let children = origProvider.apply(this, arguments as any) // eslint-disable-line prefer-rest-params + + if (isNew) { + let subs = [] + + this.shouldComponentUpdate = (_props) => { + if (this.props.value !== _props.value) { + subs.forEach((c) => { + c.context = _props.value + c.forceUpdate() + }) + } + } + + this.sub = (c) => { + subs.push(c) + let old = c.componentWillUnmount + c.componentWillUnmount = () => { + subs.splice(subs.indexOf(c), 1) + old && old.call(c) + } + } + } + + return children + } + + return ContextType +} diff --git a/fullcalendar-main/packages/core/src/reducers/Action.ts b/fullcalendar-main/packages/core/src/reducers/Action.ts new file mode 100644 index 0000000..69de83f --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/Action.ts @@ -0,0 +1,50 @@ +import { EventInput } from '../structs/event-parse.js' +import { DateRange } from '../datelib/date-range.js' +import { EventStore } from '../structs/event-store.js' +import { EventSource } from '../structs/event-source.js' +import { EventInteractionState } from '../interactions/event-interaction-state.js' +import { DateSpan } from '../structs/date-span.js' +import { DateMarker } from '../datelib/marker.js' + +export type Action = + { type: 'NOTHING' } | // hack + { type: 'SET_OPTION', optionName: string, rawOptionValue: any } | // TODO: how to link this to CalendarOptions? + + { type: 'PREV' } | + { type: 'NEXT' } | + { type: 'CHANGE_DATE', dateMarker: DateMarker } | + { type: 'CHANGE_VIEW_TYPE', viewType: string, dateMarker?: DateMarker } | + + { type: 'SELECT_DATES', selection: DateSpan } | + { type: 'UNSELECT_DATES' } | + + { type: 'SELECT_EVENT', eventInstanceId: string } | + { type: 'UNSELECT_EVENT' } | + + { type: 'SET_EVENT_DRAG', state: EventInteractionState } | + { type: 'UNSET_EVENT_DRAG' } | + + { type: 'SET_EVENT_RESIZE', state: EventInteractionState } | + { type: 'UNSET_EVENT_RESIZE' } | + + { type: 'ADD_EVENT_SOURCES', sources: EventSource<any>[] } | + { type: 'REMOVE_EVENT_SOURCE', sourceId: string } | + { type: 'REMOVE_ALL_EVENT_SOURCES' } | + + { type: 'FETCH_EVENT_SOURCES', sourceIds?: string[], isRefetch?: boolean } | // if no sourceIds, fetch all + + { type: 'RECEIVE_EVENTS', sourceId: string, fetchId: string, fetchRange: DateRange | null, rawEvents: EventInput[] } | + { + type: 'RECEIVE_EVENT_ERROR' + sourceId: string + fetchId: string + fetchRange: DateRange | null + error: Error + } | // need all these? + + { type: 'ADD_EVENTS', eventStore: EventStore } | + { type: 'RESET_EVENTS', eventStore: EventStore } | + { type: 'RESET_RAW_EVENTS', rawEvents: EventInput[], sourceId: string } | + { type: 'MERGE_EVENTS', eventStore: EventStore } | + { type: 'REMOVE_EVENTS', eventStore: EventStore } | + { type: 'REMOVE_ALL_EVENTS' } diff --git a/fullcalendar-main/packages/core/src/reducers/CalendarDataManager.ts b/fullcalendar-main/packages/core/src/reducers/CalendarDataManager.ts new file mode 100644 index 0000000..37d3a0d --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/CalendarDataManager.ts @@ -0,0 +1,707 @@ +import { buildLocale, RawLocaleInfo, organizeRawLocales, LocaleSingularArg } from '../datelib/locale.js' +import { memoize, memoizeObjArg } from '../util/memoize.js' +import { Action } from './Action.js' +import { buildBuildPluginHooks } from '../plugin-system.js' +import { PluginHooks } from '../plugin-system-struct.js' +import { DateEnv } from '../datelib/env.js' +import { CalendarImpl } from '../api/CalendarImpl.js' +import { StandardTheme } from '../theme/StandardTheme.js' +import { EventSourceHash } from '../structs/event-source.js' +import { buildViewSpecs, ViewSpec } from '../structs/view-spec.js' +import { mapHash, isPropsEqual } from '../util/object.js' +import { DateProfileGenerator, DateProfileGeneratorProps } from '../DateProfileGenerator.js' +import { reduceViewType } from './view-type.js' +import { getInitialDate, reduceCurrentDate } from './current-date.js' +import { reduceDynamicOptionOverrides } from './options.js' +import { reduceDateProfile } from './date-profile.js' +import { reduceEventSources, initEventSources, reduceEventSourcesNewTimeZone, computeEventSourcesLoading } from './eventSources.js' +import { reduceEventStore, rezoneEventStoreDates } from './eventStore.js' +import { reduceDateSelection } from './date-selection.js' +import { reduceSelectedEvent } from './selected-event.js' +import { reduceEventDrag } from './event-drag.js' +import { reduceEventResize } from './event-resize.js' +import { Emitter } from '../common/Emitter.js' +import { EventUiHash, EventUi, createEventUi } from '../component/event-ui.js' +import { EventDefHash } from '../structs/event-def.js' +import { parseToolbars } from '../toolbar-parse.js' +import { + CalendarOptionsRefined, CalendarOptions, + CALENDAR_OPTION_REFINERS, COMPLEX_OPTION_COMPARATORS, + ViewOptions, ViewOptionsRefined, + BASE_OPTION_DEFAULTS, mergeRawOptions, + BASE_OPTION_REFINERS, VIEW_OPTION_REFINERS, + CalendarListeners, CALENDAR_LISTENER_REFINERS, Dictionary, +} from '../options.js' +import { rangeContainsMarker } from '../datelib/date-range.js' +import { ViewImpl } from '../api/ViewImpl.js' +import { parseBusinessHours } from '../structs/business-hours.js' +import { globalPlugins } from '../global-plugins.js' +import { createEmptyEventStore } from '../structs/event-store.js' +import { CalendarContext } from '../CalendarContext.js' +import { CalendarDataManagerState, CalendarOptionsData, CalendarCurrentViewData, CalendarData } from './data-types.js' +import { TaskRunner } from '../util/TaskRunner.js' +import { buildTitle } from './title-formatting.js' + +export interface CalendarDataManagerProps { + optionOverrides: CalendarOptions + calendarApi: CalendarImpl + onAction?: (action: Action) => void + onData?: (data: CalendarData) => void +} + +export type ReducerFunc = ( // TODO: rename to CalendarDataInjector. move view-props-manip hook here as well? + currentState: Dictionary | null, + action: Action | null, + context: CalendarContext & CalendarDataManagerState // more than just context +) => Dictionary + +// in future refactor, do the redux-style function(state=initial) for initial-state +// also, whatever is happening in constructor, have it happen in action queue too + +export class CalendarDataManager { + private computeCurrentViewData = memoize(this._computeCurrentViewData) + private organizeRawLocales = memoize(organizeRawLocales) + private buildLocale = memoize(buildLocale) + private buildPluginHooks = buildBuildPluginHooks() + private buildDateEnv = memoize(buildDateEnv) + private buildTheme = memoize(buildTheme) + private parseToolbars = memoize(parseToolbars) + private buildViewSpecs = memoize(buildViewSpecs) + private buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator) + private buildViewApi = memoize(buildViewApi) + private buildViewUiProps = memoizeObjArg(buildViewUiProps) + private buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual) + private buildEventUiBases = memoize(buildEventUiBases) + private parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours) + private buildTitle = memoize(buildTitle) + + public emitter = new Emitter<CalendarListeners>() + private actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this)) + private props: CalendarDataManagerProps + private state: CalendarDataManagerState + private data: CalendarData + + public currentCalendarOptionsInput: CalendarOptions = {} + private currentCalendarOptionsRefined: CalendarOptionsRefined = ({} as any) + private currentViewOptionsInput: ViewOptions = {} + private currentViewOptionsRefined: ViewOptionsRefined = ({} as any) + public currentCalendarOptionsRefiners: any = {} + + private stableOptionOverrides: CalendarOptions + private stableDynamicOptionOverrides: CalendarOptions + private stableCalendarOptionsData: CalendarOptionsData + private optionsForRefining: string[] = [] + private optionsForHandling: string[] = [] + + constructor(props: CalendarDataManagerProps) { + this.props = props + this.actionRunner.pause() + + let dynamicOptionOverrides: CalendarOptions = {} + let optionsData = this.computeOptionsData( + props.optionOverrides, + dynamicOptionOverrides, + props.calendarApi, + ) + + let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView + let currentViewData = this.computeCurrentViewData( + currentViewType, + optionsData, + props.optionOverrides, + dynamicOptionOverrides, + ) + + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this + this.emitter.setThisContext(props.calendarApi) + this.emitter.setOptions(currentViewData.options) + + let currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv) + let dateProfile = currentViewData.dateProfileGenerator.build(currentDate) + + if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) { + currentDate = dateProfile.currentRange.start + } + + let calendarContext: CalendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: this.emitter, + getCurrentData: this.getCurrentData, + } + + // needs to be after setThisContext + for (let callback of optionsData.pluginHooks.contextInit) { + callback(calendarContext) + } + + // NOT DRY + let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext) + + let initialState: CalendarDataManagerState = { + dynamicOptionOverrides, + currentViewType, + currentDate, + dateProfile, + businessHours: this.parseContextBusinessHours(calendarContext), // weird to have this in state + eventSources, + eventUiBases: {}, + eventStore: createEmptyEventStore(), + renderableEventStore: createEmptyEventStore(), + dateSelection: null, + eventSelection: '', + eventDrag: null, + eventResize: null, + selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig, + } + let contextAndState = { ...calendarContext, ...initialState } + + for (let reducer of optionsData.pluginHooks.reducers) { + Object.assign(initialState, reducer(null, null, contextAndState)) + } + + if (computeIsLoading(initialState, calendarContext)) { + this.emitter.trigger('loading', true) // NOT DRY + } + + this.state = initialState + this.updateData() + this.actionRunner.resume() + } + + getCurrentData = () => this.data + + dispatch = (action: Action) => { + this.actionRunner.request(action) // protects against recursive calls to _handleAction + } + + resetOptions(optionOverrides: CalendarOptions, changedOptionNames?: string[]) { + let { props } = this + + if (changedOptionNames === undefined) { + props.optionOverrides = optionOverrides + } else { + props.optionOverrides = { ...(props.optionOverrides || {}), ...optionOverrides } + this.optionsForRefining.push(...changedOptionNames) + } + + if (changedOptionNames === undefined || changedOptionNames.length) { + this.actionRunner.request({ // hack. will cause updateData + type: 'NOTHING', + }) + } + } + + _handleAction(action: Action) { + let { props, state, emitter } = this + + let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action) + let optionsData = this.computeOptionsData( + props.optionOverrides, + dynamicOptionOverrides, + props.calendarApi, + ) + + let currentViewType = reduceViewType(state.currentViewType, action) + let currentViewData = this.computeCurrentViewData( + currentViewType, + optionsData, + props.optionOverrides, + dynamicOptionOverrides, + ) + + // wire things up + // TODO: not DRY + props.calendarApi.currentDataManager = this + emitter.setThisContext(props.calendarApi) + emitter.setOptions(currentViewData.options) + + let calendarContext: CalendarContext = { + dateEnv: optionsData.dateEnv, + options: optionsData.calendarOptions, + pluginHooks: optionsData.pluginHooks, + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter, + getCurrentData: this.getCurrentData, + } + + let { currentDate, dateProfile } = state + + if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack + dateProfile = currentViewData.dateProfileGenerator.build(currentDate) + } + + currentDate = reduceCurrentDate(currentDate, action) + dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator) + + if ( + action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator + action.type === 'NEXT' || // " + !rangeContainsMarker(dateProfile.currentRange, currentDate) + ) { + currentDate = dateProfile.currentRange.start + } + + let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext) + let eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext) + let isEventsLoading = computeEventSourcesLoading(eventSources) // BAD. also called in this func in computeIsLoading + + let renderableEventStore = + (isEventsLoading && !currentViewData.options.progressiveEventRendering) ? + (state.renderableEventStore || eventStore) : // try from previous state + eventStore + + let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext) // will memoize obj + let eventUiBySource = this.buildEventUiBySource(eventSources) + let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource) + + let newState: CalendarDataManagerState = { + dynamicOptionOverrides, + currentViewType, + currentDate, + dateProfile, + eventSources, + eventStore, + renderableEventStore, + selectionConfig, + eventUiBases, + businessHours: this.parseContextBusinessHours(calendarContext), // will memoize obj + dateSelection: reduceDateSelection(state.dateSelection, action), + eventSelection: reduceSelectedEvent(state.eventSelection, action), + eventDrag: reduceEventDrag(state.eventDrag, action), + eventResize: reduceEventResize(state.eventResize, action), + } + let contextAndState = { ...calendarContext, ...newState } + + for (let reducer of optionsData.pluginHooks.reducers) { + Object.assign(newState, reducer(state, action, contextAndState)) // give the OLD state, for old value + } + + let wasLoading = computeIsLoading(state, calendarContext) + let isLoading = computeIsLoading(newState, calendarContext) + + // TODO: use propSetHandlers in plugin system + if (!wasLoading && isLoading) { + emitter.trigger('loading', true) + } else if (wasLoading && !isLoading) { + emitter.trigger('loading', false) + } + + this.state = newState + + if (props.onAction) { + props.onAction(action) + } + } + + updateData() { + let { props, state } = this + let oldData = this.data + + let optionsData = this.computeOptionsData( + props.optionOverrides, + state.dynamicOptionOverrides, + props.calendarApi, + ) + + let currentViewData = this.computeCurrentViewData( + state.currentViewType, + optionsData, + props.optionOverrides, + state.dynamicOptionOverrides, + ) + + let data: CalendarData = this.data = { + viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), + calendarApi: props.calendarApi, + dispatch: this.dispatch, + emitter: this.emitter, + getCurrentData: this.getCurrentData, + ...optionsData, + ...currentViewData, + ...state, + } + + let changeHandlers = optionsData.pluginHooks.optionChangeHandlers + let oldCalendarOptions = oldData && oldData.calendarOptions + let newCalendarOptions = optionsData.calendarOptions + + if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) { + if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) { + // hack + state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data) + state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv) + state.renderableEventStore = data.renderableEventStore = rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv) + } + + for (let optionName in changeHandlers) { + if ( + this.optionsForHandling.indexOf(optionName) !== -1 || + oldCalendarOptions[optionName] !== newCalendarOptions[optionName] + ) { + changeHandlers[optionName](newCalendarOptions[optionName], data) + } + } + } + + this.optionsForHandling = [] + + if (props.onData) { + props.onData(data) + } + } + + computeOptionsData( + optionOverrides: CalendarOptions, + dynamicOptionOverrides: CalendarOptions, + calendarApi: CalendarImpl, + ): CalendarOptionsData { + // TODO: blacklist options that are handled by optionChangeHandlers + + if ( + !this.optionsForRefining.length && + optionOverrides === this.stableOptionOverrides && + dynamicOptionOverrides === this.stableDynamicOptionOverrides + ) { + return this.stableCalendarOptionsData + } + + let { + refinedOptions, pluginHooks, localeDefaults, availableLocaleData, extra, + } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) + + warnUnknownOptions(extra) + + let dateEnv = this.buildDateEnv( + refinedOptions.timeZone, + refinedOptions.locale, + refinedOptions.weekNumberCalculation, + refinedOptions.firstDay, + refinedOptions.weekText, + pluginHooks, + availableLocaleData, + refinedOptions.defaultRangeSeparator, + ) + + let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults) + let theme = this.buildTheme(refinedOptions, pluginHooks) + let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi) + + return this.stableCalendarOptionsData = { + calendarOptions: refinedOptions, + pluginHooks, + dateEnv, + viewSpecs, + theme, + toolbarConfig, + localeDefaults, + availableRawLocales: availableLocaleData.map, + } + } + + // always called from behind a memoizer + processRawCalendarOptions(optionOverrides: CalendarOptions, dynamicOptionOverrides: CalendarOptions) { + let { locales, locale } = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + optionOverrides, + dynamicOptionOverrides, + ]) + let availableLocaleData = this.organizeRawLocales(locales) + let availableRawLocales = availableLocaleData.map + let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options + let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins) + let refiners = this.currentCalendarOptionsRefiners = { + ...BASE_OPTION_REFINERS, + ...CALENDAR_LISTENER_REFINERS, + ...CALENDAR_OPTION_REFINERS, + ...pluginHooks.listenerRefiners, + ...pluginHooks.optionRefiners, + } + let extra = {} + + let raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + localeDefaults, + optionOverrides, + dynamicOptionOverrides, + ]) + let refined: Partial<CalendarOptionsRefined> = {} + let currentRaw = this.currentCalendarOptionsInput + let currentRefined = this.currentCalendarOptionsRefined + let anyChanges = false + + for (let optionName in raw) { + if ( + this.optionsForRefining.indexOf(optionName) === -1 && ( + raw[optionName] === currentRaw[optionName] || ( + COMPLEX_OPTION_COMPARATORS[optionName] && + (optionName in currentRaw) && + COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName]) + ) + ) + ) { + refined[optionName] = currentRefined[optionName] + } else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]) + anyChanges = true + } else { + extra[optionName] = currentRaw[optionName] + } + } + + if (anyChanges) { + this.currentCalendarOptionsInput = raw + this.currentCalendarOptionsRefined = refined as CalendarOptionsRefined + + this.stableOptionOverrides = optionOverrides + this.stableDynamicOptionOverrides = dynamicOptionOverrides + } + + this.optionsForHandling.push(...this.optionsForRefining) + this.optionsForRefining = [] + + return { + rawOptions: this.currentCalendarOptionsInput, + refinedOptions: this.currentCalendarOptionsRefined, + pluginHooks, + availableLocaleData, + localeDefaults, + extra, + } + } + + _computeCurrentViewData( + viewType: string, + optionsData: CalendarOptionsData, + optionOverrides: CalendarOptions, + dynamicOptionOverrides: CalendarOptions, + ): CalendarCurrentViewData { + let viewSpec = optionsData.viewSpecs[viewType] + + if (!viewSpec) { + throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`) + } + + let { refinedOptions, extra } = this.processRawViewOptions( + viewSpec, + optionsData.pluginHooks, + optionsData.localeDefaults, + optionOverrides, + dynamicOptionOverrides, + ) + + warnUnknownOptions(extra) + + let dateProfileGenerator = this.buildDateProfileGenerator({ + dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass as any, + duration: viewSpec.duration, + durationUnit: viewSpec.durationUnit, + usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime as any, + dateEnv: optionsData.dateEnv, + calendarApi: this.props.calendarApi, // should come from elsewhere? + slotMinTime: refinedOptions.slotMinTime, + slotMaxTime: refinedOptions.slotMaxTime, + showNonCurrentDates: refinedOptions.showNonCurrentDates, + dayCount: refinedOptions.dayCount, + dateAlignment: refinedOptions.dateAlignment, + dateIncrement: refinedOptions.dateIncrement, + hiddenDays: refinedOptions.hiddenDays, + weekends: refinedOptions.weekends, + nowInput: refinedOptions.now, + validRangeInput: refinedOptions.validRange, + visibleRangeInput: refinedOptions.visibleRange, + fixedWeekCount: refinedOptions.fixedWeekCount, + }) + + let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv) + + return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi } + } + + processRawViewOptions( + viewSpec: ViewSpec, + pluginHooks: PluginHooks, + localeDefaults: CalendarOptions, + optionOverrides: CalendarOptions, + dynamicOptionOverrides: CalendarOptions, + ) { + let raw = mergeRawOptions([ + BASE_OPTION_DEFAULTS, + viewSpec.optionDefaults, + localeDefaults, + optionOverrides, + viewSpec.optionOverrides, + dynamicOptionOverrides, + ]) + let refiners = { + ...BASE_OPTION_REFINERS, + ...CALENDAR_LISTENER_REFINERS, + ...CALENDAR_OPTION_REFINERS, + ...VIEW_OPTION_REFINERS, + ...pluginHooks.listenerRefiners, + ...pluginHooks.optionRefiners, + } + let refined: Partial<ViewOptionsRefined> = {} + let currentRaw = this.currentViewOptionsInput + let currentRefined = this.currentViewOptionsRefined + let anyChanges = false + let extra = {} + + for (let optionName in raw) { + if ( + raw[optionName] === currentRaw[optionName] || + (COMPLEX_OPTION_COMPARATORS[optionName] && + COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName])) + ) { + refined[optionName] = currentRefined[optionName] + } else { + if ( + raw[optionName] === this.currentCalendarOptionsInput[optionName] || + (COMPLEX_OPTION_COMPARATORS[optionName] && + COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName])) + ) { + if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop + refined[optionName] = this.currentCalendarOptionsRefined[optionName] + } + } else if (refiners[optionName]) { + refined[optionName] = refiners[optionName](raw[optionName]) + } else { + extra[optionName] = raw[optionName] + } + + anyChanges = true + } + } + + if (anyChanges) { + this.currentViewOptionsInput = raw + this.currentViewOptionsRefined = refined as ViewOptionsRefined + } + + return { + rawOptions: this.currentViewOptionsInput, + refinedOptions: this.currentViewOptionsRefined, + extra, + } + } +} + +function buildDateEnv( + timeZone: string, + explicitLocale: LocaleSingularArg, + weekNumberCalculation, + firstDay: number | undefined, + weekText, + pluginHooks: PluginHooks, + availableLocaleData: RawLocaleInfo, + defaultSeparator: string, +) { + let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map) + + return new DateEnv({ + calendarSystem: 'gregory', // TODO: make this a setting + timeZone, + namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl, + locale, + weekNumberCalculation, + firstDay, + weekText, + cmdFormatter: pluginHooks.cmdFormatter, + defaultSeparator, + }) +} + +function buildTheme(options: CalendarOptionsRefined, pluginHooks: PluginHooks) { + let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme + + return new ThemeClass(options) +} + +function buildDateProfileGenerator(props: DateProfileGeneratorProps): DateProfileGenerator { + let DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator + + return new DateProfileGeneratorClass(props) +} + +function buildViewApi(type: string, getCurrentData: () => CalendarData, dateEnv: DateEnv) { + return new ViewImpl(type, getCurrentData, dateEnv) +} + +function buildEventUiBySource(eventSources: EventSourceHash): EventUiHash { + return mapHash(eventSources, (eventSource) => eventSource.ui) +} + +function buildEventUiBases(eventDefs: EventDefHash, eventUiSingleBase: EventUi, eventUiBySource: EventUiHash) { + let eventUiBases: EventUiHash = { '': eventUiSingleBase } + + for (let defId in eventDefs) { + let def = eventDefs[defId] + + if (def.sourceId && eventUiBySource[def.sourceId]) { + eventUiBases[defId] = eventUiBySource[def.sourceId] + } + } + + return eventUiBases +} + +function buildViewUiProps(calendarContext: CalendarContext) { + let { options } = calendarContext + + return { + eventUiSingleBase: createEventUi( + { + display: options.eventDisplay, + editable: options.editable, // without "event" at start + startEditable: options.eventStartEditable, + durationEditable: options.eventDurationEditable, + constraint: options.eventConstraint, + overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined, + allow: options.eventAllow, + backgroundColor: options.eventBackgroundColor, + borderColor: options.eventBorderColor, + textColor: options.eventTextColor, + color: options.eventColor, + // classNames: options.eventClassNames // render hook will handle this + }, + calendarContext, + ), + selectionConfig: createEventUi( + { + constraint: options.selectConstraint, + overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined, + allow: options.selectAllow, + }, + calendarContext, + ), + } +} + +function computeIsLoading(state: CalendarDataManagerState, context: CalendarContext) { + for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) { + if (isLoadingFunc(state)) { + return true + } + } + + return false +} + +function parseContextBusinessHours(calendarContext: CalendarContext) { + return parseBusinessHours(calendarContext.options.businessHours, calendarContext) +} + +function warnUnknownOptions(options: any, viewName?: string) { + for (let optionName in options) { + console.warn( + `Unknown option '${optionName}'` + + (viewName ? ` for view '${viewName}'` : ''), + ) + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/current-date.ts b/fullcalendar-main/packages/core/src/reducers/current-date.ts new file mode 100644 index 0000000..4f6db41 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/current-date.ts @@ -0,0 +1,35 @@ +import { DateEnv, DateInput } from '../datelib/env.js' +import { DateMarker } from '../datelib/marker.js' +import { Action } from './Action.js' +import { BaseOptionsRefined } from '../options.js' + +export function reduceCurrentDate(currentDate: DateMarker, action: Action) { + switch (action.type) { + case 'CHANGE_DATE': + return action.dateMarker + default: + return currentDate + } +} + +export function getInitialDate(options: BaseOptionsRefined, dateEnv: DateEnv) { + let initialDateInput = options.initialDate + + // compute the initial ambig-timezone date + if (initialDateInput != null) { + return dateEnv.createMarker(initialDateInput) + } + return getNow(options.now, dateEnv) // getNow already returns unzoned +} + +export function getNow(nowInput: DateInput | (() => DateInput), dateEnv: DateEnv) { + if (typeof nowInput === 'function') { + nowInput = nowInput() + } + + if (nowInput == null) { + return dateEnv.createNowMarker() + } + + return dateEnv.createMarker(nowInput) +} diff --git a/fullcalendar-main/packages/core/src/reducers/data-types.ts b/fullcalendar-main/packages/core/src/reducers/data-types.ts new file mode 100644 index 0000000..25cfd84 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/data-types.ts @@ -0,0 +1,63 @@ +import { Action } from './Action.js' +import { PluginHooks } from '../plugin-system-struct.js' +import { DateEnv } from '../datelib/env.js' +import { CalendarImpl } from '../api/CalendarImpl.js' +import { EventSourceHash } from '../structs/event-source.js' +import { ViewSpecHash, ViewSpec } from '../structs/view-spec.js' +import { DateProfileGenerator, DateProfile } from '../DateProfileGenerator.js' +import { Emitter } from '../common/Emitter.js' +import { EventUiHash, EventUi } from '../component/event-ui.js' +import { DateMarker } from '../datelib/marker.js' +import { ViewImpl } from '../api/ViewImpl.js' +import { Theme } from '../theme/Theme.js' +import { EventStore } from '../structs/event-store.js' +import { DateSpan } from '../structs/date-span.js' +import { EventInteractionState } from '../interactions/event-interaction-state.js' +import { CalendarOptionsRefined, ViewOptionsRefined, CalendarOptions, CalendarListeners } from '../options.js' + +export interface CalendarDataManagerState { + dynamicOptionOverrides: CalendarOptions + currentViewType: string + currentDate: DateMarker + dateProfile: DateProfile + businessHours: EventStore + eventSources: EventSourceHash + eventUiBases: EventUiHash + eventStore: EventStore + renderableEventStore: EventStore + dateSelection: DateSpan | null + eventSelection: string + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null + selectionConfig: EventUi +} + +export interface CalendarOptionsData { + localeDefaults: CalendarOptions + calendarOptions: CalendarOptionsRefined + toolbarConfig: any + availableRawLocales: any + dateEnv: DateEnv + theme: Theme + pluginHooks: PluginHooks + viewSpecs: ViewSpecHash +} + +export interface CalendarCurrentViewData { + viewSpec: ViewSpec + options: ViewOptionsRefined + viewApi: ViewImpl + dateProfileGenerator: DateProfileGenerator +} + +type CalendarDataBase = CalendarOptionsData & CalendarCurrentViewData & CalendarDataManagerState + +// needs to be an interface so we can ambient-extend +// is a superset of CalendarContext +export interface CalendarData extends CalendarDataBase { + viewTitle: string // based on current date + calendarApi: CalendarImpl // TODO: try to remove this + dispatch: (action: Action) => void + emitter: Emitter<CalendarListeners> + getCurrentData(): CalendarData // TODO: try to remove +} diff --git a/fullcalendar-main/packages/core/src/reducers/date-profile.ts b/fullcalendar-main/packages/core/src/reducers/date-profile.ts new file mode 100644 index 0000000..910b2d3 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/date-profile.ts @@ -0,0 +1,36 @@ +import { DateProfile, DateProfileGenerator } from '../DateProfileGenerator.js' +import { Action } from './Action.js' +import { DateMarker } from '../datelib/marker.js' + +export function reduceDateProfile( + currentDateProfile: DateProfile | null, + action: Action, + currentDate: DateMarker, + dateProfileGenerator: DateProfileGenerator, +): DateProfile { + let dp: DateProfile + + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + return dateProfileGenerator.build(action.dateMarker || currentDate) + + case 'CHANGE_DATE': + return dateProfileGenerator.build(action.dateMarker) + + case 'PREV': + dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate) + if (dp.isValid) { + return dp + } + break + + case 'NEXT': + dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate) + if (dp.isValid) { + return dp + } + break + } + + return currentDateProfile +} diff --git a/fullcalendar-main/packages/core/src/reducers/date-selection.ts b/fullcalendar-main/packages/core/src/reducers/date-selection.ts new file mode 100644 index 0000000..baa8d64 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/date-selection.ts @@ -0,0 +1,15 @@ +import { DateSpan } from '../structs/date-span.js' +import { Action } from './Action.js' + +export function reduceDateSelection(currentSelection: DateSpan | null, action: Action) { + switch (action.type) { + case 'UNSELECT_DATES': + return null + + case 'SELECT_DATES': + return action.selection + + default: + return currentSelection + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/event-drag.ts b/fullcalendar-main/packages/core/src/reducers/event-drag.ts new file mode 100644 index 0000000..ac28eef --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/event-drag.ts @@ -0,0 +1,23 @@ +import { Action } from './Action.js' +import { EventInteractionState } from '../interactions/event-interaction-state.js' + +export function reduceEventDrag(currentDrag: EventInteractionState | null, action: Action): EventInteractionState | null { + let newDrag: EventInteractionState + + switch (action.type) { + case 'UNSET_EVENT_DRAG': + return null + + case 'SET_EVENT_DRAG': + newDrag = action.state + + return { + affectedEvents: newDrag.affectedEvents, + mutatedEvents: newDrag.mutatedEvents, + isEvent: newDrag.isEvent, + } + + default: + return currentDrag + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/event-resize.ts b/fullcalendar-main/packages/core/src/reducers/event-resize.ts new file mode 100644 index 0000000..b3a38b7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/event-resize.ts @@ -0,0 +1,23 @@ +import { EventInteractionState } from '../interactions/event-interaction-state.js' +import { Action } from './Action.js' + +export function reduceEventResize(currentResize: EventInteractionState | null, action: Action): EventInteractionState | null { + let newResize: EventInteractionState + + switch (action.type) { + case 'UNSET_EVENT_RESIZE': + return null + + case 'SET_EVENT_RESIZE': + newResize = action.state + + return { + affectedEvents: newResize.affectedEvents, + mutatedEvents: newResize.mutatedEvents, + isEvent: newResize.isEvent, + } + + default: + return currentResize + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/eventSources.ts b/fullcalendar-main/packages/core/src/reducers/eventSources.ts new file mode 100644 index 0000000..8031af1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/eventSources.ts @@ -0,0 +1,273 @@ +import { EventSource, EventSourceHash } from '../structs/event-source.js' +import { parseEventSource, buildEventSourceRefiners } from '../structs/event-source-parse.js' +import { arrayToHash, filterHash } from '../util/object.js' +import { DateRange } from '../datelib/date-range.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { Action } from './Action.js' +import { guid } from '../util/misc.js' +import { CalendarContext } from '../CalendarContext.js' +import { CalendarOptions } from '../options.js' + +export function initEventSources(calendarOptions, dateProfile: DateProfile, context: CalendarContext) { + let activeRange = dateProfile ? dateProfile.activeRange : null + + return addSources( + {}, + parseInitialSources(calendarOptions, context), + activeRange, + context, + ) +} + +export function reduceEventSources( + eventSources: EventSourceHash, + action: Action, + dateProfile: DateProfile, + context: CalendarContext, +): EventSourceHash { + let activeRange = dateProfile ? dateProfile.activeRange : null // need this check? + + switch (action.type) { + case 'ADD_EVENT_SOURCES': // already parsed + return addSources(eventSources, action.sources, activeRange, context) + + case 'REMOVE_EVENT_SOURCE': + return removeSource(eventSources, action.sourceId) + + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return fetchDirtySources(eventSources, activeRange, context) + } + return eventSources + + case 'FETCH_EVENT_SOURCES': + return fetchSourcesByIds( + eventSources, + (action as any).sourceIds ? // why no type? + arrayToHash((action as any).sourceIds) : + excludeStaticSources(eventSources, context), + activeRange, + action.isRefetch || false, + context, + ) + + case 'RECEIVE_EVENTS': + case 'RECEIVE_EVENT_ERROR': + return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange) + + case 'REMOVE_ALL_EVENT_SOURCES': + return {} + + default: + return eventSources + } +} + +export function reduceEventSourcesNewTimeZone(eventSources: EventSourceHash, dateProfile: DateProfile, context: CalendarContext) { + let activeRange = dateProfile ? dateProfile.activeRange : null // need this check? + + return fetchSourcesByIds( + eventSources, + excludeStaticSources(eventSources, context), + activeRange, + true, + context, + ) +} + +export function computeEventSourcesLoading(eventSources: EventSourceHash): boolean { + for (let sourceId in eventSources) { + if (eventSources[sourceId].isFetching) { + return true + } + } + + return false +} + +function addSources( + eventSourceHash: EventSourceHash, + sources: EventSource<any>[], + fetchRange: DateRange | null, + context: CalendarContext, +): EventSourceHash { + let hash: EventSourceHash = {} + + for (let source of sources) { + hash[source.sourceId] = source + } + + if (fetchRange) { + hash = fetchDirtySources(hash, fetchRange, context) + } + + return { ...eventSourceHash, ...hash } +} + +function removeSource(eventSourceHash: EventSourceHash, sourceId: string): EventSourceHash { + return filterHash(eventSourceHash, (eventSource: EventSource<any>) => eventSource.sourceId !== sourceId) +} + +function fetchDirtySources(sourceHash: EventSourceHash, fetchRange: DateRange, context: CalendarContext): EventSourceHash { + return fetchSourcesByIds( + sourceHash, + filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), + fetchRange, + false, + context, + ) +} + +function isSourceDirty(eventSource: EventSource<any>, fetchRange: DateRange, context: CalendarContext) { + if (!doesSourceNeedRange(eventSource, context)) { + return !eventSource.latestFetchId + } + return !context.options.lazyFetching || + !eventSource.fetchRange || + eventSource.isFetching || // always cancel outdated in-progress fetches + fetchRange.start < eventSource.fetchRange.start || + fetchRange.end > eventSource.fetchRange.end +} + +function fetchSourcesByIds( + prevSources: EventSourceHash, + sourceIdHash: { [sourceId: string]: any }, + fetchRange: DateRange, + isRefetch: boolean, + context: CalendarContext, +): EventSourceHash { + let nextSources: EventSourceHash = {} + + for (let sourceId in prevSources) { + let source = prevSources[sourceId] + + if (sourceIdHash[sourceId]) { + nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context) + } else { + nextSources[sourceId] = source + } + } + + return nextSources +} + +function fetchSource(eventSource: EventSource<any>, fetchRange: DateRange, isRefetch: boolean, context: CalendarContext) { + let { options, calendarApi } = context + let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId] + let fetchId = guid() + + sourceDef.fetch( + { + eventSource, + range: fetchRange, + isRefetch, + context, + }, + (res) => { + let { rawEvents } = res + + if (options.eventSourceSuccess) { + rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents + } + + if (eventSource.success) { + rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents + } + + context.dispatch({ + type: 'RECEIVE_EVENTS', + sourceId: eventSource.sourceId, + fetchId, + fetchRange, + rawEvents, + }) + }, + (error) => { + let errorHandled = false + + if (options.eventSourceFailure) { + options.eventSourceFailure.call(calendarApi, error) + errorHandled = true + } + + if (eventSource.failure) { + eventSource.failure(error) + errorHandled = true + } + + if (!errorHandled) { + console.warn(error.message, error) + } + + context.dispatch({ + type: 'RECEIVE_EVENT_ERROR', + sourceId: eventSource.sourceId, + fetchId, + fetchRange, + error, + }) + }, + ) + + return { + ...eventSource, + isFetching: true, + latestFetchId: fetchId, + } +} + +function receiveResponse(sourceHash: EventSourceHash, sourceId: string, fetchId: string, fetchRange: DateRange) { + let eventSource: EventSource<any> = sourceHash[sourceId] + + if ( + eventSource && // not already removed + fetchId === eventSource.latestFetchId + ) { + return { + ...sourceHash, + [sourceId]: { + ...eventSource, + isFetching: false, + fetchRange, // also serves as a marker that at least one fetch has completed + }, + } + } + + return sourceHash +} + +function excludeStaticSources(eventSources: EventSourceHash, context: CalendarContext): EventSourceHash { + return filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context)) +} + +function parseInitialSources(rawOptions: CalendarOptions, context: CalendarContext) { + let refiners = buildEventSourceRefiners(context) + let rawSources = [].concat(rawOptions.eventSources || []) + let sources = [] // parsed + + if (rawOptions.initialEvents) { + rawSources.unshift(rawOptions.initialEvents) + } + + if (rawOptions.events) { + rawSources.unshift(rawOptions.events) + } + + for (let rawSource of rawSources) { + let source = parseEventSource(rawSource, context, refiners) + if (source) { + sources.push(source) + } + } + + return sources +} + +function doesSourceNeedRange(eventSource: EventSource<any>, context: CalendarContext) { + let defs = context.pluginHooks.eventSourceDefs + + return !defs[eventSource.sourceDefId].ignoreRange +} diff --git a/fullcalendar-main/packages/core/src/reducers/eventStore.ts b/fullcalendar-main/packages/core/src/reducers/eventStore.ts new file mode 100644 index 0000000..5ca9fe4 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/eventStore.ts @@ -0,0 +1,256 @@ +import { filterHash, mapHash } from '../util/object.js' +import { EventDef } from '../structs/event-def.js' +import { EventInstance, EventInstanceHash } from '../structs/event-instance.js' +import { EventInput } from '../structs/event-parse.js' +import { + EventStore, + mergeEventStores, + createEmptyEventStore, + filterEventStoreDefs, + excludeSubEventStore, + parseEvents, +} from '../structs/event-store.js' +import { Action } from './Action.js' +import { EventSourceHash, EventSource } from '../structs/event-source.js' +import { DateRange } from '../datelib/date-range.js' +import { DateProfile } from '../DateProfileGenerator.js' +import { DateEnv } from '../datelib/env.js' +import { CalendarContext } from '../CalendarContext.js' +import { expandRecurring } from '../structs/recurring-event.js' + +export function reduceEventStore( + eventStore: EventStore, + action: Action, + eventSources: EventSourceHash, + dateProfile: DateProfile, + context: CalendarContext, +): EventStore { + switch (action.type) { + case 'RECEIVE_EVENTS': // raw + return receiveRawEvents( + eventStore, + eventSources[action.sourceId], + action.fetchId, + action.fetchRange, + action.rawEvents, + context, + ) + + case 'RESET_RAW_EVENTS': + return resetRawEvents( + eventStore, + eventSources[action.sourceId], + action.rawEvents, + dateProfile.activeRange, + context, + ) + + case 'ADD_EVENTS': // already parsed, but not expanded + return addEvent( + eventStore, + action.eventStore, // new ones + dateProfile ? dateProfile.activeRange : null, + context, + ) + + case 'RESET_EVENTS': + return action.eventStore + + case 'MERGE_EVENTS': // already parsed and expanded + return mergeEventStores(eventStore, action.eventStore) + + case 'PREV': // TODO: how do we track all actions that affect dateProfile :( + case 'NEXT': + case 'CHANGE_DATE': + case 'CHANGE_VIEW_TYPE': + if (dateProfile) { + return expandRecurring(eventStore, dateProfile.activeRange, context) + } + return eventStore + + case 'REMOVE_EVENTS': + return excludeSubEventStore(eventStore, action.eventStore) + + case 'REMOVE_EVENT_SOURCE': + return excludeEventsBySourceId(eventStore, action.sourceId) + + case 'REMOVE_ALL_EVENT_SOURCES': + return filterEventStoreDefs(eventStore, (eventDef: EventDef) => ( + !eventDef.sourceId // only keep events with no source id + )) + + case 'REMOVE_ALL_EVENTS': + return createEmptyEventStore() + + default: + return eventStore + } +} + +function receiveRawEvents( + eventStore: EventStore, + eventSource: EventSource<any>, + fetchId: string, + fetchRange: DateRange | null, + rawEvents: EventInput[], + context: CalendarContext, +): EventStore { + if ( + eventSource && // not already removed + fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources + ) { + let subset = parseEvents( + transformRawEvents(rawEvents, eventSource, context), + eventSource, + context, + ) + + if (fetchRange) { + subset = expandRecurring(subset, fetchRange, context) + } + + return mergeEventStores( + excludeEventsBySourceId(eventStore, eventSource.sourceId), + subset, + ) + } + + return eventStore +} + +function resetRawEvents( + existingEventStore: EventStore, + eventSource: EventSource<any>, + rawEvents: EventInput[], + activeRange: DateRange, + context: CalendarContext, +): EventStore { + const { defIdMap, instanceIdMap } = buildPublicIdMaps(existingEventStore) + + let newEventStore = parseEvents( + transformRawEvents(rawEvents, eventSource, context), + eventSource, + context, + false, + defIdMap, + instanceIdMap, + ) + + return expandRecurring(newEventStore, activeRange, context) +} + +function transformRawEvents(rawEvents, eventSource: EventSource<any>, context: CalendarContext) { + let calEachTransform = context.options.eventDataTransform + let sourceEachTransform = eventSource ? eventSource.eventDataTransform : null + + if (sourceEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform) + } + + if (calEachTransform) { + rawEvents = transformEachRawEvent(rawEvents, calEachTransform) + } + + return rawEvents +} + +function transformEachRawEvent(rawEvents, func) { + let refinedEvents + + if (!func) { + refinedEvents = rawEvents + } else { + refinedEvents = [] + + for (let rawEvent of rawEvents) { + let refinedEvent = func(rawEvent) + + if (refinedEvent) { + refinedEvents.push(refinedEvent) + } else if (refinedEvent == null) { + refinedEvents.push(rawEvent) + } // if a different falsy value, do nothing + } + } + + return refinedEvents +} + +function addEvent(eventStore: EventStore, subset: EventStore, expandRange: DateRange | null, context: CalendarContext): EventStore { + if (expandRange) { + subset = expandRecurring(subset, expandRange, context) + } + + return mergeEventStores(eventStore, subset) +} + +export function rezoneEventStoreDates(eventStore: EventStore, oldDateEnv: DateEnv, newDateEnv: DateEnv): EventStore { + let { defs } = eventStore + + let instances = mapHash(eventStore.instances, (instance: EventInstance): EventInstance => { + let def = defs[instance.defId] + + if (def.allDay) { + return instance // isn't dependent on timezone + } + return { + ...instance, + range: { + start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)), + end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo)), + }, + forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, + forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo, + } + }) + + return { defs, instances } +} + +function excludeEventsBySourceId(eventStore: EventStore, sourceId: string) { + return filterEventStoreDefs(eventStore, (eventDef: EventDef) => eventDef.sourceId !== sourceId) +} + +// QUESTION: why not just return instances? do a general object-property-exclusion util +export function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore { + return { + defs: eventStore.defs, + instances: filterHash(eventStore.instances, (instance: EventInstance) => !removals[instance.instanceId]), + } +} + +// ID reusing +// ------------------------------------------------------------------------------------------------- + +export type EventDefIdMap = { [publicId: string]: string } +export type EventInstanceIdMap = { [publicId: string]: string } + +function buildPublicIdMaps(eventStore: EventStore): { + defIdMap: EventDefIdMap + instanceIdMap: EventInstanceIdMap +} { + const { defs, instances } = eventStore + const defIdMap: EventDefIdMap = {} + const instanceIdMap: EventInstanceIdMap = {} + + for (let defId in defs) { + const def = defs[defId] + const { publicId } = def + + if (publicId) { + defIdMap[publicId] = defId + } + } + + for (let instanceId in instances) { + const instance = instances[instanceId] + const def = defs[instance.defId] + const { publicId } = def + + if (publicId) { + instanceIdMap[publicId] = instanceId + } + } + + return { defIdMap, instanceIdMap } +} diff --git a/fullcalendar-main/packages/core/src/reducers/options.ts b/fullcalendar-main/packages/core/src/reducers/options.ts new file mode 100644 index 0000000..67921ef --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/options.ts @@ -0,0 +1,10 @@ +import { Action } from './Action.js' + +export function reduceDynamicOptionOverrides(dynamicOptionOverrides, action: Action) { + switch (action.type) { + case 'SET_OPTION': + return { ...dynamicOptionOverrides, [action.optionName]: action.rawOptionValue } + default: + return dynamicOptionOverrides + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/selected-event.ts b/fullcalendar-main/packages/core/src/reducers/selected-event.ts new file mode 100644 index 0000000..68c32f5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/selected-event.ts @@ -0,0 +1,14 @@ +import { Action } from './Action.js' + +export function reduceSelectedEvent(currentInstanceId: string, action: Action): string { + switch (action.type) { + case 'UNSELECT_EVENT': + return '' + + case 'SELECT_EVENT': + return action.eventInstanceId + + default: + return currentInstanceId + } +} diff --git a/fullcalendar-main/packages/core/src/reducers/title-formatting.ts b/fullcalendar-main/packages/core/src/reducers/title-formatting.ts new file mode 100644 index 0000000..48a0677 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/title-formatting.ts @@ -0,0 +1,55 @@ +import { DateProfile } from '../DateProfileGenerator.js' +import { diffWholeDays } from '../datelib/marker.js' +import { createFormatter, FormatterInput } from '../datelib/formatting.js' +import { DateRange } from '../datelib/date-range.js' +import { DateEnv } from '../datelib/env.js' +import { BaseOptions } from '../options.js' + +// Computes what the title at the top of the calendarApi should be for this view +export function buildTitle(dateProfile: DateProfile, viewOptions: BaseOptions, dateEnv: DateEnv) { + let range: DateRange + + // for views that span a large unit of time, show the proper interval, ignoring stray days before and after + if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) { + range = dateProfile.currentRange + } else { // for day units or smaller, use the actual day range + range = dateProfile.activeRange + } + + return dateEnv.formatRange( + range.start, + range.end, + createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), + { + isEndExclusive: dateProfile.isRangeAllDay, + defaultSeparator: viewOptions.titleRangeSeparator, + }, + ) +} + +// Generates the format string that should be used to generate the title for the current date range. +// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. +function buildTitleFormat(dateProfile: DateProfile): FormatterInput { + let { currentRangeUnit } = dateProfile + + if (currentRangeUnit === 'year') { + return { year: 'numeric' } + } + + if (currentRangeUnit === 'month') { + return { year: 'numeric', month: 'long' } // like "September 2014" + } + + let days = diffWholeDays( + dateProfile.currentRange.start, + dateProfile.currentRange.end, + ) + + if (days !== null && days > 1) { + // multi-day range. shorter, like "Sep 9 - 10 2014" + return { year: 'numeric', month: 'short', day: 'numeric' } + } + + // one day. longer, like "September 9 2014" + return { year: 'numeric', month: 'long', day: 'numeric' } +} diff --git a/fullcalendar-main/packages/core/src/reducers/view-type.ts b/fullcalendar-main/packages/core/src/reducers/view-type.ts new file mode 100644 index 0000000..5eeb836 --- /dev/null +++ b/fullcalendar-main/packages/core/src/reducers/view-type.ts @@ -0,0 +1,10 @@ +import { Action } from './Action.js' + +export function reduceViewType(viewType: string, action: Action): string { + switch (action.type) { + case 'CHANGE_VIEW_TYPE': + viewType = action.viewType + } + + return viewType +} diff --git a/fullcalendar-main/packages/core/src/render-hook-misc.ts b/fullcalendar-main/packages/core/src/render-hook-misc.ts new file mode 100644 index 0000000..0591ed5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/render-hook-misc.ts @@ -0,0 +1,35 @@ +import { DateMeta } from './component/date-rendering.js' +import { Duration } from './datelib/duration.js' +import { ViewApi } from './api/ViewApi.js' +import { MountArg } from './common/render-hook.js' + +export interface SlotLaneContentArg extends Partial<DateMeta> { // TODO: move? + time?: Duration + date?: Date + view: ViewApi + // this interface is for date-specific slots AND time-general slots. make an OR? +} +export type SlotLaneMountArg = MountArg<SlotLaneContentArg> + +export interface SlotLabelContentArg { // TODO: move? + level: number + time: Duration + date: Date + view: ViewApi + text: string +} +export type SlotLabelMountArg = MountArg<SlotLabelContentArg> + +export interface AllDayContentArg { + text: string + view: ViewApi +} +export type AllDayMountArg = MountArg<AllDayContentArg> + +export interface DayHeaderContentArg extends DateMeta { + date: Date + view: ViewApi + text: string + [otherProp: string]: any +} +export type DayHeaderMountArg = MountArg<DayHeaderContentArg> diff --git a/fullcalendar-main/packages/core/src/scrollgrid/ScrollGridImpl.ts b/fullcalendar-main/packages/core/src/scrollgrid/ScrollGridImpl.ts new file mode 100644 index 0000000..b8036bf --- /dev/null +++ b/fullcalendar-main/packages/core/src/scrollgrid/ScrollGridImpl.ts @@ -0,0 +1,30 @@ +import { SectionConfig, ChunkConfig, ColProps, CssDimValue } from './util.js' +import { Component, Ref } from '../preact.js' +import { ViewContext } from '../ViewContext.js' + +export interface ScrollGridProps { + elRef?: Ref<any> + colGroups?: ColGroupConfig[] + sections: ScrollGridSectionConfig[] + liquid: boolean // liquid *height* + forPrint: boolean + collapsibleWidth: boolean // can ALL sections be fully collapsed in width? +} + +export interface ScrollGridSectionConfig extends SectionConfig { + key: string + chunks?: ScrollGridChunkConfig[] // TODO: make this mandatory, somehow also accomodate outerContent +} + +export interface ScrollGridChunkConfig extends ChunkConfig { + key: string +} + +export interface ColGroupConfig { + width?: CssDimValue + cols: ColProps[] +} + +export type ScrollGridImpl = { + new(props: ScrollGridProps, context: ViewContext): Component<ScrollGridProps> +} diff --git a/fullcalendar-main/packages/core/src/scrollgrid/Scroller.tsx b/fullcalendar-main/packages/core/src/scrollgrid/Scroller.tsx new file mode 100644 index 0000000..3aa13e1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/scrollgrid/Scroller.tsx @@ -0,0 +1,126 @@ +import { createElement, ComponentChildren, Ref } from '../preact.js' +import { BaseComponent, setRef } from '../vdom-util.js' +import { CssDimValue, ScrollerLike } from './util.js' + +export type OverflowValue = 'auto' | 'hidden' | 'scroll' | 'visible' + +export interface ScrollerProps { + elRef?: Ref<HTMLElement> + overflowX: OverflowValue + overflowY: OverflowValue + overcomeLeft?: number + overcomeRight?: number + overcomeBottom?: number + maxHeight?: CssDimValue + liquid?: boolean + liquidIsAbsolute?: boolean + children?: ComponentChildren +} + +const VISIBLE_HIDDEN_RE = /^(visible|hidden)$/ + +export class Scroller extends BaseComponent<ScrollerProps> implements ScrollerLike { + public el: HTMLElement // TODO: just use this.base? + + render() { + let { props } = this + let { liquid, liquidIsAbsolute } = props + let isAbsolute = liquid && liquidIsAbsolute + let className = ['fc-scroller'] + + if (liquid) { + if (liquidIsAbsolute) { + className.push('fc-scroller-liquid-absolute') + } else { + className.push('fc-scroller-liquid') + } + } + + return ( + <div + ref={this.handleEl} + className={className.join(' ')} + style={{ + overflowX: props.overflowX, + overflowY: props.overflowY, + left: (isAbsolute && -(props.overcomeLeft || 0)) || '', + right: (isAbsolute && -(props.overcomeRight || 0)) || '', + bottom: (isAbsolute && -(props.overcomeBottom || 0)) || '', + marginLeft: (!isAbsolute && -(props.overcomeLeft || 0)) || '', + marginRight: (!isAbsolute && -(props.overcomeRight || 0)) || '', + marginBottom: (!isAbsolute && -(props.overcomeBottom || 0)) || '', + maxHeight: props.maxHeight || '', + }} + > + {props.children} + </div> + ) + } + + handleEl = (el: HTMLElement) => { + this.el = el + setRef(this.props.elRef, el) + } + + needsXScrolling() { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return false + } + + // testing scrollWidth>clientWidth is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + + let { el } = this + let realClientWidth = this.el.getBoundingClientRect().width - this.getYScrollbarWidth() + let { children } = el + + for (let i = 0; i < children.length; i += 1) { + let childEl = children[i] + + if (childEl.getBoundingClientRect().width > realClientWidth) { + return true + } + } + + return false + } + + needsYScrolling() { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return false + } + + // testing scrollHeight>clientHeight is unreliable cross-browser when pixel heights aren't integers. + // much more reliable to see if children are taller than the scroller, even tho doesn't account for + // inner-child margins and absolute positioning + + let { el } = this + let realClientHeight = this.el.getBoundingClientRect().height - this.getXScrollbarWidth() + let { children } = el + + for (let i = 0; i < children.length; i += 1) { + let childEl = children[i] + + if (childEl.getBoundingClientRect().height > realClientHeight) { + return true + } + } + + return false + } + + getXScrollbarWidth() { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) { + return 0 + } + return this.el.offsetHeight - this.el.clientHeight // only works because we guarantee no borders. TODO: add to CSS with important? + } + + getYScrollbarWidth() { + if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) { + return 0 + } + return this.el.offsetWidth - this.el.clientWidth // only works because we guarantee no borders. TODO: add to CSS with important? + } +} diff --git a/fullcalendar-main/packages/core/src/scrollgrid/SimpleScrollGrid.tsx b/fullcalendar-main/packages/core/src/scrollgrid/SimpleScrollGrid.tsx new file mode 100644 index 0000000..884c699 --- /dev/null +++ b/fullcalendar-main/packages/core/src/scrollgrid/SimpleScrollGrid.tsx @@ -0,0 +1,277 @@ +import { VNode, createElement, Fragment } from '../preact.js' +import { BaseComponent, setRef } from '../vdom-util.js' +import { Scroller, OverflowValue } from './Scroller.js' +import { RefMap } from '../util/RefMap.js' +import { + ColProps, SectionConfig, renderMicroColGroup, computeShrinkWidth, getScrollGridClassNames, getSectionClassNames, getAllowYScrolling, + renderChunkContent, getSectionHasLiquidHeight, ChunkConfig, hasShrinkWidth, CssDimValue, + isColPropsEqual, +} from './util.js' +import { getCanVGrowWithinCell } from '../util/table-styling.js' +import { memoize } from '../util/memoize.js' +import { isPropsEqual } from '../util/object.js' +import { getScrollbarWidths } from '../util/scrollbar-width.js' + +export interface SimpleScrollGridProps { + cols: ColProps[] + sections: SimpleScrollGridSection[] + liquid: boolean // liquid *height* + collapsibleWidth: boolean // can ALL sections be fully collapsed in width? + height?: CssDimValue // TODO: give to real ScrollGrid +} + +export interface SimpleScrollGridSection extends SectionConfig { + key: string + chunk?: ChunkConfig +} + +interface SimpleScrollGridState { + shrinkWidth: number | null + forceYScrollbars: boolean + scrollerClientWidths: { [key: string]: number } + scrollerClientHeights: { [key: string]: number } +} + +export class SimpleScrollGrid extends BaseComponent<SimpleScrollGridProps, SimpleScrollGridState> { + processCols = memoize((a) => a, isColPropsEqual) // so we get same `cols` props every time + + // yucky to memoize VNodes, but much more efficient for consumers + renderMicroColGroup: typeof renderMicroColGroup = memoize(renderMicroColGroup) + + scrollerRefs = new RefMap<Scroller>() + scrollerElRefs = new RefMap<HTMLElement>(this._handleScrollerEl.bind(this)) + + state: SimpleScrollGridState = { + shrinkWidth: null, + forceYScrollbars: false, + scrollerClientWidths: {}, + scrollerClientHeights: {}, + } + + render(): VNode { + let { props, state, context } = this + let sectionConfigs = props.sections || [] + let cols = this.processCols(props.cols) + + let microColGroupNode = this.renderMicroColGroup(cols, state.shrinkWidth) + let classNames = getScrollGridClassNames(props.liquid, context) + + if (props.collapsibleWidth) { + classNames.push('fc-scrollgrid-collapsible') + } + + // TODO: make DRY + let configCnt = sectionConfigs.length + let configI = 0 + let currentConfig: SimpleScrollGridSection + let headSectionNodes: VNode[] = [] + let bodySectionNodes: VNode[] = [] + let footSectionNodes: VNode[] = [] + + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') { + headSectionNodes.push(this.renderSection(currentConfig, microColGroupNode, true)) + configI += 1 + } + + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') { + bodySectionNodes.push(this.renderSection(currentConfig, microColGroupNode, false)) + configI += 1 + } + + while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') { + footSectionNodes.push(this.renderSection(currentConfig, microColGroupNode, true)) + configI += 1 + } + + // firefox bug: when setting height on table and there is a thead or tfoot, + // the necessary height:100% on the liquid-height body section forces the *whole* table to be taller. (bug #5524) + // use getCanVGrowWithinCell as a way to detect table-stupid firefox. + // if so, use a simpler dom structure, jam everything into a lone tbody. + let isBuggy = !getCanVGrowWithinCell() + + const roleAttrs = { role: 'rowgroup' } + + return createElement( + 'table', + { + role: 'grid', + className: classNames.join(' '), + style: { height: props.height }, + }, + Boolean(!isBuggy && headSectionNodes.length) && createElement('thead', roleAttrs, ...headSectionNodes), + Boolean(!isBuggy && bodySectionNodes.length) && createElement('tbody', roleAttrs, ...bodySectionNodes), + Boolean(!isBuggy && footSectionNodes.length) && createElement('tfoot', roleAttrs, ...footSectionNodes), + isBuggy && createElement('tbody', roleAttrs, ...headSectionNodes, ...bodySectionNodes, ...footSectionNodes), + ) + } + + renderSection(sectionConfig: SimpleScrollGridSection, microColGroupNode: VNode, isHeader: boolean) { + if ('outerContent' in sectionConfig) { + return ( + <Fragment key={sectionConfig.key}> + {sectionConfig.outerContent} + </Fragment> + ) + } + + return ( + <tr + key={sectionConfig.key} + role="presentation" + className={getSectionClassNames(sectionConfig, this.props.liquid).join(' ')} + > + {this.renderChunkTd(sectionConfig, microColGroupNode, sectionConfig.chunk, isHeader)} + </tr> + ) + } + + renderChunkTd( + sectionConfig: SimpleScrollGridSection, + microColGroupNode: VNode, + chunkConfig: ChunkConfig, + isHeader: boolean, + ): createElement.JSX.Element { + if ('outerContent' in chunkConfig) { + return chunkConfig.outerContent + } + + let { props } = this + let { forceYScrollbars, scrollerClientWidths, scrollerClientHeights } = this.state + + let needsYScrolling = getAllowYScrolling(props, sectionConfig) // TODO: do lazily. do in section config? + let isLiquid = getSectionHasLiquidHeight(props, sectionConfig) + + // for `!props.liquid` - is WHOLE scrollgrid natural height? + // TODO: do same thing in advanced scrollgrid? prolly not b/c always has horizontal scrollbars + let overflowY: OverflowValue = + !props.liquid ? 'visible' : + forceYScrollbars ? 'scroll' : + !needsYScrolling ? 'hidden' : + 'auto' + + let sectionKey = sectionConfig.key + let content = renderChunkContent(sectionConfig, chunkConfig, { + tableColGroupNode: microColGroupNode, + tableMinWidth: '', + clientWidth: (!props.collapsibleWidth && scrollerClientWidths[sectionKey] !== undefined) ? scrollerClientWidths[sectionKey] : null, + clientHeight: scrollerClientHeights[sectionKey] !== undefined ? scrollerClientHeights[sectionKey] : null, + expandRows: sectionConfig.expandRows, + syncRowHeights: false, + rowSyncHeights: [], + reportRowHeightChange: () => {}, + }, isHeader) + + return createElement( + isHeader ? 'th' : 'td', + { + ref: chunkConfig.elRef as any, + role: 'presentation', + }, + <div className={`fc-scroller-harness${isLiquid ? ' fc-scroller-harness-liquid' : ''}`}> + <Scroller + ref={this.scrollerRefs.createRef(sectionKey)} + elRef={this.scrollerElRefs.createRef(sectionKey)} + overflowY={overflowY} + overflowX={!props.liquid ? 'visible' : 'hidden' /* natural height? */} + maxHeight={sectionConfig.maxHeight} + liquid={isLiquid} + liquidIsAbsolute // because its within a harness + > + {content} + </Scroller> + </div>, + ) + } + + _handleScrollerEl(scrollerEl: HTMLElement | null, key: string) { + let section = getSectionByKey(this.props.sections, key) + + if (section) { + setRef(section.chunk.scrollerElRef, scrollerEl) + } + } + + // TODO: can do a really simple print-view. dont need to join rows + handleSizing = () => { + this.safeSetState({ + shrinkWidth: this.computeShrinkWidth(), // will create each chunk's <colgroup>. TODO: precompute hasShrinkWidth + ...this.computeScrollerDims(), + }) + } + + componentDidMount() { + this.handleSizing() + this.context.addResizeHandler(this.handleSizing) + } + + componentDidUpdate() { + // TODO: need better solution when state contains non-sizing things + this.handleSizing() + } + + componentWillUnmount() { + this.context.removeResizeHandler(this.handleSizing) + } + + computeShrinkWidth() { + return hasShrinkWidth(this.props.cols) + ? computeShrinkWidth(this.scrollerElRefs.getAll()) + : 0 + } + + computeScrollerDims() { + let scrollbarWidth = getScrollbarWidths() + let { scrollerRefs, scrollerElRefs } = this + + let forceYScrollbars = false + let scrollerClientWidths: { [index: string]: number } = {} + let scrollerClientHeights: { [index: string]: number } = {} + + for (let sectionKey in scrollerRefs.currentMap) { + let scroller = scrollerRefs.currentMap[sectionKey] + + if (scroller && scroller.needsYScrolling()) { + forceYScrollbars = true + break + } + } + + for (let section of this.props.sections) { + let sectionKey = section.key + let scrollerEl = scrollerElRefs.currentMap[sectionKey] + + if (scrollerEl) { + let harnessEl = scrollerEl.parentNode as HTMLElement // TODO: weird way to get this. need harness b/c doesn't include table borders + + scrollerClientWidths[sectionKey] = Math.floor( + harnessEl.getBoundingClientRect().width - ( + forceYScrollbars + ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future + : 0 + ), + ) + + scrollerClientHeights[sectionKey] = Math.floor( + harnessEl.getBoundingClientRect().height, // never has horizontal scrollbars + ) + } + } + + return { forceYScrollbars, scrollerClientWidths, scrollerClientHeights } + } +} + +SimpleScrollGrid.addStateEquality({ + scrollerClientWidths: isPropsEqual, + scrollerClientHeights: isPropsEqual, +}) + +function getSectionByKey(sections: SimpleScrollGridSection[], key: string): SimpleScrollGridSection | null { + for (let section of sections) { + if (section.key === key) { + return section + } + } + + return null +} diff --git a/fullcalendar-main/packages/core/src/scrollgrid/util.tsx b/fullcalendar-main/packages/core/src/scrollgrid/util.tsx new file mode 100644 index 0000000..c52d7cc --- /dev/null +++ b/fullcalendar-main/packages/core/src/scrollgrid/util.tsx @@ -0,0 +1,226 @@ +import { VNode, createElement, Ref } from '../preact.js' +import { findElements } from '../util/dom-manip.js' +import { ViewContext } from '../ViewContext.js' +import { computeSmallestCellWidth } from '../util/misc.js' +import { isPropsEqual } from '../util/object.js' +import { isArraysEqual } from '../util/array.js' +import { BaseOptionsRefined } from '../options.js' + +export type CssDimValue = string | number // TODO: move to more general file + +export interface ColProps { + width?: CssDimValue + minWidth?: CssDimValue + span?: number +} + +export interface SectionConfig { + outerContent?: VNode + type: 'body' | 'header' | 'footer' + className?: string + maxHeight?: number + liquid?: boolean + expandRows?: boolean // TODO: how to get a bottom rule? + syncRowHeights?: boolean // yuck + isSticky?: boolean +} + +export type ChunkConfigContent = (contentProps: ChunkContentCallbackArgs) => VNode +export type ChunkConfigRowContent = VNode | ChunkConfigContent + +export interface ChunkConfig { + elRef?: Ref<HTMLTableCellElement> + outerContent?: VNode + content?: ChunkConfigContent + rowContent?: ChunkConfigRowContent + scrollerElRef?: Ref<HTMLDivElement> + tableClassName?: string +} + +export interface ChunkContentCallbackArgs { // TODO: util for wrapping tables!? + tableColGroupNode: VNode + tableMinWidth: CssDimValue + clientWidth: number | null // important to know whether 0 or not-yet-determined. for headless testing + clientHeight: number | null // + expandRows: boolean + syncRowHeights: boolean + rowSyncHeights: number[] + reportRowHeightChange: (rowEl: HTMLElement, isStable: boolean) => void +} + +export function computeShrinkWidth(chunkEls: HTMLElement[]) { // all in same COL! + let shrinkCells = findElements(chunkEls, '.fc-scrollgrid-shrink') + let largestWidth = 0 + + for (let shrinkCell of shrinkCells) { + largestWidth = Math.max( + largestWidth, + computeSmallestCellWidth(shrinkCell), + ) + } + + return Math.ceil(largestWidth) // <table> elements work best with integers. round up to ensure contents fits +} + +export interface ScrollerLike { // have scrollers implement? + needsYScrolling(): boolean + needsXScrolling(): boolean +} + +export function getSectionHasLiquidHeight(props: { liquid: boolean }, sectionConfig: SectionConfig) { + return props.liquid && sectionConfig.liquid // does the section do liquid-height? (need to have whole scrollgrid liquid-height as well) +} + +export function getAllowYScrolling(props: { liquid: boolean }, sectionConfig: SectionConfig) { + return sectionConfig.maxHeight != null || // if its possible for the height to max out, we might need scrollbars + getSectionHasLiquidHeight(props, sectionConfig) // if the section is liquid height, it might condense enough to require scrollbars +} + +// TODO: ONLY use `arg`. force out internal function to use same API +export function renderChunkContent( + sectionConfig: SectionConfig, + chunkConfig: ChunkConfig, + arg: ChunkContentCallbackArgs, + isHeader: boolean, +) { + let { expandRows } = arg + + let content: VNode = typeof chunkConfig.content === 'function' ? + chunkConfig.content(arg) : + createElement( + 'table', + { + role: 'presentation', + className: [ + chunkConfig.tableClassName, + sectionConfig.syncRowHeights ? 'fc-scrollgrid-sync-table' : '', + ].join(' '), + style: { + minWidth: arg.tableMinWidth, // because colMinWidths arent enough + width: arg.clientWidth, + height: expandRows ? arg.clientHeight : '', // css `height` on a <table> serves as a min-height + }, + }, + arg.tableColGroupNode, + createElement( + isHeader ? 'thead' : 'tbody', + { + role: 'presentation', + }, + typeof chunkConfig.rowContent === 'function' + ? chunkConfig.rowContent(arg) + : chunkConfig.rowContent, + ), + ) + + return content +} + +export function isColPropsEqual(cols0: ColProps[], cols1: ColProps[]) { + return isArraysEqual(cols0, cols1, isPropsEqual) +} + +export function renderMicroColGroup(cols: ColProps[], shrinkWidth?: number): VNode { + let colNodes: VNode[] = [] + + /* + for ColProps with spans, it would have been great to make a single <col span=""> + HOWEVER, Chrome was getting messing up distributing the width to <td>/<th> elements with colspans. + SOLUTION: making individual <col> elements makes Chrome behave. + */ + for (let colProps of cols) { + let span = colProps.span || 1 + + for (let i = 0; i < span; i += 1) { + colNodes.push( + <col + style={{ + width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''), + minWidth: colProps.minWidth || '', + }} + />, + ) + } + } + + return createElement('colgroup', {}, ...colNodes) +} + +export function sanitizeShrinkWidth(shrinkWidth?: number) { + /* why 4? if we do 0, it will kill any border, which are needed for computeSmallestCellWidth + 4 accounts for 2 2-pixel borders. TODO: better solution? */ + return shrinkWidth == null ? 4 : shrinkWidth +} + +export function hasShrinkWidth(cols: ColProps[]) { + for (let col of cols) { + if (col.width === 'shrink') { + return true + } + } + + return false +} + +export function getScrollGridClassNames(liquid: boolean, context: ViewContext) { + let classNames = [ + 'fc-scrollgrid', + context.theme.getClass('table'), + ] + + if (liquid) { + classNames.push('fc-scrollgrid-liquid') + } + + return classNames +} + +export function getSectionClassNames(sectionConfig: SectionConfig, wholeTableVGrow: boolean) { + let classNames = [ + 'fc-scrollgrid-section', + `fc-scrollgrid-section-${sectionConfig.type}`, + sectionConfig.className, // used? + ] + + if (wholeTableVGrow && sectionConfig.liquid && sectionConfig.maxHeight == null) { + classNames.push('fc-scrollgrid-section-liquid') + } + + if (sectionConfig.isSticky) { + classNames.push('fc-scrollgrid-section-sticky') + } + + return classNames +} + +export function renderScrollShim(arg: ChunkContentCallbackArgs) { + return ( + <div + className="fc-scrollgrid-sticky-shim" + style={{ + width: arg.clientWidth, + minWidth: arg.tableMinWidth, + }} + /> + ) +} + +export function getStickyHeaderDates(options: BaseOptionsRefined) { + let { stickyHeaderDates } = options + + if (stickyHeaderDates == null || stickyHeaderDates === 'auto') { + stickyHeaderDates = options.height === 'auto' || options.viewHeight === 'auto' + } + + return stickyHeaderDates +} + +export function getStickyFooterScrollbar(options: BaseOptionsRefined) { + let { stickyFooterScrollbar } = options + + if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') { + stickyFooterScrollbar = options.height === 'auto' || options.viewHeight === 'auto' + } + + return stickyFooterScrollbar +} diff --git a/fullcalendar-main/packages/core/src/seg-hierarchy.ts b/fullcalendar-main/packages/core/src/seg-hierarchy.ts new file mode 100644 index 0000000..70daa37 --- /dev/null +++ b/fullcalendar-main/packages/core/src/seg-hierarchy.ts @@ -0,0 +1,316 @@ +export interface SegSpan { + start: number + end: number +} + +export interface SegEntry { + index: number + thickness?: number // should be an integer + span: SegSpan +} + +// used internally. exposed for subclasses of SegHierarchy +export interface SegInsertion { + level: number // will have an equal coord, or slightly before, entries in existing level + levelCoord: number + lateral: number // where to insert in the existing level. -1 if creating a new level + touchingLevel: number // -1 if no touching + touchingLateral: number // -1 if no touching + touchingEntry: SegEntry // the last touching entry in the level + stackCnt: number +} + +export interface SegRect extends SegEntry { + thickness: number + levelCoord: number +} + +export interface SegEntryGroup { + entries: SegEntry[] + span: SegSpan +} + +export class SegHierarchy { + // settings + strictOrder: boolean = false + allowReslicing: boolean = false + maxCoord: number = -1 // -1 means no max + maxStackCnt: number = -1 // -1 means no max + + levelCoords: number[] = [] // ordered + entriesByLevel: SegEntry[][] = [] // parallel with levelCoords + stackCnts: { [entryId: string]: number } = {} // TODO: use better technique!? + + constructor( + private getEntryThickness = (entry: SegEntry): number => { + // if no thickness known, assume 1 (if 0, so small it always fits) + return entry.thickness || 1 + }, + ) {} + + addSegs(inputs: SegEntry[]): SegEntry[] { + let hiddenEntries: SegEntry[] = [] + + for (let input of inputs) { + this.insertEntry(input, hiddenEntries) + } + + return hiddenEntries + } + + insertEntry(entry: SegEntry, hiddenEntries: SegEntry[]): void { + let insertion = this.findInsertion(entry) + + if (this.isInsertionValid(insertion, entry)) { + this.insertEntryAt(entry, insertion) + } else { + this.handleInvalidInsertion(insertion, entry, hiddenEntries) + } + } + + isInsertionValid(insertion: SegInsertion, entry: SegEntry): boolean { + return (this.maxCoord === -1 || insertion.levelCoord + this.getEntryThickness(entry) <= this.maxCoord) && + (this.maxStackCnt === -1 || insertion.stackCnt < this.maxStackCnt) + } + + handleInvalidInsertion(insertion: SegInsertion, entry: SegEntry, hiddenEntries: SegEntry[]): void { + if (this.allowReslicing && insertion.touchingEntry) { + const hiddenEntry = { + ...entry, + span: intersectSpans(entry.span, insertion.touchingEntry.span), + } + + hiddenEntries.push(hiddenEntry) + this.splitEntry(entry, insertion.touchingEntry, hiddenEntries) + } else { + hiddenEntries.push(entry) + } + } + + /* + Does NOT add what hit the `barrier` into hiddenEntries. Should already be done. + */ + splitEntry(entry: SegEntry, barrier: SegEntry, hiddenEntries: SegEntry[]): void { + let entrySpan = entry.span + let barrierSpan = barrier.span + + if (entrySpan.start < barrierSpan.start) { + this.insertEntry({ + index: entry.index, + thickness: entry.thickness, + span: { start: entrySpan.start, end: barrierSpan.start }, + }, hiddenEntries) + } + + if (entrySpan.end > barrierSpan.end) { + this.insertEntry({ + index: entry.index, + thickness: entry.thickness, + span: { start: barrierSpan.end, end: entrySpan.end }, + }, hiddenEntries) + } + } + + insertEntryAt(entry: SegEntry, insertion: SegInsertion): void { + let { entriesByLevel, levelCoords } = this + + if (insertion.lateral === -1) { + // create a new level + insertAt(levelCoords, insertion.level, insertion.levelCoord) + insertAt(entriesByLevel, insertion.level, [entry]) + } else { + // insert into existing level + insertAt(entriesByLevel[insertion.level], insertion.lateral, entry) + } + + this.stackCnts[buildEntryKey(entry)] = insertion.stackCnt + } + + /* + does not care about limits + */ + findInsertion(newEntry: SegEntry): SegInsertion { + let { levelCoords, entriesByLevel, strictOrder, stackCnts } = this + let levelCnt = levelCoords.length + let candidateCoord = 0 + let touchingLevel: number = -1 + let touchingLateral: number = -1 + let touchingEntry: SegEntry = null + let stackCnt = 0 + + for (let trackingLevel = 0; trackingLevel < levelCnt; trackingLevel += 1) { + const trackingCoord = levelCoords[trackingLevel] + + // if the current level is past the placed entry, we have found a good empty space and can stop. + // if strictOrder, keep finding more lateral intersections. + if (!strictOrder && trackingCoord >= candidateCoord + this.getEntryThickness(newEntry)) { + break + } + + let trackingEntries = entriesByLevel[trackingLevel] + let trackingEntry: SegEntry + let searchRes = binarySearch(trackingEntries, newEntry.span.start, getEntrySpanEnd) // find first entry after newEntry's end + let lateralIndex = searchRes[0] + searchRes[1] // if exact match (which doesn't collide), go to next one + + while ( // loop through entries that horizontally intersect + (trackingEntry = trackingEntries[lateralIndex]) && // but not past the whole entry list + trackingEntry.span.start < newEntry.span.end // and not entirely past newEntry + ) { + let trackingEntryBottom = trackingCoord + this.getEntryThickness(trackingEntry) + // intersects into the top of the candidate? + if (trackingEntryBottom > candidateCoord) { + candidateCoord = trackingEntryBottom + touchingEntry = trackingEntry + touchingLevel = trackingLevel + touchingLateral = lateralIndex + } + // butts up against top of candidate? (will happen if just intersected as well) + if (trackingEntryBottom === candidateCoord) { + // accumulate the highest possible stackCnt of the trackingEntries that butt up + stackCnt = Math.max(stackCnt, stackCnts[buildEntryKey(trackingEntry)] + 1) + } + lateralIndex += 1 + } + } + + // the destination level will be after touchingEntry's level. find it + let destLevel = 0 + if (touchingEntry) { + destLevel = touchingLevel + 1 + while (destLevel < levelCnt && levelCoords[destLevel] < candidateCoord) { + destLevel += 1 + } + } + + // if adding to an existing level, find where to insert + let destLateral = -1 + if (destLevel < levelCnt && levelCoords[destLevel] === candidateCoord) { + destLateral = binarySearch(entriesByLevel[destLevel], newEntry.span.end, getEntrySpanEnd)[0] + } + + return { + touchingLevel, + touchingLateral, + touchingEntry, + stackCnt, + levelCoord: candidateCoord, + level: destLevel, + lateral: destLateral, + } + } + + // sorted by levelCoord (lowest to highest) + toRects(): SegRect[] { + let { entriesByLevel, levelCoords } = this + let levelCnt = entriesByLevel.length + let rects: SegRect[] = [] + + for (let level = 0; level < levelCnt; level += 1) { + let entries = entriesByLevel[level] + let levelCoord = levelCoords[level] + + for (let entry of entries) { + rects.push({ + ...entry, + thickness: this.getEntryThickness(entry), + levelCoord, + }) + } + } + + return rects + } +} + +export function getEntrySpanEnd(entry: SegEntry) { + return entry.span.end +} + +export function buildEntryKey(entry: SegEntry) { // TODO: use Map instead? + return entry.index + ':' + entry.span.start +} + +// returns groups with entries sorted by input order +export function groupIntersectingEntries(entries: SegEntry[]): SegEntryGroup[] { + let merges: SegEntryGroup[] = [] + + for (let entry of entries) { + let filteredMerges: SegEntryGroup[] = [] + let hungryMerge: SegEntryGroup = { // the merge that will eat what it collides with + span: entry.span, + entries: [entry], + } + + for (let merge of merges) { + if (intersectSpans(merge.span, hungryMerge.span)) { + hungryMerge = { + entries: merge.entries.concat(hungryMerge.entries), // keep preexisting merge's items first. maintains order + span: joinSpans(merge.span, hungryMerge.span), + } + } else { + filteredMerges.push(merge) + } + } + + filteredMerges.push(hungryMerge) + merges = filteredMerges + } + + return merges +} + +export function joinSpans(span0: SegSpan, span1: SegSpan): SegSpan { + return { + start: Math.min(span0.start, span1.start), + end: Math.max(span0.end, span1.end), + } +} + +export function intersectSpans(span0: SegSpan, span1: SegSpan): SegSpan | null { + let start = Math.max(span0.start, span1.start) + let end = Math.min(span0.end, span1.end) + + if (start < end) { + return { start, end } + } + + return null +} + +// general util +// --------------------------------------------------------------------------------------------------------------------- + +function insertAt<Item>(arr: Item[], index: number, item: Item) { + arr.splice(index, 0, item) +} + +export function binarySearch<Item>( + a: Item[], + searchVal: number, + getItemVal: (item: Item) => number, +): [number, number] { // returns [level, isExactMatch ? 1 : 0] + let startIndex = 0 + let endIndex = a.length // exclusive + + if (!endIndex || searchVal < getItemVal(a[startIndex])) { // no items OR before first item + return [0, 0] + } + if (searchVal > getItemVal(a[endIndex - 1])) { // after last item + return [endIndex, 0] + } + + while (startIndex < endIndex) { + let middleIndex = Math.floor(startIndex + (endIndex - startIndex) / 2) + let middleVal = getItemVal(a[middleIndex]) + + if (searchVal < middleVal) { + endIndex = middleIndex + } else if (searchVal > middleVal) { + startIndex = middleIndex + 1 + } else { // equal! + return [middleIndex, 1] + } + } + + return [startIndex, 0] +} diff --git a/fullcalendar-main/packages/core/src/structs/business-hours.ts b/fullcalendar-main/packages/core/src/structs/business-hours.ts new file mode 100644 index 0000000..b6ee007 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/business-hours.ts @@ -0,0 +1,49 @@ +import { EventInput } from './event-parse.js' +import { EventStore, parseEvents } from './event-store.js' +import { CalendarContext } from '../CalendarContext.js' + +/* +Utils for converting raw business hour input into an EventStore, +for both rendering and the constraint system. +*/ + +export type BusinessHoursInput = boolean | EventInput | EventInput[] + +const DEF_DEFAULTS = { + startTime: '09:00', + endTime: '17:00', + daysOfWeek: [1, 2, 3, 4, 5], // monday - friday + display: 'inverse-background', + classNames: 'fc-non-business', + groupId: '_businessHours', // so multiple defs get grouped +} + +/* +TODO: pass around as EventDefHash!!! +*/ +export function parseBusinessHours(input: BusinessHoursInput, context: CalendarContext): EventStore { + return parseEvents( + refineInputs(input), + null, + context, + ) +} + +function refineInputs(input: BusinessHoursInput) { + let rawDefs + + if (input === true) { + rawDefs = [{}] // will get DEF_DEFAULTS verbatim + } else if (Array.isArray(input)) { + // if specifying an array, every sub-definition NEEDS a day-of-week + rawDefs = input.filter((rawDef) => rawDef.daysOfWeek) + } else if (typeof input === 'object' && input) { // non-null object + rawDefs = [input] + } else { // is probably false + rawDefs = [] + } + + rawDefs = rawDefs.map((rawDef) => ({ ...DEF_DEFAULTS, ...rawDef })) + + return rawDefs +} diff --git a/fullcalendar-main/packages/core/src/structs/constraint.ts b/fullcalendar-main/packages/core/src/structs/constraint.ts new file mode 100644 index 0000000..102dd6d --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/constraint.ts @@ -0,0 +1,24 @@ +import { EventStore, parseEvents } from './event-store.js' +import { EventInput } from './event-parse.js' +import { DateSpanApi } from './date-span.js' +import { EventImpl } from '../api/EventImpl.js' +import { SplittableProps } from '../component/event-splitting.js' +import { CalendarContext } from '../CalendarContext.js' + +// TODO: rename to "criteria" ? +export type ConstraintInput = 'businessHours' | string | EventInput | EventInput[] +export type Constraint = 'businessHours' | string | EventStore | false // false means won't pass at all +export type OverlapFunc = ((stillEvent: EventImpl, movingEvent: EventImpl | null) => boolean) +export type AllowFunc = (span: DateSpanApi, movingEvent: EventImpl | null) => boolean +export type isPropsValidTester = (props: SplittableProps, context: CalendarContext) => boolean + +export function normalizeConstraint(input: ConstraintInput, context: CalendarContext): Constraint | null { + if (Array.isArray(input)) { + return parseEvents(input, null, context, true) // allowOpenRange=true + } if (typeof input === 'object' && input) { // non-null object + return parseEvents([input], null, context, true) // allowOpenRange=true + } if (input != null) { + return String(input) + } + return null +} diff --git a/fullcalendar-main/packages/core/src/structs/date-span.ts b/fullcalendar-main/packages/core/src/structs/date-span.ts new file mode 100644 index 0000000..5818b4b --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/date-span.ts @@ -0,0 +1,179 @@ +import { DateRange, rangesEqual, OpenDateRange } from '../datelib/date-range.js' +import { DateInput, DateEnv } from '../datelib/env.js' +import { Duration } from '../datelib/duration.js' +import { createEventInstance } from './event-instance.js' +import { parseEventDef, refineEventDef } from './event-parse.js' +import { EventRenderRange, compileEventUi } from '../component/event-rendering.js' +import { EventUiHash } from '../component/event-ui.js' +import { CalendarContext } from '../CalendarContext.js' +import { refineProps, identity, Identity } from '../options.js' + +/* +A data-structure for a date-range that will be visually displayed. +Contains other metadata like allDay, and anything else Components might like to store. + +TODO: in future, put otherProps in own object. +*/ + +export interface OpenDateSpanInput { + start?: DateInput + end?: DateInput + allDay?: boolean + [otherProp: string]: any +} + +export interface DateSpanInput extends OpenDateSpanInput { + start: DateInput + end: DateInput +} + +export interface OpenDateSpan { + range: OpenDateRange + allDay: boolean + [otherProp: string]: any +} + +export interface DateSpan extends OpenDateSpan { + range: DateRange +} + +export interface RangeApi { + start: Date + end: Date + startStr: string + endStr: string +} + +export interface DateSpanApi extends RangeApi { + allDay: boolean +} + +export interface RangeApiWithTimeZone extends RangeApi { + timeZone: string +} + +export interface DatePointApi { + date: Date + dateStr: string + allDay: boolean +} + +const STANDARD_PROPS = { + start: identity as Identity<DateInput>, + end: identity as Identity<DateInput>, + allDay: Boolean, +} + +export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv, defaultDuration?: Duration): DateSpan | null { + let span = parseOpenDateSpan(raw, dateEnv) + let { range } = span + + if (!range.start) { + return null + } + + if (!range.end) { + if (defaultDuration == null) { + return null + } + range.end = dateEnv.add(range.start, defaultDuration) + } + + return span as DateSpan +} + +/* +TODO: somehow combine with parseRange? +Will return null if the start/end props were present but parsed invalidly. +*/ +export function parseOpenDateSpan(raw: OpenDateSpanInput, dateEnv: DateEnv): OpenDateSpan | null { + let { refined: standardProps, extra } = refineProps(raw, STANDARD_PROPS) + let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null + let endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null + let { allDay } = standardProps + + if (allDay == null) { + allDay = (startMeta && startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified) + } + + return { + range: { + start: startMeta ? startMeta.marker : null, + end: endMeta ? endMeta.marker : null, + }, + allDay, + ...extra, + } +} + +export function isDateSpansEqual(span0: DateSpan, span1: DateSpan): boolean { + return rangesEqual(span0.range, span1.range) && + span0.allDay === span1.allDay && + isSpanPropsEqual(span0, span1) +} + +// the NON-DATE-RELATED props +function isSpanPropsEqual(span0: DateSpan, span1: DateSpan): boolean { + for (let propName in span1) { + if (propName !== 'range' && propName !== 'allDay') { + if (span0[propName] !== span1[propName]) { + return false + } + } + } + + // are there any props that span0 has that span1 DOESN'T have? + // both have range/allDay, so no need to special-case. + for (let propName in span0) { + if (!(propName in span1)) { + return false + } + } + + return true +} + +export function buildDateSpanApi(span: DateSpan, dateEnv: DateEnv): DateSpanApi { + return { + ...buildRangeApi(span.range, dateEnv, span.allDay), + allDay: span.allDay, + } +} + +export function buildRangeApiWithTimeZone(range: DateRange, dateEnv: DateEnv, omitTime?: boolean): RangeApiWithTimeZone { + return { + ...buildRangeApi(range, dateEnv, omitTime), + timeZone: dateEnv.timeZone, + } +} + +export function buildRangeApi(range: DateRange, dateEnv: DateEnv, omitTime?: boolean): RangeApi { + return { + start: dateEnv.toDate(range.start), + end: dateEnv.toDate(range.end), + startStr: dateEnv.formatIso(range.start, { omitTime }), + endStr: dateEnv.formatIso(range.end, { omitTime }), + } +} + +export function fabricateEventRange(dateSpan: DateSpan, eventUiBases: EventUiHash, context: CalendarContext): EventRenderRange { + let res = refineEventDef({ editable: false }, context) + let def = parseEventDef( + res.refined, + res.extra, + '', // sourceId + dateSpan.allDay, + true, // hasEnd + context, + ) + + return { + def, + ui: compileEventUi(def, eventUiBases), + instance: createEventInstance(def.defId, dateSpan.range), + range: dateSpan.range, + isStart: true, + isEnd: true, + } +} diff --git a/fullcalendar-main/packages/core/src/structs/drag-meta.ts b/fullcalendar-main/packages/core/src/structs/drag-meta.ts new file mode 100644 index 0000000..a52241b --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/drag-meta.ts @@ -0,0 +1,38 @@ +import { createDuration, Duration } from '../datelib/duration.js' +import { refineProps, RawOptionsFromRefiners, Dictionary } from '../options.js' + +/* +Information about what will happen when an external element is dragged-and-dropped +onto a calendar. Contains information for creating an event. +*/ + +const DRAG_META_REFINERS = { + startTime: createDuration, + duration: createDuration, + create: Boolean, + sourceId: String, +} + +export type DragMetaInput = + RawOptionsFromRefiners<typeof DRAG_META_REFINERS> & + { [otherProp: string]: any } // for leftoverProps + +export interface DragMeta { + startTime: Duration | null + duration: Duration | null + create: boolean // create an event when dropped? + sourceId: string // similar to addEvent's parameter + leftoverProps: Dictionary +} + +export function parseDragMeta(raw: DragMetaInput): DragMeta { + let { refined, extra } = refineProps(raw, DRAG_META_REFINERS) + + return { + startTime: refined.startTime || null, + duration: refined.duration || null, + create: refined.create != null ? refined.create : true, + sourceId: refined.sourceId, + leftoverProps: extra, + } +} diff --git a/fullcalendar-main/packages/core/src/structs/event-def.ts b/fullcalendar-main/packages/core/src/structs/event-def.ts new file mode 100644 index 0000000..39457f1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-def.ts @@ -0,0 +1,20 @@ +import { Duration } from '../datelib/duration.js' +import { EventUi } from '../component/event-ui.js' +import { Dictionary } from '../options.js' + +export interface EventDef { // TODO: add recurring type here? + defId: string + sourceId: string + publicId: string + groupId: string + allDay: boolean + hasEnd: boolean + recurringDef: { typeId: number, typeData: any, duration: Duration | null } | null + title: string + url: string + ui: EventUi + interactive?: boolean + extendedProps: Dictionary +} + +export type EventDefHash = { [defId: string]: EventDef } diff --git a/fullcalendar-main/packages/core/src/structs/event-instance.ts b/fullcalendar-main/packages/core/src/structs/event-instance.ts new file mode 100644 index 0000000..9169bc9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-instance.ts @@ -0,0 +1,27 @@ +import { DateRange } from '../datelib/date-range.js' +import { guid } from '../util/misc.js' + +export interface EventInstance { + instanceId: string + defId: string + range: DateRange + forcedStartTzo: number | null + forcedEndTzo: number | null +} + +export type EventInstanceHash = { [instanceId: string]: EventInstance } + +export function createEventInstance( + defId: string, + range: DateRange, + forcedStartTzo?: number, + forcedEndTzo?: number, +): EventInstance { + return { + instanceId: guid(), + defId, + range, + forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo, + forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo, + } +} diff --git a/fullcalendar-main/packages/core/src/structs/event-mutation.ts b/fullcalendar-main/packages/core/src/structs/event-mutation.ts new file mode 100644 index 0000000..1d38fa8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-mutation.ts @@ -0,0 +1,146 @@ +import { Duration } from '../datelib/duration.js' +import { EventStore, createEmptyEventStore } from './event-store.js' +import { EventDef } from './event-def.js' +import { EventInstance } from './event-instance.js' +import { computeAlignedDayRange } from '../util/date.js' +import { startOfDay } from '../datelib/marker.js' +import { EventUiHash, EventUi } from '../component/event-ui.js' +import { compileEventUis } from '../component/event-rendering.js' +import { CalendarContext } from '../CalendarContext.js' +import { getDefaultEventEnd } from '../calendar-utils.js' + +/* +A data structure for how to modify an EventDef/EventInstance within an EventStore +*/ + +export interface EventMutation { + datesDelta?: Duration // body start+end moving together. for dragging + startDelta?: Duration // for resizing + endDelta?: Duration // for resizing + standardProps?: any // for the def. should not include extendedProps + extendedProps?: any // for the def +} + +// applies the mutation to ALL defs/instances within the event store +export function applyMutationToEventStore( + eventStore: EventStore, + eventConfigBase: EventUiHash, + mutation: EventMutation, + context: CalendarContext, +): EventStore { + let eventConfigs = compileEventUis(eventStore.defs, eventConfigBase) + let dest = createEmptyEventStore() + + for (let defId in eventStore.defs) { + let def = eventStore.defs[defId] + + dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, context) + } + + for (let instanceId in eventStore.instances) { + let instance = eventStore.instances[instanceId] + let def = dest.defs[instance.defId] // important to grab the newly modified def + + dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, context) + } + + return dest +} + +export type eventDefMutationApplier = (eventDef: EventDef, mutation: EventMutation, context: CalendarContext) => void + +function applyMutationToEventDef(eventDef: EventDef, eventConfig: EventUi, mutation: EventMutation, context: CalendarContext): EventDef { + let standardProps = mutation.standardProps || {} + + // if hasEnd has not been specified, guess a good value based on deltas. + // if duration will change, there's no way the default duration will persist, + // and thus, we need to mark the event as having a real end + if ( + standardProps.hasEnd == null && + eventConfig.durationEditable && + (mutation.startDelta || mutation.endDelta) + ) { + standardProps.hasEnd = true // TODO: is this mutation okay? + } + + let copy: EventDef = { + ...eventDef, + ...standardProps, + ui: { ...eventDef.ui, ...standardProps.ui }, // the only prop we want to recursively overlay + } + + if (mutation.extendedProps) { + copy.extendedProps = { ...copy.extendedProps, ...mutation.extendedProps } + } + + for (let applier of context.pluginHooks.eventDefMutationAppliers) { + applier(copy, mutation, context) + } + + if (!copy.hasEnd && context.options.forceEventDuration) { + copy.hasEnd = true + } + + return copy +} + +function applyMutationToEventInstance( + eventInstance: EventInstance, + eventDef: EventDef, // must first be modified by applyMutationToEventDef + eventConfig: EventUi, + mutation: EventMutation, + context: CalendarContext, +): EventInstance { + let { dateEnv } = context + let forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true + let clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false + let copy = { ...eventInstance } as EventInstance + + if (forceAllDay) { + copy.range = computeAlignedDayRange(copy.range) + } + + if (mutation.datesDelta && eventConfig.startEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.datesDelta), + end: dateEnv.add(copy.range.end, mutation.datesDelta), + } + } + + if (mutation.startDelta && eventConfig.durationEditable) { + copy.range = { + start: dateEnv.add(copy.range.start, mutation.startDelta), + end: copy.range.end, + } + } + + if (mutation.endDelta && eventConfig.durationEditable) { + copy.range = { + start: copy.range.start, + end: dateEnv.add(copy.range.end, mutation.endDelta), + } + } + + if (clearEnd) { + copy.range = { + start: copy.range.start, + end: getDefaultEventEnd(eventDef.allDay, copy.range.start, context), + } + } + + // in case event was all-day but the supplied deltas were not + // better util for this? + if (eventDef.allDay) { + copy.range = { + start: startOfDay(copy.range.start), + end: startOfDay(copy.range.end), + } + } + + // handle invalid durations + if (copy.range.end < copy.range.start) { + copy.range.end = getDefaultEventEnd(eventDef.allDay, copy.range.start, context) + } + + return copy +} diff --git a/fullcalendar-main/packages/core/src/structs/event-parse.ts b/fullcalendar-main/packages/core/src/structs/event-parse.ts new file mode 100644 index 0000000..098333b --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-parse.ts @@ -0,0 +1,246 @@ +import { guid } from '../util/misc.js' +import { DateInput } from '../datelib/env.js' +import { startOfDay } from '../datelib/marker.js' +import { parseRecurring } from './recurring-event.js' +import { CalendarContext } from '../CalendarContext.js' +import { EventDef } from './event-def.js' +import { createEventInstance, EventInstance } from './event-instance.js' +import { EventSource } from './event-source.js' +import { RefinedOptionsFromRefiners, RawOptionsFromRefiners, identity, Identity, Dictionary, refineProps, GenericRefiners } from '../options.js' +import { EVENT_UI_REFINERS, createEventUi, EventUiInput, EventUiRefined } from '../component/event-ui.js' +import { EventDefIdMap, EventInstanceIdMap } from '../reducers/eventStore.js' + +export const EVENT_NON_DATE_REFINERS = { + id: String, + groupId: String, + title: String, + url: String, + interactive: Boolean, +} + +export const EVENT_DATE_REFINERS = { + start: identity as Identity<DateInput>, + end: identity as Identity<DateInput>, + date: identity as Identity<DateInput>, + allDay: Boolean, +} + +const EVENT_REFINERS = { // does NOT include EVENT_UI_REFINERS + ...EVENT_NON_DATE_REFINERS, + ...EVENT_DATE_REFINERS, + extendedProps: identity as Identity<Dictionary>, +} + +type BuiltInEventRefiners = typeof EVENT_REFINERS + +export interface EventRefiners extends BuiltInEventRefiners { + // for ambient +} + +export type EventInput = + EventUiInput & + RawOptionsFromRefiners<Required<EventRefiners>> & // Required hack + { [extendedProp: string]: any } + +export type EventRefined = + EventUiRefined & + RefinedOptionsFromRefiners<Required<EventRefiners>> // Required hack + +export interface EventTuple { + def: EventDef + instance: EventInstance | null +} + +export type EventInputTransformer = (input: EventInput) => EventInput +export type EventDefMemberAdder = (refined: EventRefined) => Partial<EventDef> + +export function parseEvent( + raw: EventInput, + eventSource: EventSource<any> | null, + context: CalendarContext, + allowOpenRange: boolean, + refiners = buildEventRefiners(context), + defIdMap?: EventDefIdMap, + instanceIdMap?: EventInstanceIdMap, +): EventTuple | null { + let { refined, extra } = refineEventDef(raw, context, refiners) + + let defaultAllDay = computeIsDefaultAllDay(eventSource, context) + let recurringRes = parseRecurring( + refined, + defaultAllDay, + context.dateEnv, + context.pluginHooks.recurringTypes, + ) + + if (recurringRes) { + let def = parseEventDef( + refined, + extra, + eventSource ? eventSource.sourceId : '', + recurringRes.allDay, + Boolean(recurringRes.duration), + context, + defIdMap, + ) + + def.recurringDef = { // don't want all the props from recurringRes. TODO: more efficient way to do this + typeId: recurringRes.typeId, + typeData: recurringRes.typeData, + duration: recurringRes.duration, + } + + return { def, instance: null } + } + + let singleRes = parseSingle(refined, defaultAllDay, context, allowOpenRange) + + if (singleRes) { + let def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', singleRes.allDay, singleRes.hasEnd, context, defIdMap) + let instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo) + + if (instanceIdMap && def.publicId && instanceIdMap[def.publicId]) { + instance.instanceId = instanceIdMap[def.publicId] + } + + return { def, instance } + } + + return null +} + +export function refineEventDef(raw: EventInput, context: CalendarContext, refiners = buildEventRefiners(context)): { + refined: RefinedOptionsFromRefiners<GenericRefiners>, + extra: Dictionary, +} { + return refineProps(raw, refiners) +} + +export function buildEventRefiners(context: CalendarContext): GenericRefiners { + return { ...EVENT_UI_REFINERS, ...EVENT_REFINERS, ...context.pluginHooks.eventRefiners } +} + +/* +Will NOT populate extendedProps with the leftover properties. +Will NOT populate date-related props. +*/ +export function parseEventDef( + refined: EventRefined, + extra: Dictionary, + sourceId: string, + allDay: boolean, + hasEnd: boolean, + context: CalendarContext, + defIdMap?: EventDefIdMap, +): EventDef { + let def: EventDef = { + title: refined.title || '', + groupId: refined.groupId || '', + publicId: refined.id || '', + url: refined.url || '', + recurringDef: null, + defId: ((defIdMap && refined.id) ? defIdMap[refined.id] : '') || guid(), + sourceId, + allDay, + hasEnd, + interactive: refined.interactive, + ui: createEventUi(refined, context), + extendedProps: { + ...(refined.extendedProps || {}), + ...extra, + }, + } + + for (let memberAdder of context.pluginHooks.eventDefMemberAdders) { + Object.assign(def, memberAdder(refined)) + } + + // help out EventImpl from having user modify props + Object.freeze(def.ui.classNames) + Object.freeze(def.extendedProps) + + return def +} + +function parseSingle(refined: EventRefined, defaultAllDay: boolean | null, context: CalendarContext, allowOpenRange?: boolean) { + let { allDay } = refined + let startMeta + let startMarker = null + let hasEnd = false + let endMeta + let endMarker = null + + let startInput = refined.start != null ? refined.start : refined.date + startMeta = context.dateEnv.createMarkerMeta(startInput) + + if (startMeta) { + startMarker = startMeta.marker + } else if (!allowOpenRange) { + return null + } + + if (refined.end != null) { + endMeta = context.dateEnv.createMarkerMeta(refined.end) + } + + if (allDay == null) { + if (defaultAllDay != null) { + allDay = defaultAllDay + } else { + // fall back to the date props LAST + allDay = (!startMeta || startMeta.isTimeUnspecified) && + (!endMeta || endMeta.isTimeUnspecified) + } + } + + if (allDay && startMarker) { + startMarker = startOfDay(startMarker) + } + + if (endMeta) { + endMarker = endMeta.marker + + if (allDay) { + endMarker = startOfDay(endMarker) + } + + if (startMarker && endMarker <= startMarker) { + endMarker = null + } + } + + if (endMarker) { + hasEnd = true + } else if (!allowOpenRange) { + hasEnd = context.options.forceEventDuration || false + + endMarker = context.dateEnv.add( + startMarker, + allDay ? + context.options.defaultAllDayEventDuration : + context.options.defaultTimedEventDuration, + ) + } + + return { + allDay, + hasEnd, + range: { start: startMarker, end: endMarker }, + forcedStartTzo: startMeta ? startMeta.forcedTzo : null, + forcedEndTzo: endMeta ? endMeta.forcedTzo : null, + } +} + +function computeIsDefaultAllDay(eventSource: EventSource<any> | null, context: CalendarContext): boolean | null { + let res = null + + if (eventSource) { + res = eventSource.defaultAllDay + } + + if (res == null) { + res = context.options.defaultAllDay + } + + return res +} diff --git a/fullcalendar-main/packages/core/src/structs/event-source-def.ts b/fullcalendar-main/packages/core/src/structs/event-source-def.ts new file mode 100644 index 0000000..7ed1949 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-source-def.ts @@ -0,0 +1,8 @@ +import { EventSourceFetcher } from './event-source.js' +import { EventSourceRefined } from './event-source-parse.js' + +export interface EventSourceDef<Meta> { + ignoreRange?: boolean + parseMeta: (refined: EventSourceRefined) => Meta | null + fetch: EventSourceFetcher<Meta> +} diff --git a/fullcalendar-main/packages/core/src/structs/event-source-parse.ts b/fullcalendar-main/packages/core/src/structs/event-source-parse.ts new file mode 100644 index 0000000..8bc1c8f --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-source-parse.ts @@ -0,0 +1,103 @@ +import { EventInput, EventInputTransformer } from './event-parse.js' +import { EventSourceFunc } from '../event-sources/func-event-source.js' +import { EventSource, EventSourceSuccessResponseHandler, EventSourceErrorResponseHandler } from './event-source.js' +import { JSON_FEED_EVENT_SOURCE_REFINERS } from '../event-sources/json-feed-event-source-refiners.js' +import { CalendarContext } from '../CalendarContext.js' +import { guid } from '../util/misc.js' +import { EVENT_UI_REFINERS, createEventUi, EventUiInput, EventUiRefined } from '../component/event-ui.js' +import { identity, Identity, RawOptionsFromRefiners, refineProps, RefinedOptionsFromRefiners } from '../options.js' + +const EVENT_SOURCE_REFINERS = { // does NOT include EVENT_UI_REFINERS + id: String, + defaultAllDay: Boolean, + url: String, + format: String, + events: identity as Identity<EventInput[] | EventSourceFunc>, // array or function + eventDataTransform: identity as Identity<EventInputTransformer>, + + // for any network-related sources + success: identity as Identity<EventSourceSuccessResponseHandler>, + failure: identity as Identity<EventSourceErrorResponseHandler>, +} + +type BuiltInEventSourceRefiners = typeof EVENT_SOURCE_REFINERS & + typeof JSON_FEED_EVENT_SOURCE_REFINERS + +export interface EventSourceRefiners extends BuiltInEventSourceRefiners { + // for extending +} + +export type EventSourceInputObject = + EventUiInput & + RawOptionsFromRefiners<Required<EventSourceRefiners>> // Required hack + +export type EventSourceInput = + EventSourceInputObject | // object in extended form + EventInput[] | + EventSourceFunc | // just a function + string // a URL for a JSON feed + +export type EventSourceRefined = + EventUiRefined & + RefinedOptionsFromRefiners<Required<EventSourceRefiners>> // Required hack + +export function parseEventSource( + raw: EventSourceInput, + context: CalendarContext, + refiners = buildEventSourceRefiners(context), +): EventSource<any> | null { + let rawObj: EventSourceInputObject + + if (typeof raw === 'string') { + rawObj = { url: raw } + } else if (typeof raw === 'function' || Array.isArray(raw)) { + rawObj = { events: raw } + } else if (typeof raw === 'object' && raw) { // not null + rawObj = raw + } + + if (rawObj) { + let { refined, extra } = refineProps(rawObj, refiners) + let metaRes = buildEventSourceMeta(refined, context) + + if (metaRes) { + return { + _raw: raw, + isFetching: false, + latestFetchId: '', + fetchRange: null, + defaultAllDay: refined.defaultAllDay, + eventDataTransform: refined.eventDataTransform, + success: refined.success, + failure: refined.failure, + publicId: refined.id || '', + sourceId: guid(), + sourceDefId: metaRes.sourceDefId, + meta: metaRes.meta, + ui: createEventUi(refined, context), + extendedProps: extra, + } + } + } + + return null +} + +export function buildEventSourceRefiners(context: CalendarContext) { + return { ...EVENT_UI_REFINERS, ...EVENT_SOURCE_REFINERS, ...context.pluginHooks.eventSourceRefiners } +} + +function buildEventSourceMeta(raw: EventSourceRefined, context: CalendarContext) { + let defs = context.pluginHooks.eventSourceDefs + + for (let i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence + let def = defs[i] + let meta = def.parseMeta(raw) + + if (meta) { + return { sourceDefId: i, meta } + } + } + + return null +} diff --git a/fullcalendar-main/packages/core/src/structs/event-source.ts b/fullcalendar-main/packages/core/src/structs/event-source.ts new file mode 100644 index 0000000..9da6ad5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-source.ts @@ -0,0 +1,48 @@ +import { EventInput, EventInputTransformer } from './event-parse.js' +import { DateRange } from '../datelib/date-range.js' +import { EventUi } from '../component/event-ui.js' +import { CalendarContext } from '../CalendarContext.js' +import { CalendarImpl } from '../api/CalendarImpl.js' +import { Dictionary } from '../options.js' + +/* +TODO: "EventSource" is the same name as a built-in type in TypeScript. Rethink. +*/ + +export type EventSourceSuccessResponseHandler = (this: CalendarImpl, rawData: any, response: any) => EventInput[] | void +export type EventSourceErrorResponseHandler = (error: Error) => void + +export interface EventSource<Meta> { + _raw: any + sourceId: string + sourceDefId: number // one of the few IDs that's a NUMBER not a string + meta: Meta + publicId: string + isFetching: boolean + latestFetchId: string + fetchRange: DateRange | null + defaultAllDay: boolean | null + eventDataTransform: EventInputTransformer // best to have this here? + ui: EventUi + success: EventSourceSuccessResponseHandler | null + failure: EventSourceErrorResponseHandler | null + extendedProps: Dictionary // undocumented +} + +export type EventSourceHash = { [sourceId: string]: EventSource<any> } + +export interface EventSourceFetcherRes { + rawEvents: EventInput[] + response?: Response +} + +export type EventSourceFetcher<Meta> = ( + arg: { + eventSource: EventSource<Meta> + range: DateRange + isRefetch: boolean + context: CalendarContext + }, + successCallback: (res: EventSourceFetcherRes) => void, + errorCallback: (error: Error) => void, +) => void diff --git a/fullcalendar-main/packages/core/src/structs/event-store.ts b/fullcalendar-main/packages/core/src/structs/event-store.ts new file mode 100644 index 0000000..bd3f925 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/event-store.ts @@ -0,0 +1,122 @@ +import { EventDef, EventDefHash } from './event-def.js' +import { EventInstance, EventInstanceHash } from './event-instance.js' +import { EventInput, parseEvent, EventTuple, buildEventRefiners } from './event-parse.js' +import { filterHash } from '../util/object.js' +import { CalendarContext } from '../CalendarContext.js' +import { EventSource } from './event-source.js' +import { EventDefIdMap, EventInstanceIdMap } from '../reducers/eventStore.js' + +/* +A data structure that encapsulates EventDefs and EventInstances. +Utils for parsing this data from raw EventInputs. +Utils for manipulating an EventStore. +*/ + +export interface EventStore { + defs: EventDefHash + instances: EventInstanceHash +} + +export function parseEvents( + rawEvents: EventInput[], + eventSource: EventSource<any> | null, + context: CalendarContext, + allowOpenRange?: boolean, + defIdMap?: EventDefIdMap, + instanceIdMap?: EventInstanceIdMap, +): EventStore { + let eventStore = createEmptyEventStore() + let eventRefiners = buildEventRefiners(context) + + for (let rawEvent of rawEvents) { + let tuple = parseEvent(rawEvent, eventSource, context, allowOpenRange, eventRefiners, defIdMap, instanceIdMap) + + if (tuple) { + eventTupleToStore(tuple, eventStore) + } + } + + return eventStore +} + +export function eventTupleToStore(tuple: EventTuple, eventStore: EventStore = createEmptyEventStore()) { + eventStore.defs[tuple.def.defId] = tuple.def + + if (tuple.instance) { + eventStore.instances[tuple.instance.instanceId] = tuple.instance + } + + return eventStore +} + +// retrieves events that have the same groupId as the instance specified by `instanceId` +// or they are the same as the instance. +// why might instanceId not be in the store? an event from another calendar? +export function getRelevantEvents(eventStore: EventStore, instanceId: string): EventStore { + let instance = eventStore.instances[instanceId] + + if (instance) { + let def = eventStore.defs[instance.defId] + + // get events/instances with same group + let newStore = filterEventStoreDefs(eventStore, (lookDef) => isEventDefsGrouped(def, lookDef)) + + // add the original + // TODO: wish we could use eventTupleToStore or something like it + newStore.defs[def.defId] = def + newStore.instances[instance.instanceId] = instance + + return newStore + } + + return createEmptyEventStore() +} + +function isEventDefsGrouped(def0: EventDef, def1: EventDef): boolean { + return Boolean(def0.groupId && def0.groupId === def1.groupId) +} + +export function createEmptyEventStore(): EventStore { + return { defs: {}, instances: {} } +} + +export function mergeEventStores(store0: EventStore, store1: EventStore): EventStore { + return { + defs: { ...store0.defs, ...store1.defs }, + instances: { ...store0.instances, ...store1.instances }, + } +} + +export function filterEventStoreDefs(eventStore: EventStore, filterFunc: (eventDef: EventDef) => boolean): EventStore { + let defs = filterHash(eventStore.defs, filterFunc) + let instances = filterHash(eventStore.instances, (instance: EventInstance) => ( + defs[instance.defId] // still exists? + )) + return { defs, instances } +} + +export function excludeSubEventStore(master: EventStore, sub: EventStore): EventStore { + let { defs, instances } = master + let filteredDefs: { [defId: string]: EventDef } = {} + let filteredInstances: { [instanceId: string]: EventInstance } = {} + + for (let defId in defs) { + if (!sub.defs[defId]) { // not explicitly excluded + filteredDefs[defId] = defs[defId] + } + } + + for (let instanceId in instances) { + if ( + !sub.instances[instanceId] && // not explicitly excluded + filteredDefs[instances[instanceId].defId] // def wasn't filtered away + ) { + filteredInstances[instanceId] = instances[instanceId] + } + } + + return { + defs: filteredDefs, + instances: filteredInstances, + } +} diff --git a/fullcalendar-main/packages/core/src/structs/recurring-event-simple-declare.ts b/fullcalendar-main/packages/core/src/structs/recurring-event-simple-declare.ts new file mode 100644 index 0000000..e1dbffc --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/recurring-event-simple-declare.ts @@ -0,0 +1,6 @@ +import { SIMPLE_RECURRING_REFINERS } from './recurring-event-simple-refiners.js' + +type ExtraRefiners = typeof SIMPLE_RECURRING_REFINERS +declare module './event-parse.js' { + interface EventRefiners extends ExtraRefiners {} +} diff --git a/fullcalendar-main/packages/core/src/structs/recurring-event-simple-refiners.ts b/fullcalendar-main/packages/core/src/structs/recurring-event-simple-refiners.ts new file mode 100644 index 0000000..bf3b332 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/recurring-event-simple-refiners.ts @@ -0,0 +1,12 @@ +import { createDuration } from '../datelib/duration.js' +import { DateInput } from '../datelib/env.js' +import { identity, Identity } from '../options.js' + +export const SIMPLE_RECURRING_REFINERS = { + daysOfWeek: identity as Identity<number[]>, + startTime: createDuration, + endTime: createDuration, + duration: createDuration, + startRecur: identity as Identity<DateInput>, + endRecur: identity as Identity<DateInput>, +} diff --git a/fullcalendar-main/packages/core/src/structs/recurring-event-simple.ts b/fullcalendar-main/packages/core/src/structs/recurring-event-simple.ts new file mode 100644 index 0000000..e084a33 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/recurring-event-simple.ts @@ -0,0 +1,109 @@ +import { startOfDay, addDays, DateMarker } from '../datelib/marker.js' +import { Duration, subtractDurations } from '../datelib/duration.js' +import { arrayToHash } from '../util/object.js' +import { RecurringType } from './recurring-event.js' +import { EventRefined } from './event-parse.js' +import { DateRange, intersectRanges } from '../datelib/date-range.js' +import { DateEnv } from '../datelib/env.js' +import { createPlugin } from '../plugin-system.js' +import { SIMPLE_RECURRING_REFINERS } from './recurring-event-simple-refiners.js' +import './recurring-event-simple-declare.js' + +/* +An implementation of recurring events that only supports every-day or weekly recurrences. +*/ + +interface SimpleRecurringData { + daysOfWeek: number[] | null + startTime: Duration | null + endTime: Duration | null + startRecur: DateMarker | null + endRecur: DateMarker | null +} + +let recurring: RecurringType<SimpleRecurringData> = { + + parse(refined: EventRefined, dateEnv: DateEnv) { + if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) { + let recurringData: SimpleRecurringData = { + daysOfWeek: refined.daysOfWeek || null, + startTime: refined.startTime || null, + endTime: refined.endTime || null, + startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null, + endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null, + } + + let duration: Duration + + if (refined.duration) { + duration = refined.duration + } + if (!duration && refined.startTime && refined.endTime) { + duration = subtractDurations(refined.endTime, refined.startTime) + } + + return { + allDayGuess: Boolean(!refined.startTime && !refined.endTime), + duration, + typeData: recurringData, // doesn't need endTime anymore but oh well + } + } + + return null + }, + + expand(typeData: SimpleRecurringData, framingRange: DateRange, dateEnv: DateEnv): DateMarker[] { + let clippedFramingRange = intersectRanges( + framingRange, + { start: typeData.startRecur, end: typeData.endRecur }, + ) + + if (clippedFramingRange) { + return expandRanges( + typeData.daysOfWeek, + typeData.startTime, + clippedFramingRange, + dateEnv, + ) + } + return [] + }, + +} + +export const simpleRecurringEventsPlugin = createPlugin({ + name: 'simple-recurring-event', + recurringTypes: [recurring], + eventRefiners: SIMPLE_RECURRING_REFINERS, +}) + +function expandRanges( + daysOfWeek: number[] | null, + startTime: Duration | null, + framingRange: DateRange, + dateEnv: DateEnv, +): DateMarker[] { + let dowHash: { [num: string]: true } | null = daysOfWeek ? arrayToHash(daysOfWeek) : null + let dayMarker = startOfDay(framingRange.start) + let endMarker = framingRange.end + let instanceStarts: DateMarker[] = [] + + while (dayMarker < endMarker) { + let instanceStart + + // if everyday, or this particular day-of-week + if (!dowHash || dowHash[dayMarker.getUTCDay()]) { + if (startTime) { + instanceStart = dateEnv.add(dayMarker, startTime) + } else { + instanceStart = dayMarker + } + + instanceStarts.push(instanceStart) + } + + dayMarker = addDays(dayMarker, 1) + } + + return instanceStarts +} diff --git a/fullcalendar-main/packages/core/src/structs/recurring-event.ts b/fullcalendar-main/packages/core/src/structs/recurring-event.ts new file mode 100644 index 0000000..3a69c7c --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/recurring-event.ts @@ -0,0 +1,121 @@ +import { EventDef } from './event-def.js' +import { EventInstance, createEventInstance } from './event-instance.js' +import { DateRange } from '../datelib/date-range.js' +import { DateEnv } from '../datelib/env.js' +import { Duration } from '../datelib/duration.js' +import { DateMarker, startOfDay } from '../datelib/marker.js' +import { EventStore } from './event-store.js' +import { CalendarContext } from '../CalendarContext.js' +import { filterHash } from '../util/object.js' +import { EventRefined } from './event-parse.js' + +/* +The plugin system for defining how a recurring event is expanded into individual instances. +*/ + +export interface ParsedRecurring<RecurringData> { + typeData: RecurringData + allDayGuess: boolean | null + duration: Duration | null // signals hasEnd +} + +export interface RecurringType<RecurringData> { + parse: (refined: EventRefined, dateEnv: DateEnv) => ParsedRecurring<RecurringData> | null // TODO: rename to post-process or something + expand: (typeData: any, framingRange: DateRange, dateEnv: DateEnv) => DateMarker[] +} + +export function parseRecurring( + refined: EventRefined, + defaultAllDay: boolean | null, + dateEnv: DateEnv, + recurringTypes: RecurringType<any>[], +) { + for (let i = 0; i < recurringTypes.length; i += 1) { + let parsed = recurringTypes[i].parse(refined, dateEnv) + + if (parsed) { + let { allDay } = refined + if (allDay == null) { + allDay = defaultAllDay + if (allDay == null) { + allDay = parsed.allDayGuess + if (allDay == null) { + allDay = false + } + } + } + + return { + allDay, + duration: parsed.duration, + typeData: parsed.typeData, + typeId: i, + } + } + } + + return null +} + +export function expandRecurring(eventStore: EventStore, framingRange: DateRange, context: CalendarContext): EventStore { + let { dateEnv, pluginHooks, options } = context + let { defs, instances } = eventStore + + // remove existing recurring instances + // TODO: bad. always expand events as a second step + instances = filterHash(instances, (instance: EventInstance) => !defs[instance.defId].recurringDef) + + for (let defId in defs) { + let def = defs[defId] + + if (def.recurringDef) { + let { duration } = def.recurringDef + + if (!duration) { + duration = def.allDay ? + options.defaultAllDayEventDuration : + options.defaultTimedEventDuration + } + + let starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes) + + for (let start of starts) { + let instance = createEventInstance(defId, { + start, + end: dateEnv.add(start, duration), + }) + instances[instance.instanceId] = instance + } + } + } + + return { defs, instances } +} + +/* +Event MUST have a recurringDef +*/ +function expandRecurringRanges( + eventDef: EventDef, + duration: Duration, + framingRange: DateRange, + dateEnv: DateEnv, + recurringTypes: RecurringType<any>[], +): DateMarker[] { + let typeDef = recurringTypes[eventDef.recurringDef.typeId] + let markers = typeDef.expand( + eventDef.recurringDef.typeData, + { + start: dateEnv.subtract(framingRange.start, duration), // for when event starts before framing range and goes into + end: framingRange.end, + }, + dateEnv, + ) + + // the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to + if (eventDef.allDay) { + markers = markers.map(startOfDay) + } + + return markers +} diff --git a/fullcalendar-main/packages/core/src/structs/view-config.tsx b/fullcalendar-main/packages/core/src/structs/view-config.tsx new file mode 100644 index 0000000..4643dfd --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/view-config.tsx @@ -0,0 +1,84 @@ +import { ViewProps } from '../View.js' +import { mapHash } from '../util/object.js' +import { ComponentType, Component, createElement } from '../preact.js' +import { buildViewClassNames } from '../common/ViewContainer.js' +import { MountArg } from '../common/render-hook.js' +import { ViewContext, ViewContextType } from '../ViewContext.js' +import { ViewOptions } from '../options.js' +import { Duration } from '../datelib/duration.js' +import { ContentContainer } from '../content-inject/ContentContainer.js' +import { BaseComponent } from '../vdom-util.js' + +/* +A view-config represents information for either: +A) creating a new instantiatable view class. in which case, supplied a class/type in addition to options, OR +B) options to customize an existing view, in which case only provides options. +*/ + +export type ViewComponent = Component<ViewProps> // an instance +export type ViewComponentType = ComponentType<ViewProps> + +export type ViewConfigInput = ViewComponentType | ViewOptions +export type ViewConfigInputHash = { [viewType: string]: ViewConfigInput } + +export interface ViewConfig { + superType: string + component: ViewComponentType | null + rawOptions: ViewOptions +} + +export type ViewConfigHash = { [viewType: string]: ViewConfig } + +export function parseViewConfigs(inputs: ViewConfigInputHash): ViewConfigHash { + return mapHash(inputs, parseViewConfig) +} + +function parseViewConfig(input: ViewConfigInput): ViewConfig { + let rawOptions: ViewOptions = typeof input === 'function' ? + { component: input } : + input + let { component } = rawOptions + + if (rawOptions.content) { + // TODO: remove content/classNames/didMount/etc from options? + component = createViewHookComponent(rawOptions) + } else if (component && !((component as any).prototype instanceof BaseComponent)) { + // WHY?: people were using `component` property for `content` + // TODO: converge on one setting name + component = createViewHookComponent({ ...rawOptions, content: component }) + } + + return { + superType: rawOptions.type as any, + component: component as any, + rawOptions, // includes type and component too :( + } +} + +export interface SpecificViewContentArg extends ViewProps { + nextDayThreshold: Duration +} + +export type SpecificViewMountArg = MountArg<SpecificViewContentArg> + +function createViewHookComponent(options: ViewOptions) { + return (viewProps: ViewProps) => ( + <ViewContextType.Consumer> + {(context: ViewContext) => ( + <ContentContainer + elTag="div" + elClasses={buildViewClassNames(context.viewSpec)} + renderProps={{ + ...viewProps, + nextDayThreshold: context.options.nextDayThreshold, + }} + generatorName={undefined} + customGenerator={options.content as any} + classNameGenerator={options.classNames as any} + didMount={options.didMount as any} + willUnmount={options.willUnmount as any} + /> + )} + </ViewContextType.Consumer> + ) +} diff --git a/fullcalendar-main/packages/core/src/structs/view-def.ts b/fullcalendar-main/packages/core/src/structs/view-def.ts new file mode 100644 index 0000000..939df45 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/view-def.ts @@ -0,0 +1,97 @@ +import { ViewConfigHash, ViewComponentType } from './view-config.js' +import { ViewOptions } from '../options.js' + +/* +Represents information for an instantiatable View class along with settings +that are specific to that view. No other settings, like calendar-wide settings, are stored. +*/ +export interface ViewDef { + type: string + component: ViewComponentType + overrides: ViewOptions + defaults: ViewOptions +} + +export type ViewDefHash = { [viewType: string]: ViewDef } + +export function compileViewDefs(defaultConfigs: ViewConfigHash, overrideConfigs: ViewConfigHash): ViewDefHash { + let hash: ViewDefHash = {} + let viewType: string + + for (viewType in defaultConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) + } + + for (viewType in overrideConfigs) { + ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) + } + + return hash +} + +function ensureViewDef( + viewType: string, + hash: ViewDefHash, + defaultConfigs: ViewConfigHash, + overrideConfigs: ViewConfigHash, +): ViewDef | null { + if (hash[viewType]) { + return hash[viewType] + } + + let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) + + if (viewDef) { + hash[viewType] = viewDef + } + + return viewDef +} + +function buildViewDef( + viewType: string, + hash: ViewDefHash, + defaultConfigs: ViewConfigHash, + overrideConfigs: ViewConfigHash, +): ViewDef | null { + let defaultConfig = defaultConfigs[viewType] + let overrideConfig = overrideConfigs[viewType] + + let queryProp = (name) => ( + (defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] : + ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null) + ) + + let theComponent = queryProp('component') as ViewComponentType + let superType = queryProp('superType') as string + let superDef: ViewDef | null = null + + if (superType) { + if (superType === viewType) { + throw new Error('Can\'t have a custom view type that references itself') + } + + superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs) + } + + if (!theComponent && superDef) { + theComponent = superDef.component + } + + if (!theComponent) { + return null // don't throw a warning, might be settings for a single-unit view + } + + return { + type: viewType, + component: theComponent, + defaults: { + ...(superDef ? superDef.defaults : {}), + ...(defaultConfig ? defaultConfig.rawOptions : {}), + }, + overrides: { + ...(superDef ? superDef.overrides : {}), + ...(overrideConfig ? overrideConfig.rawOptions : {}), + }, + } +} diff --git a/fullcalendar-main/packages/core/src/structs/view-spec.ts b/fullcalendar-main/packages/core/src/structs/view-spec.ts new file mode 100644 index 0000000..c2d6c66 --- /dev/null +++ b/fullcalendar-main/packages/core/src/structs/view-spec.ts @@ -0,0 +1,154 @@ +import { ViewDef, compileViewDefs } from './view-def.js' +import { Duration, createDuration, greatestDurationDenominator, DurationInput } from '../datelib/duration.js' +import { mapHash } from '../util/object.js' +import { ViewOptions, CalendarOptions, BASE_OPTION_DEFAULTS } from '../options.js' +import { ViewConfigInputHash, parseViewConfigs, ViewConfigHash, ViewComponentType } from './view-config.js' + +/* +Represents everything needed to instantiate a new view instance, +including options that have been compiled from view-specific and calendar-wide options, +as well as duration information. + +Overall flow: +ViewConfig -> ViewDef -> ViewSpec +*/ +export interface ViewSpec { + type: string + component: ViewComponentType + duration: Duration + durationUnit: string + singleUnit: string + optionDefaults: ViewOptions + optionOverrides: ViewOptions + buttonTextOverride: string + buttonTextDefault: string + buttonTitleOverride: string | ((...args: any[]) => string) + buttonTitleDefault: string | ((...args: any[]) => string) +} + +export type ViewSpecHash = { [viewType: string]: ViewSpec } + +export function buildViewSpecs( + defaultInputs: ViewConfigInputHash, + optionOverrides: CalendarOptions, + dynamicOptionOverrides: CalendarOptions, + localeDefaults, +): ViewSpecHash { + let defaultConfigs = parseViewConfigs(defaultInputs) + let overrideConfigs = parseViewConfigs(optionOverrides.views) + let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs) + + return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults)) +} + +function buildViewSpec( + viewDef: ViewDef, + overrideConfigs: ViewConfigHash, + optionOverrides: CalendarOptions, + dynamicOptionOverrides: CalendarOptions, + localeDefaults, +): ViewSpec { + let durationInput = + viewDef.overrides.duration || + viewDef.defaults.duration || + dynamicOptionOverrides.duration || + optionOverrides.duration + + let duration = null + let durationUnit = '' + let singleUnit = '' + let singleUnitOverrides: ViewOptions = {} + + if (durationInput) { + duration = createDurationCached(durationInput) + + if (duration) { // valid? + let denom = greatestDurationDenominator(duration) + durationUnit = denom.unit + + if (denom.value === 1) { + singleUnit = durationUnit + singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {} + } + } + } + + let queryButtonText = (optionsSubset) => { + let buttonTextMap = optionsSubset.buttonText || {} + let buttonTextKey = viewDef.defaults.buttonTextKey as string + + if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) { + return buttonTextMap[buttonTextKey] + } + if (buttonTextMap[viewDef.type] != null) { + return buttonTextMap[viewDef.type] + } + if (buttonTextMap[singleUnit] != null) { + return buttonTextMap[singleUnit] + } + return null + } + + let queryButtonTitle = (optionsSubset) => { // TODO: more DRY with queryButtonText + let buttonHints = optionsSubset.buttonHints || {} + let buttonKey = viewDef.defaults.buttonTextKey as string // use same key as text + + if (buttonKey != null && buttonHints[buttonKey] != null) { + return buttonHints[buttonKey] + } + if (buttonHints[viewDef.type] != null) { + return buttonHints[viewDef.type] + } + if (buttonHints[singleUnit] != null) { + return buttonHints[singleUnit] + } + return null + } + + return { + type: viewDef.type, + component: viewDef.component, + duration, + durationUnit, + singleUnit, + optionDefaults: viewDef.defaults, + optionOverrides: { ...singleUnitOverrides, ...viewDef.overrides }, + + buttonTextOverride: + queryButtonText(dynamicOptionOverrides) || + queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence + viewDef.overrides.buttonText, // `buttonText` for view-specific options is a string + buttonTextDefault: + queryButtonText(localeDefaults) || + viewDef.defaults.buttonText || + queryButtonText(BASE_OPTION_DEFAULTS) || + viewDef.type, // fall back to given view name + + // not DRY + buttonTitleOverride: + queryButtonTitle(dynamicOptionOverrides) || + queryButtonTitle(optionOverrides) || + viewDef.overrides.buttonHint, + buttonTitleDefault: + queryButtonTitle(localeDefaults) || + viewDef.defaults.buttonHint || + queryButtonTitle(BASE_OPTION_DEFAULTS), + // will eventually fall back to buttonText + } +} + +// hack to get memoization working + +let durationInputMap: { [json: string]: Duration } = {} + +function createDurationCached(durationInput: DurationInput) { + let json = JSON.stringify(durationInput) + let res = durationInputMap[json] + + if (res === undefined) { + res = createDuration(durationInput) + durationInputMap[json] = res + } + + return res +} diff --git a/fullcalendar-main/packages/core/src/styleUtils.ts b/fullcalendar-main/packages/core/src/styleUtils.ts new file mode 100644 index 0000000..7456373 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styleUtils.ts @@ -0,0 +1,103 @@ + +const styleTexts: string[] = [] +const styleEls = new Map<ParentNode, HTMLStyleElement>() + +export function injectStyles(styleText: string): void { + styleTexts.push(styleText) + styleEls.forEach((styleEl) => { + appendStylesTo(styleEl, styleText) + }) +} + +export function ensureElHasStyles(el: HTMLElement): void { + if ( + el.isConnected && // sometimes true if SSR system simulates DOM + el.getRootNode // sometimes undefined if SSR system simulates DOM + ) { + registerStylesRoot(el.getRootNode() as ParentNode) + } +} + +function registerStylesRoot(rootNode: ParentNode): void { + let styleEl: HTMLStyleElement = styleEls.get(rootNode) + + if (!styleEl || !styleEl.isConnected) { + styleEl = rootNode.querySelector('style[data-fullcalendar]') + + if (!styleEl) { + styleEl = document.createElement('style') + styleEl.setAttribute('data-fullcalendar', '') + + const nonce = getNonceValue() + if (nonce) { + styleEl.nonce = nonce + } + + const parentEl = rootNode === document ? document.head : rootNode + const insertBefore = rootNode === document + ? parentEl.querySelector('script,link[rel=stylesheet],link[as=style],style') + : parentEl.firstChild + + parentEl.insertBefore(styleEl, insertBefore) + } + + styleEls.set(rootNode, styleEl) + hydrateStylesRoot(styleEl) + } +} + +function hydrateStylesRoot(styleEl: HTMLStyleElement): void { + for (const styleText of styleTexts) { + appendStylesTo(styleEl, styleText) + } +} + +function appendStylesTo(styleEl: HTMLStyleElement, styleText: string): void { + const { sheet } = styleEl + const ruleCnt = sheet.cssRules.length + + styleText.split('}').forEach((styleStr, i) => { + styleStr = styleStr.trim() + if (styleStr) { + sheet.insertRule(styleStr + '}', ruleCnt + i) + } + }) +} + +// nonce +// ------------------------------------------------------------------------------------------------- + +let queriedNonceValue: string | undefined + +function getNonceValue() { + if (queriedNonceValue === undefined) { + queriedNonceValue = queryNonceValue() + } + return queriedNonceValue +} + +/* +TODO: discourage meta tag and instead put nonce attribute on placeholder <style> tag +*/ +function queryNonceValue() { + const metaWithNonce = document.querySelector('meta[name="csp-nonce"]') + + if (metaWithNonce && metaWithNonce.hasAttribute('content')) { + return metaWithNonce.getAttribute('content') + } + + const elWithNonce = document.querySelector('script[nonce]') + + if (elWithNonce) { + return (elWithNonce as any).nonce || '' + } + + return '' +} + +// main +// ------------------------------------------------------------------------------------------------- + +if (typeof document !== 'undefined') { + registerStylesRoot(document) +} diff --git a/fullcalendar-main/packages/core/src/styles/bg.css b/fullcalendar-main/packages/core/src/styles/bg.css new file mode 100644 index 0000000..5874d5d --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/bg.css @@ -0,0 +1,39 @@ + +.fc { + + & .fc-bg-event, + & .fc-non-business, + & .fc-highlight { + // will always have a harness with position:relative/absolute, so absolutely expand + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + & .fc-non-business { + background: var(--fc-non-business-color); + } + + & .fc-bg-event { + background: var(--fc-bg-event-color); + opacity: var(--fc-bg-event-opacity); + + & .fc-event-title { + margin: .5em; + font-size: var(--fc-small-font-size); + font-style: italic; + } + } + + & .fc-highlight { + background: var(--fc-highlight-color); + } + + & .fc-cell-shaded, + & .fc-day-disabled { + background: var(--fc-neutral-bg-color); + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/button-group.css b/fullcalendar-main/packages/core/src/styles/button-group.css new file mode 100644 index 0000000..721e512 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/button-group.css @@ -0,0 +1,55 @@ + +.fc { + + & .fc-button-group { + position: relative; + display: inline-flex; + vertical-align: middle; + } + + & .fc-button-group > .fc-button { + position: relative; + flex: 1 1 auto; + } + + & .fc-button-group > .fc-button:hover { + z-index: 1; + } + + & .fc-button-group > .fc-button:focus, + & .fc-button-group > .fc-button:active, + & .fc-button-group > .fc-button.fc-button-active { + z-index: 1; + } + +} + +.fc-direction-ltr { + + & .fc-button-group > .fc-button:not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + & .fc-button-group > .fc-button:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + +} + +.fc-direction-rtl { + + & .fc-button-group > .fc-button:not(:first-child) { + margin-right: -1px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + & .fc-button-group > .fc-button:not(:last-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/button.css b/fullcalendar-main/packages/core/src/styles/button.css new file mode 100644 index 0000000..7e0fcb5 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/button.css @@ -0,0 +1,108 @@ + +/* +Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css + +These styles only apply when the standard-theme is activated. +When it's NOT activated, the fc-button classes won't even be in the DOM. +*/ + +.fc { + + // reset + + & .fc-button { + border-radius: 0; + overflow: visible; + text-transform: none; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + & .fc-button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; + } + + & .fc-button { + -webkit-appearance: button; + } + + & .fc-button:not(:disabled) { + cursor: pointer; + } + + // theme + + & .fc-button { + display: inline-block; + font-weight: 400; + text-align: center; + vertical-align: middle; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.4em 0.65em; + font-size: 1em; + line-height: 1.5; + border-radius: 0.25em; + } + + & .fc-button:hover { + text-decoration: none; + } + + & .fc-button:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); + } + + & .fc-button:disabled { + opacity: 0.65; + } + + // "primary" coloring + + & .fc-button-primary { + color: var(--fc-button-text-color); + background-color: var(--fc-button-bg-color); + border-color: var(--fc-button-border-color); + } + + & .fc-button-primary:hover { + color: var(--fc-button-text-color); + background-color: var(--fc-button-hover-bg-color); + border-color: var(--fc-button-hover-border-color); + } + + & .fc-button-primary:disabled { // not DRY + color: var(--fc-button-text-color); + background-color: var(--fc-button-bg-color); + border-color: var(--fc-button-border-color); // overrides :hover + } + + & .fc-button-primary:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } + + & .fc-button-primary:not(:disabled):active, + & .fc-button-primary:not(:disabled).fc-button-active { + color: var(--fc-button-text-color); + background-color: var(--fc-button-active-bg-color); + border-color: var(--fc-button-active-border-color); + } + + & .fc-button-primary:not(:disabled):active:focus, + & .fc-button-primary:not(:disabled).fc-button-active:focus { + box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5); + } + + // icons within buttons + + & .fc-button .fc-icon { + vertical-align: middle; + font-size: 1.5em; // bump up the size (but don't make it bigger than line-height of button, which is 1.5em also) + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/calendar-root.css b/fullcalendar-main/packages/core/src/styles/calendar-root.css new file mode 100644 index 0000000..b441e30 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/calendar-root.css @@ -0,0 +1,68 @@ + +.fc { + // layout of immediate children + display: flex; + flex-direction: column; + + font-size: 1em; + + &, + & *, + & *:before, + & *:after { + box-sizing: border-box; + } + + & table { + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; // normalize cross-browser + } + + & th { + text-align: center; + } + + & th, + & td { + vertical-align: top; + padding: 0; + } + + & a[data-navlink] { + cursor: pointer; + } + + & a[data-navlink]:hover { + text-decoration: underline; + } +} + +.fc-direction-ltr { + direction: ltr; + text-align: left; +} + +.fc-direction-rtl { + direction: rtl; + text-align: right; +} + +.fc-theme-standard { + & td, + & th { + border: 1px solid var(--fc-border-color); + } +} + +// for FF, which doesn't expand a 100% div within a table cell. use absolute positioning +// inner-wrappers are responsible for being absolute +// TODO: best place for this? +.fc-liquid-hack { + + & td, + & th { + position: relative; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/col-header.css b/fullcalendar-main/packages/core/src/styles/col-header.css new file mode 100644 index 0000000..723aae0 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/col-header.css @@ -0,0 +1,9 @@ + +.fc { + + & .fc-col-header-cell-cushion { + display: inline-block; // x-browser for when sticky (when multi-tier header) + padding: 2px 4px; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/event.css b/fullcalendar-main/packages/core/src/styles/event.css new file mode 100644 index 0000000..15f63c1 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/event.css @@ -0,0 +1,135 @@ + +$event-selected-dimmer-z: 1; +$event-main-z: 2; +$event-selected-hit-z: 3; +$event-resizer-z: 4; + + +// link resets +// ---------------------------------------------------------------------------------------------------- + +a.fc-event, +a.fc-event:hover { + text-decoration: none; +} + +// cursor +.fc-event[href], +.fc-event.fc-event-draggable { + cursor: pointer; +} + + +// event text content +// ---------------------------------------------------------------------------------------------------- + +.fc-event { + & .fc-event-main { + position: relative; + z-index: $event-main-z; + } +} + + +// dragging +// ---------------------------------------------------------------------------------------------------- + +.fc-event-dragging { + + &:not(.fc-event-selected) { // MOUSE + opacity: 0.75; + } + + &.fc-event-selected { // TOUCH + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); + } + +} + + +// resizing +// ---------------------------------------------------------------------------------------------------- +// (subclasses should hone positioning for touch and non-touch) + +.fc-event { + + & .fc-event-resizer { + display: none; + position: absolute; + z-index: $event-resizer-z; + } + +} + +.fc-event:hover, // MOUSE +.fc-event-selected { // TOUCH + + & .fc-event-resizer { + display: block; + } + +} + +.fc-event-selected { + + & .fc-event-resizer { + border-radius: calc(var(--fc-event-resizer-dot-total-width) / 2); + border-width: var(--fc-event-resizer-dot-border-width); + width: var(--fc-event-resizer-dot-total-width); + height: var(--fc-event-resizer-dot-total-width); + border-style: solid; + border-color: inherit; + background: var(--fc-page-bg-color); + + // expand hit area + &:before { + content: ''; + position: absolute; + top: -20px; + left: -20px; + right: -20px; + bottom: -20px; + } + + } + +} + + +// selecting (always TOUCH) +// OR, focused by tab-index +// (TODO: maybe not the best focus-styling for .fc-daygrid-dot-event) +// ---------------------------------------------------------------------------------------------------- + +.fc-event-selected, +.fc-event:focus { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + + // expand hit area (subclasses should expand) + &:before { + content: ""; + position: absolute; + z-index: $event-selected-hit-z; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + // dimmer effect + &:after { + content: ""; + background: var(--fc-event-selected-overlay-color); + position: absolute; + z-index: $event-selected-dimmer-z; + + // assume there's a border on all sides. overcome it. + // sometimes there's NOT a border, in which case the dimmer will go over + // an adjacent border, which looks fine. + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/h-event.css b/fullcalendar-main/packages/core/src/styles/h-event.css new file mode 100644 index 0000000..17ea83b --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/h-event.css @@ -0,0 +1,98 @@ + +/* +A HORIZONTAL event +*/ + +.fc-h-event { // allowed to be top-level + display: block; + border: 1px solid var(--fc-event-border-color); + background-color: var(--fc-event-bg-color); + + & .fc-event-main { + color: var(--fc-event-text-color); + } + + & .fc-event-main-frame { + display: flex; // for make fc-event-title-container expand + } + + & .fc-event-time { + max-width: 100%; // clip overflow on this element + overflow: hidden; + } + + & .fc-event-title-container { // serves as a container for the sticky cushion + flex-grow: 1; + flex-shrink: 1; + min-width: 0; // important for allowing to shrink all the way + } + + & .fc-event-title { + display: inline-block; // need this to be sticky cross-browser + vertical-align: top; // for not messing up line-height + left: 0; // for sticky + right: 0; // for sticky + max-width: 100%; // clip overflow on this element + overflow: hidden; + } + + &.fc-event-selected:before { + // expand hit area + top: -10px; + bottom: -10px; + } + +} + +// adjust border and border-radius (if there is any) for non-start/end + +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left-width: 0; +} + +.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end), +.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-width: 0; +} + +// resizers + +.fc-h-event:not(.fc-event-selected) .fc-event-resizer { + top: 0; + bottom: 0; + width: var(--fc-event-resizer-thickness); +} + +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end { + cursor: w-resize; + left: calc(-0.5 * var(--fc-event-resizer-thickness)); +} + +.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start { + cursor: e-resize; + right: calc(-0.5 * var(--fc-event-resizer-thickness)); +} + +// resizers for TOUCH + +.fc-h-event.fc-event-selected .fc-event-resizer { + top: 50%; + margin-top: calc(-0.5 * var(--fc-event-resizer-dot-total-width)); +} + +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end { + left: calc(-0.5 * var(--fc-event-resizer-dot-total-width)); +} + +.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end, +.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start { + right: calc(-0.5 * var(--fc-event-resizer-dot-total-width)); +} diff --git a/fullcalendar-main/packages/core/src/styles/icon-generation/FullCalendar Icons.json b/fullcalendar-main/packages/core/src/styles/icon-generation/FullCalendar Icons.json new file mode 100644 index 0000000..6a46f53 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/icon-generation/FullCalendar Icons.json @@ -0,0 +1,4809 @@ +{ + "metadata": { + "name": "FullCalendar Icons", + "lastOpened": 0, + "created": 1552512246070 + }, + "iconSets": [ + { + "selection": [ + { + "id": 1, + "order": 0 + }, + { + "id": 2, + "order": 0 + }, + { + "id": 3, + "order": 0 + }, + { + "id": 4, + "order": 0 + }, + { + "id": 5, + "order": 0 + }, + { + "id": 6, + "order": 0 + }, + { + "id": 7, + "order": 0 + }, + { + "id": 8, + "order": 0 + }, + { + "id": 9, + "order": 0 + }, + { + "id": 10, + "order": 0 + }, + { + "id": 11, + "order": 0 + }, + { + "id": 12, + "order": 0 + }, + { + "id": 13, + "order": 0 + }, + { + "id": 14, + "order": 0 + }, + { + "id": 15, + "order": 0 + }, + { + "id": 16, + "order": 0 + }, + { + "id": 17, + "order": 0 + }, + { + "id": 18, + "order": 0 + }, + { + "id": 19, + "order": 0 + }, + { + "id": 20, + "order": 0 + }, + { + "id": 21, + "order": 0 + }, + { + "id": 22, + "order": 0 + }, + { + "id": 23, + "order": 0 + }, + { + "id": 24, + "order": 0 + }, + { + "id": 25, + "order": 0 + }, + { + "id": 26, + "order": 0 + }, + { + "id": 27, + "order": 0 + }, + { + "id": 28, + "order": 0 + }, + { + "id": 29, + "order": 0 + }, + { + "id": 30, + "order": 0 + }, + { + "id": 31, + "order": 0 + }, + { + "id": 32, + "order": 0 + }, + { + "id": 33, + "order": 0 + }, + { + "id": 34, + "order": 0 + }, + { + "id": 35, + "order": 0 + }, + { + "id": 36, + "order": 0 + }, + { + "id": 37, + "order": 0 + }, + { + "id": 38, + "order": 0 + }, + { + "id": 39, + "order": 0 + }, + { + "id": 40, + "order": 0 + }, + { + "id": 41, + "order": 0 + }, + { + "id": 42, + "order": 0 + }, + { + "id": 43, + "order": 0 + }, + { + "id": 44, + "order": 0 + }, + { + "id": 45, + "order": 0 + }, + { + "id": 46, + "order": 0 + }, + { + "id": 47, + "order": 0 + }, + { + "id": 48, + "order": 9, + "prevSize": 24, + "name": "chevron-left", + "code": 59648, + "tempChar": "" + }, + { + "id": 49, + "order": 10, + "prevSize": 24, + "name": "chevron-right", + "code": 59649, + "tempChar": "" + }, + { + "id": 50, + "order": 0 + }, + { + "id": 51, + "order": 0 + }, + { + "id": 52, + "order": 11, + "prevSize": 24, + "name": "chevrons-left", + "code": 59650, + "tempChar": "" + }, + { + "id": 53, + "order": 12, + "prevSize": 24, + "name": "chevrons-right", + "code": 59651, + "tempChar": "" + }, + { + "id": 54, + "order": 0 + }, + { + "id": 55, + "order": 0 + }, + { + "id": 56, + "order": 0 + }, + { + "id": 57, + "order": 0 + }, + { + "id": 58, + "order": 0 + }, + { + "id": 59, + "order": 0 + }, + { + "id": 60, + "order": 0 + }, + { + "id": 61, + "order": 0 + }, + { + "id": 62, + "order": 0 + }, + { + "id": 63, + "order": 0 + }, + { + "id": 64, + "order": 0 + }, + { + "id": 65, + "order": 0 + }, + { + "id": 66, + "order": 0 + }, + { + "id": 67, + "order": 0 + }, + { + "id": 68, + "order": 0 + }, + { + "id": 69, + "order": 0 + }, + { + "id": 70, + "order": 0 + }, + { + "id": 71, + "order": 0 + }, + { + "id": 72, + "order": 0 + }, + { + "id": 73, + "order": 0 + }, + { + "id": 74, + "order": 0 + }, + { + "id": 75, + "order": 0 + }, + { + "id": 76, + "order": 0 + }, + { + "id": 77, + "order": 0 + }, + { + "id": 78, + "order": 0 + }, + { + "id": 79, + "order": 0 + }, + { + "id": 80, + "order": 0 + }, + { + "id": 81, + "order": 0 + }, + { + "id": 82, + "order": 0 + }, + { + "id": 83, + "order": 0 + }, + { + "id": 84, + "order": 0 + }, + { + "id": 85, + "order": 0 + }, + { + "id": 86, + "order": 0 + }, + { + "id": 87, + "order": 0 + }, + { + "id": 88, + "order": 0 + }, + { + "id": 89, + "order": 0 + }, + { + "id": 90, + "order": 0 + }, + { + "id": 91, + "order": 0 + }, + { + "id": 92, + "order": 0 + }, + { + "id": 93, + "order": 0 + }, + { + "id": 94, + "order": 0 + }, + { + "id": 95, + "order": 0 + }, + { + "id": 96, + "order": 0 + }, + { + "id": 97, + "order": 0 + }, + { + "id": 98, + "order": 0 + }, + { + "id": 99, + "order": 0 + }, + { + "id": 100, + "order": 0 + }, + { + "id": 101, + "order": 0 + }, + { + "id": 102, + "order": 0 + }, + { + "id": 103, + "order": 0 + }, + { + "id": 104, + "order": 0 + }, + { + "id": 105, + "order": 0 + }, + { + "id": 106, + "order": 0 + }, + { + "id": 107, + "order": 0 + }, + { + "id": 108, + "order": 0 + }, + { + "id": 109, + "order": 0 + }, + { + "id": 110, + "order": 0 + }, + { + "id": 111, + "order": 0 + }, + { + "id": 112, + "order": 0 + }, + { + "id": 113, + "order": 0 + }, + { + "id": 114, + "order": 0 + }, + { + "id": 115, + "order": 0 + }, + { + "id": 116, + "order": 0 + }, + { + "id": 117, + "order": 0 + }, + { + "id": 118, + "order": 0 + }, + { + "id": 119, + "order": 0 + }, + { + "id": 120, + "order": 0 + }, + { + "id": 121, + "order": 0 + }, + { + "id": 122, + "order": 0 + }, + { + "id": 123, + "order": 0 + }, + { + "id": 124, + "order": 0 + }, + { + "id": 125, + "order": 0 + }, + { + "id": 126, + "order": 0 + }, + { + "id": 127, + "order": 0 + }, + { + "id": 128, + "order": 0 + }, + { + "id": 129, + "order": 0 + }, + { + "id": 130, + "order": 0 + }, + { + "id": 131, + "order": 0 + }, + { + "id": 132, + "order": 0 + }, + { + "id": 133, + "order": 0 + }, + { + "id": 134, + "order": 0 + }, + { + "id": 135, + "order": 0 + }, + { + "id": 136, + "order": 0 + }, + { + "id": 137, + "order": 0 + }, + { + "id": 138, + "order": 0 + }, + { + "id": 139, + "order": 0 + }, + { + "id": 140, + "order": 0 + }, + { + "id": 141, + "order": 0 + }, + { + "id": 142, + "order": 0 + }, + { + "id": 143, + "order": 0 + }, + { + "id": 144, + "order": 0 + }, + { + "id": 145, + "order": 0 + }, + { + "id": 146, + "order": 0 + }, + { + "id": 147, + "order": 0 + }, + { + "id": 148, + "order": 0 + }, + { + "id": 149, + "order": 0 + }, + { + "id": 150, + "order": 0 + }, + { + "id": 151, + "order": 0 + }, + { + "id": 152, + "order": 0 + }, + { + "id": 153, + "order": 0 + }, + { + "id": 154, + "order": 0 + }, + { + "id": 155, + "order": 0 + }, + { + "id": 156, + "order": 0 + }, + { + "id": 157, + "order": 14, + "prevSize": 24, + "name": "minus-square", + "code": 59652, + "tempChar": "" + }, + { + "id": 158, + "order": 0 + }, + { + "id": 159, + "order": 0 + }, + { + "id": 160, + "order": 0 + }, + { + "id": 161, + "order": 0 + }, + { + "id": 162, + "order": 0 + }, + { + "id": 163, + "order": 0 + }, + { + "id": 164, + "order": 0 + }, + { + "id": 165, + "order": 0 + }, + { + "id": 166, + "order": 0 + }, + { + "id": 167, + "order": 0 + }, + { + "id": 168, + "order": 0 + }, + { + "id": 169, + "order": 0 + }, + { + "id": 170, + "order": 0 + }, + { + "id": 171, + "order": 0 + }, + { + "id": 172, + "order": 0 + }, + { + "id": 173, + "order": 0 + }, + { + "id": 174, + "order": 0 + }, + { + "id": 175, + "order": 0 + }, + { + "id": 176, + "order": 0 + }, + { + "id": 177, + "order": 0 + }, + { + "id": 178, + "order": 0 + }, + { + "id": 179, + "order": 0 + }, + { + "id": 180, + "order": 0 + }, + { + "id": 181, + "order": 0 + }, + { + "id": 182, + "order": 0 + }, + { + "id": 183, + "order": 0 + }, + { + "id": 184, + "order": 0 + }, + { + "id": 185, + "order": 0 + }, + { + "id": 186, + "order": 13, + "prevSize": 24, + "name": "plus-square", + "code": 59653, + "tempChar": "" + }, + { + "id": 187, + "order": 0 + }, + { + "id": 188, + "order": 0 + }, + { + "id": 189, + "order": 0 + }, + { + "id": 190, + "order": 0 + }, + { + "id": 191, + "order": 0 + }, + { + "id": 192, + "order": 0 + }, + { + "id": 193, + "order": 0 + }, + { + "id": 194, + "order": 0 + }, + { + "id": 195, + "order": 0 + }, + { + "id": 196, + "order": 0 + }, + { + "id": 197, + "order": 0 + }, + { + "id": 198, + "order": 0 + }, + { + "id": 199, + "order": 0 + }, + { + "id": 200, + "order": 0 + }, + { + "id": 201, + "order": 0 + }, + { + "id": 202, + "order": 0 + }, + { + "id": 203, + "order": 0 + }, + { + "id": 204, + "order": 0 + }, + { + "id": 205, + "order": 0 + }, + { + "id": 206, + "order": 0 + }, + { + "id": 207, + "order": 0 + }, + { + "id": 208, + "order": 0 + }, + { + "id": 209, + "order": 0 + }, + { + "id": 210, + "order": 0 + }, + { + "id": 211, + "order": 0 + }, + { + "id": 212, + "order": 0 + }, + { + "id": 213, + "order": 0 + }, + { + "id": 214, + "order": 0 + }, + { + "id": 215, + "order": 0 + }, + { + "id": 216, + "order": 0 + }, + { + "id": 217, + "order": 0 + }, + { + "id": 218, + "order": 0 + }, + { + "id": 219, + "order": 0 + }, + { + "id": 220, + "order": 0 + }, + { + "id": 221, + "order": 0 + }, + { + "id": 222, + "order": 0 + }, + { + "id": 223, + "order": 0 + }, + { + "id": 224, + "order": 0 + }, + { + "id": 225, + "order": 0 + }, + { + "id": 226, + "order": 0 + }, + { + "id": 227, + "order": 0 + }, + { + "id": 228, + "order": 0 + }, + { + "id": 229, + "order": 0 + }, + { + "id": 230, + "order": 0 + }, + { + "id": 231, + "order": 0 + }, + { + "id": 232, + "order": 0 + }, + { + "id": 233, + "order": 0 + }, + { + "id": 234, + "order": 0 + }, + { + "id": 235, + "order": 0 + }, + { + "id": 236, + "order": 0 + }, + { + "id": 237, + "order": 0 + }, + { + "id": 238, + "order": 0 + }, + { + "id": 239, + "order": 0 + }, + { + "id": 240, + "order": 0 + }, + { + "id": 241, + "order": 0 + }, + { + "id": 242, + "order": 0 + }, + { + "id": 243, + "order": 0 + }, + { + "id": 244, + "order": 0 + }, + { + "id": 245, + "order": 0 + }, + { + "id": 246, + "order": 0 + }, + { + "id": 247, + "order": 0 + }, + { + "id": 248, + "order": 0 + }, + { + "id": 249, + "order": 0 + }, + { + "id": 250, + "order": 0 + }, + { + "id": 251, + "order": 0 + }, + { + "id": 252, + "order": 0 + }, + { + "id": 253, + "order": 0 + }, + { + "id": 254, + "order": 0 + }, + { + "id": 255, + "order": 0 + }, + { + "id": 256, + "order": 0 + }, + { + "id": 257, + "order": 0 + }, + { + "id": 258, + "order": 0 + }, + { + "id": 259, + "order": 0 + }, + { + "id": 260, + "order": 0 + }, + { + "id": 261, + "order": 0 + }, + { + "id": 262, + "order": 0 + }, + { + "id": 263, + "order": 0 + }, + { + "id": 264, + "order": 0 + }, + { + "id": 265, + "order": 0 + }, + { + "id": 266, + "order": 0 + }, + { + "id": 267, + "order": 0 + }, + { + "id": 268, + "order": 0 + }, + { + "id": 269, + "order": 0 + }, + { + "id": 270, + "order": 0 + }, + { + "id": 271, + "order": 15, + "prevSize": 24, + "name": "x", + "code": 59654, + "tempChar": "" + }, + { + "id": 272, + "order": 0 + }, + { + "id": 273, + "order": 0 + }, + { + "id": 274, + "order": 0 + }, + { + "id": 275, + "order": 0 + }, + { + "id": 276, + "order": 0 + } + ], + "id": 1, + "metadata": { + "name": "Feather", + "importSize": { + "width": 24, + "height": 24 + }, + "url": "https://feathericons.com/", + "designer": "Cole Bemis", + "designerURL": "http://colebemis.com/", + "license": "MIT" + }, + "height": 1024, + "prevSize": 24, + "icons": [ + { + "id": 0, + "paths": [ + "M938.667 469.333h-170.667c-18.731 0-34.645 12.075-40.491 29.184l-87.509 262.571-215.509-646.571c-7.467-22.357-31.616-34.432-53.973-27.008-13.227 4.395-22.827 14.635-27.008 27.008l-118.272 354.816h-139.904c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h170.667c18.048-0.128 34.56-11.392 40.491-29.184l87.509-262.571 215.509 646.571c4.181 12.373 13.781 22.571 26.965 26.965 22.357 7.467 46.507-4.651 53.973-26.965l118.315-354.816h139.904c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "activity" + ], + "grid": 0 + }, + { + "id": 1, + "paths": [ + "M213.333 682.667h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-426.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v426.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-42.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h42.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-426.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-682.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v426.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h42.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM544.768 612.693c-1.493-1.835-3.371-3.712-5.461-5.461-18.091-15.104-45.013-12.629-60.075 5.461l-213.333 256c-6.144 7.339-9.899 16.896-9.899 27.307 0 23.552 19.115 42.667 42.667 42.667h426.667c9.6 0.043 19.328-3.2 27.307-9.899 18.091-15.104 20.565-41.984 5.461-60.075zM512 706.645l122.24 146.688h-244.48z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "airplay" + ], + "grid": 0 + }, + { + "id": 2, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM469.333 341.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "alert-circle" + ], + "grid": 0 + }, + { + "id": 3, + "paths": [ + "M335.36 42.667c-10.923 0-21.845 4.181-30.165 12.501l-250.027 250.027c-7.723 7.723-12.501 18.389-12.501 30.165v353.28c0 10.923 4.181 21.845 12.501 30.165l250.027 250.027c7.723 7.723 18.389 12.501 30.165 12.501h353.28c10.923 0 21.845-4.181 30.165-12.501l250.027-250.027c7.723-7.723 12.501-18.389 12.501-30.165v-353.28c0-10.923-4.181-21.845-12.501-30.165l-250.027-250.027c-7.723-7.723-18.389-12.501-30.165-12.501zM353.024 128h317.952l225.024 225.024v317.952l-225.024 225.024h-317.952l-225.024-225.024v-317.952zM469.333 341.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "alert-octagon" + ], + "grid": 0 + }, + { + "id": 4, + "paths": [ + "M475.648 186.624c3.115-5.248 7.893-10.325 14.251-14.165 4.992-3.029 10.283-4.907 15.616-5.717 5.547-0.853 11.221-0.597 16.683 0.725s10.581 3.712 15.147 7.040c4.352 3.2 8.149 7.253 11.093 12.075l361.216 603.008c3.285 5.589 5.461 12.715 5.547 20.565 0.085 5.845-1.024 11.349-3.029 16.341-2.091 5.205-5.205 9.941-9.131 13.952s-8.619 7.211-13.781 9.429c-4.949 2.091-10.411 3.328-15.787 3.371l-722.731 0.085c-6.485-0.043-13.696-1.749-20.523-5.717-5.077-2.944-9.259-6.656-12.501-10.923-3.413-4.437-5.931-9.557-7.381-14.976s-1.835-11.093-1.109-16.64c0.683-5.333 2.432-10.667 5.035-15.147zM402.432 142.763l-361.387 603.307c-8.96 15.531-14.293 31.616-16.427 47.829-2.219 16.853-1.024 33.792 3.285 49.877s11.733 31.36 22.101 44.843c9.984 13.013 22.613 24.277 37.547 32.896 19.797 11.435 41.643 17.067 62.933 17.152h722.901c17.707-0.213 34.261-3.797 49.323-10.24 15.616-6.656 29.611-16.341 41.259-28.245s20.992-26.069 27.307-41.856c6.101-15.189 9.344-31.787 9.173-49.067-0.256-22.869-6.528-44.544-17.323-62.891l-361.557-603.605c-9.088-14.976-20.608-27.349-33.835-37.035-13.696-10.069-29.141-17.152-45.312-21.12s-33.152-4.779-49.92-2.219c-16.213 2.475-32.128 8.149-46.891 17.109-18.304 11.093-33.067 26.24-43.179 43.264zM469.333 384v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 725.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "alert-triangle" + ], + "grid": 0 + }, + { + "id": 5, + "paths": [ + "M768 384h-512c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h512c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 213.333h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 554.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM768 725.333h-512c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h512c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "align-center" + ], + "grid": 0 + }, + { + "id": 6, + "paths": [ + "M896 384h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 213.333h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 554.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 725.333h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "align-justify" + ], + "grid": 0 + }, + { + "id": 7, + "paths": [ + "M725.333 384h-597.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h597.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 213.333h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 554.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM725.333 725.333h-597.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h597.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "align-left" + ], + "grid": 0 + }, + { + "id": 8, + "paths": [ + "M896 384h-597.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h597.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 213.333h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 554.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM896 725.333h-597.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h597.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "align-right" + ], + "grid": 0 + }, + { + "id": 9, + "paths": [ + "M597.333 213.333c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM213.333 469.333h-128c-23.552 0-42.667 19.115-42.667 42.667 0 63.488 12.629 124.16 35.541 179.499 23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541 0-23.552-19.115-42.667-42.667-42.667h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h82.987c-4.053 36.565-13.184 71.509-26.709 104.192-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-32.683 13.525-67.627 22.699-104.192 26.709v-515.029c7.765-2.005 15.317-4.523 22.613-7.552 20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.517 28.416 55.424 37.077c7.296 3.029 14.848 5.547 22.613 7.552v515.029c-36.565-4.053-71.509-13.184-104.192-26.709-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-13.525-32.683-22.699-67.627-26.709-104.192h82.987c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "anchor" + ], + "grid": 0 + }, + { + "id": 10, + "paths": [ + "M487.339 298.667l94.848-164.267c26.624 4.907 52.267 12.587 76.629 22.656 46.976 19.456 89.259 47.957 124.672 83.413 17.792 17.749 33.835 37.248 47.872 58.197h-221.611zM314.88 426.667l-94.805-164.139c6.528-7.637 13.312-14.976 20.395-22.059 35.456-35.456 77.739-63.957 124.672-83.413 37.845-15.701 78.805-25.472 121.728-28.245l-110.592 191.573zM339.541 640h-189.696c-14.123-39.979-21.845-83.072-21.845-128 0-52.096 10.368-101.675 29.056-146.859 3.243-7.808 6.741-15.488 10.453-23.040l110.933 192.171zM745.515 489.728l-61.056-105.728h189.696c14.123 39.979 21.845 83.072 21.845 128 0 52.096-10.368 101.675-29.056 146.859-3.243 7.808-6.741 15.488-10.453 23.040l-109.909-190.379zM537.088 895.189l172.032-297.856 94.805 164.181c-6.528 7.595-13.312 14.976-20.395 22.016-35.456 35.456-77.739 63.957-124.672 83.413-37.845 15.701-78.805 25.472-121.728 28.245zM456.491 978.091c1.963 0.384 3.968 0.597 5.973 0.683 16.299 1.664 32.811 2.56 49.536 2.56 63.488 0 124.16-12.629 179.499-35.541 57.472-23.808 109.141-58.667 152.363-101.888 15.616-15.616 30.123-32.299 43.435-50.005 1.749-1.963 3.328-4.096 4.693-6.357 21.504-29.568 39.595-61.781 53.803-96.043 22.912-55.339 35.541-116.011 35.541-179.499s-12.629-124.16-35.541-179.499c-0.768-1.835-1.536-3.669-2.304-5.504-0.683-1.835-1.451-3.669-2.347-5.376-23.595-53.12-56.747-100.992-97.237-141.483-43.221-43.221-94.891-78.123-152.363-101.888-38.997-16.171-80.597-27.179-123.989-32.299-1.963-0.384-3.968-0.597-5.973-0.683-16.299-1.707-32.853-2.603-49.579-2.603-63.488 0-124.16 12.629-179.499 35.541-57.472 23.808-109.141 58.667-152.363 101.931-15.616 15.616-30.123 32.299-43.435 49.963-1.749 2.005-3.328 4.096-4.693 6.357-21.461 29.568-39.595 61.781-53.803 96.043-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c0.768 1.835 1.536 3.669 2.304 5.504 0.683 1.835 1.451 3.669 2.347 5.376 23.595 53.12 56.747 100.992 97.237 141.483 43.221 43.221 94.891 78.123 152.363 101.888 38.997 16.128 80.597 27.179 123.989 32.299zM536.661 725.333l-94.848 164.267c-26.624-4.907-52.267-12.587-76.629-22.656-46.976-19.456-89.259-47.957-124.672-83.413-17.792-17.749-33.835-37.248-47.872-58.197h221.611zM659.84 512l-73.899 128h-147.883l-73.899-128 73.899-128h147.883z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "aperture" + ], + "grid": 0 + }, + { + "id": 11, + "paths": [ + "M170.667 384h682.667v469.333h-682.667zM42.667 85.333c-23.552 0-42.667 19.115-42.667 42.667v213.333c0 23.552 19.115 42.667 42.667 42.667h42.667v512c0 23.552 19.115 42.667 42.667 42.667h768c23.552 0 42.667-19.115 42.667-42.667v-512h42.667c23.552 0 42.667-19.115 42.667-42.667v-213.333c0-23.552-19.115-42.667-42.667-42.667zM85.333 170.667h853.333v128h-853.333zM426.667 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "archive" + ], + "grid": 0 + }, + { + "id": 12, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM469.333 341.333v238.336l-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l170.667 170.667c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 0.213 0 0.469 0 0.683 0 5.419-0.043 10.88-1.109 15.957-3.243 5.035-2.091 9.771-5.163 13.867-9.259l170.667-170.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835v-238.336c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-circle" + ], + "grid": 0 + }, + { + "id": 13, + "paths": [ + "M725.333 682.667h-323.669l353.835-353.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-353.835 353.835v-323.669c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v426.667c0 0.128 0 0.256 0 0.384 0.043 5.632 1.195 11.008 3.243 15.915 4.309 10.453 12.672 18.816 23.168 23.168 4.864 2.005 10.283 3.157 15.915 3.2 0.128 0 0.213 0 0.341 0h426.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-left" + ], + "grid": 0 + }, + { + "id": 14, + "paths": [ + "M682.667 298.667v323.669l-353.835-353.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l353.835 353.835h-323.669c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h426.667c5.76 0 11.264-1.152 16.299-3.2 10.453-4.309 18.816-12.715 23.168-23.168 2.048-4.907 3.157-10.283 3.2-15.957 0-0.128 0-0.213 0-0.341v-426.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-right" + ], + "grid": 0 + }, + { + "id": 15, + "paths": [ + "M780.501 481.835l-225.835 225.835v-494.336c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v494.336l-225.835-225.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l298.667 298.667c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 5.675 0.043 11.349-1.024 16.64-3.243 5.035-2.091 9.771-5.163 13.867-9.259l298.667-298.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down" + ], + "grid": 0 + }, + { + "id": 16, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM682.667 469.333h-238.336l97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-170.667 170.667c-0.085 0.085-0.128 0.128-0.213 0.213-3.968 4.053-6.997 8.661-9.045 13.611-4.309 10.453-4.309 22.229 0 32.683 2.048 4.949 5.077 9.557 9.045 13.611 0.085 0.085 0.128 0.128 0.213 0.213l170.667 170.667c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835h238.336c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-left-circle" + ], + "grid": 0 + }, + { + "id": 17, + "paths": [ + "M542.165 780.501l-225.835-225.835h494.336c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-494.336l225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-298.667 298.667c-4.096 4.096-7.168 8.789-9.259 13.824-4.309 10.453-4.309 22.272 0 32.725 2.048 4.949 5.077 9.557 9.045 13.611 0.085 0.085 0.128 0.128 0.213 0.213l298.667 298.667c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-left" + ], + "grid": 0 + }, + { + "id": 18, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM341.333 554.667h238.336l-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l170.667-170.667c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0-0.213 0-0.469 0-0.683-0.043-5.419-1.109-10.88-3.243-15.957-2.091-5.035-5.163-9.771-9.259-13.867l-170.667-170.667c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l97.835 97.835h-238.336c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-right-circle" + ], + "grid": 0 + }, + { + "id": 19, + "paths": [ + "M481.835 243.499l225.835 225.835h-494.336c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h494.336l-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l298.667-298.667c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-298.667-298.667c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-right" + ], + "grid": 0 + }, + { + "id": 20, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM554.667 682.667v-238.336l97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-170.667-170.667c-0.085-0.085-0.128-0.128-0.213-0.213-4.053-3.968-8.661-6.997-13.611-9.045-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.557 5.077-13.611 9.045-0.085 0.085-0.128 0.128-0.213 0.213l-170.667 170.667c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835v238.336c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-circle" + ], + "grid": 0 + }, + { + "id": 21, + "paths": [ + "M341.333 725.333v-323.669l353.835 353.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-353.835-353.835h323.669c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-426.667c-5.803 0-11.307 1.152-16.341 3.243-5.12 2.133-9.771 5.248-13.653 9.088-4.011 3.968-7.253 8.704-9.429 13.995-2.091 5.035-3.243 10.539-3.243 16.341v426.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-left" + ], + "grid": 0 + }, + { + "id": 22, + "paths": [ + "M298.667 341.333h323.669l-353.835 353.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l353.835-353.835v323.669c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-426.667c0-5.76-1.152-11.264-3.2-16.299-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-426.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-right" + ], + "grid": 0 + }, + { + "id": 23, + "paths": [ + "M243.499 542.165l225.835-225.835v494.336c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-494.336l225.835 225.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-298.667-298.667c-0.085-0.085-0.128-0.128-0.213-0.213-4.053-3.968-8.661-6.997-13.611-9.045-10.453-4.309-22.272-4.309-32.725 0-5.035 2.091-9.728 5.163-13.824 9.259l-298.667 298.667c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up" + ], + "grid": 0 + }, + { + "id": 24, + "paths": [ + "M640 512c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM671.573 653.568c5.547 7.765 11.691 15.061 18.389 21.76 15.701 15.701 34.475 28.373 55.381 37.035 20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323v-42.667c0-63.488-12.629-124.16-35.584-179.499-23.808-57.472-58.667-109.141-101.931-152.363s-94.933-78.080-152.363-101.888c-55.339-22.912-116.011-35.541-179.499-35.541s-124.117 12.629-179.499 35.541c-57.429 23.808-109.099 58.667-152.363 101.931s-78.080 94.933-101.888 152.363c-22.912 55.381-35.541 116.011-35.541 179.499s12.629 124.16 35.584 179.499c23.808 57.472 58.667 109.141 101.931 152.363s94.933 78.080 152.363 101.888c55.339 22.912 116.011 35.541 179.499 35.541 53.12 0 104.235-8.832 151.851-25.088 48.853-16.683 93.952-41.088 133.632-71.723 18.645-14.379 22.101-41.173 7.723-59.861s-41.173-22.101-59.861-7.723c-32.299 24.96-69.12 44.885-109.013 58.496-38.869 13.269-80.683 20.523-124.288 20.523-52.096 0-101.675-10.325-146.859-29.056-46.976-19.456-89.259-47.957-124.715-83.413s-63.957-77.739-83.413-124.672c-18.731-45.099-29.099-94.677-29.099-146.773s10.325-101.632 29.056-146.816c19.456-46.976 47.957-89.259 83.413-124.715s77.739-63.957 124.672-83.413c45.184-18.731 94.763-29.056 146.816-29.056s101.675 10.325 146.859 29.056c46.976 19.456 89.259 47.957 124.715 83.413 35.456 35.413 63.957 77.739 83.413 124.672 18.688 45.141 29.056 94.72 29.056 146.816v42.667c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733-7.893 7.893-17.323 14.251-27.733 18.56-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56-7.893-7.893-14.251-17.323-18.56-27.733-4.096-9.984-6.4-20.949-6.4-32.597v-213.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667c-14.165-10.667-29.739-19.584-46.421-26.496-25.173-10.453-52.779-16.171-81.579-16.171s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.499 43.136-46.293 69.248c-10.453 25.216-16.171 52.821-16.171 81.621s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293 2.987-2.987 5.931-6.101 8.747-9.259z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "at-sign" + ], + "grid": 0 + }, + { + "id": 25, + "paths": [ + "M639.403 658.091l32 241.152-137.429-82.475c-13.269-7.851-29.995-8.363-43.904 0l-137.429 82.475 32.043-241.109c39.381 15.829 82.389 24.533 127.317 24.533 44.971 0 87.979-8.704 127.403-24.576zM654.123 554.283c-2.475 1.28-4.779 2.773-6.912 4.48-11.819 7.381-24.32 13.824-37.333 19.2-30.080 12.459-63.104 19.371-97.877 19.371s-67.797-6.912-97.877-19.371c-31.275-12.971-59.477-31.957-83.115-55.595s-42.667-51.84-55.595-83.115c-12.501-30.123-19.413-63.147-19.413-97.92s6.912-67.797 19.371-97.877c12.971-31.275 31.957-59.477 55.595-83.115s51.84-42.667 83.115-55.595c30.123-12.501 63.147-19.413 97.92-19.413s67.797 6.912 97.877 19.371c31.275 12.971 59.477 31.957 83.115 55.595s42.667 51.84 55.595 83.115c12.501 30.123 19.413 63.147 19.413 97.92s-6.912 67.797-19.371 97.877c-12.971 31.275-31.957 59.477-55.595 83.115-11.861 11.861-24.875 22.571-38.869 31.915zM304.64 612.48l-48.256 363.221c-3.115 23.339 13.312 44.8 36.693 47.915 9.984 1.323 19.669-0.939 27.563-5.717l191.36-114.816 191.403 114.816c20.224 12.117 46.421 5.589 58.539-14.635 5.205-8.661 6.955-18.389 5.717-27.563l-48.213-363.307c11.947-9.173 23.296-19.115 33.92-29.739 31.445-31.445 56.789-69.035 74.112-110.805 16.683-40.235 25.856-84.352 25.856-130.517s-9.173-90.283-25.856-130.56c-17.323-41.813-42.667-79.36-74.112-110.805s-69.035-56.789-110.805-74.112c-40.277-16.683-84.395-25.856-130.56-25.856s-90.283 9.173-130.56 25.856c-41.813 17.323-79.36 42.667-110.805 74.112s-56.789 69.035-74.112 110.805c-16.683 40.277-25.856 84.395-25.856 130.56s9.173 90.283 25.856 130.56c17.323 41.813 42.667 79.36 74.112 110.805 10.667 10.667 22.016 20.608 33.963 29.781z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "award" + ], + "grid": 0 + }, + { + "id": 26, + "paths": [ + "M810.667 853.333v-426.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v426.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM554.667 853.333v-682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v682.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM298.667 853.333v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bar-chart-2" + ], + "grid": 0 + }, + { + "id": 27, + "paths": [ + "M554.667 853.333v-426.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v426.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM810.667 853.333v-682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v682.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM298.667 853.333v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bar-chart" + ], + "grid": 0 + }, + { + "id": 28, + "paths": [ + "M213.333 725.333h-85.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-341.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h136.107c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-136.107c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v341.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM640 298.667h85.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v341.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-136.107c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h136.107c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-341.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM1024 554.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM433.835 232.32l-170.667 256c-13.056 19.627-7.765 46.080 11.819 59.179 7.339 4.907 15.659 7.211 23.68 7.168h176.256l-126.464 189.653c-13.056 19.627-7.765 46.080 11.819 59.179s46.080 7.765 59.179-11.819l170.667-256c4.523-6.656 7.211-14.848 7.211-23.68 0-23.552-19.115-42.667-42.667-42.667h-176.256l126.464-189.653c13.056-19.627 7.765-46.080-11.819-59.179s-46.080-7.765-59.179 11.819z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "battery-charging" + ], + "grid": 0 + }, + { + "id": 29, + "paths": [ + "M128 213.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v341.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-341.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM128 298.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v341.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-341.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM1024 554.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "battery" + ], + "grid": 0 + }, + { + "id": 30, + "paths": [ + "M548.907 874.581c-2.944 5.077-6.699 9.216-10.965 12.501-4.48 3.413-9.557 5.888-14.976 7.339s-11.093 1.835-16.64 1.067c-5.333-0.725-10.667-2.475-15.701-5.419-6.912-4.011-12.075-9.472-15.317-15.232-11.691-20.48-37.717-27.605-58.197-15.915s-27.605 37.717-15.915 58.197c10.667 18.731 26.581 35.115 46.635 46.763 14.933 8.661 30.976 13.995 47.232 16.171 16.853 2.261 33.792 1.109 49.877-3.2s31.36-11.691 44.885-22.016c13.013-9.941 24.32-22.571 32.981-37.461 11.819-20.395 4.864-46.507-15.488-58.325s-46.507-4.864-58.325 15.488zM810.667 341.205c0.043-40.32-7.936-78.848-22.485-114.048-15.104-36.608-37.248-69.504-64.683-97.067s-60.288-49.792-96.853-64.981c-35.243-14.635-73.813-22.741-114.176-22.784-61.781-0.085-119.424 18.645-166.4 50.347-19.541 13.141-24.704 39.68-11.52 59.221s39.723 24.661 59.264 11.477c33.109-22.357 74.112-35.797 118.528-35.755 28.971 0.043 56.491 5.845 81.536 16.256 26.069 10.837 49.536 26.709 69.205 46.464s35.499 43.264 46.251 69.333c10.283 24.96 15.957 52.309 16 81.109-1.792 70.741 7.381 148.309 28.373 225.152 6.229 22.741 29.653 36.139 52.395 29.909s36.139-29.653 29.909-52.395c-18.901-69.291-26.965-138.667-25.344-200.832 0-0.043 0-0.085 0-0.128 0-0.128 0-0.256 0-0.427 0-0.213 0-0.427 0-0.64 0-0.085 0-0.128 0-0.213zM298.496 358.869l323.84 323.797h-402.645c3.797-6.187 7.552-12.8 11.349-19.84 16.555-30.976 32.597-70.187 44.885-119.211 12.715-50.645 21.376-111.531 22.571-184.789zM12.501 72.832l207.189 207.189c-4.523 21.035-6.613 41.984-6.357 61.867 0 73.941-8.277 133.632-20.181 180.992-10.624 42.368-24.149 75.008-37.333 99.669-13.397 25.003-26.539 41.984-36.224 52.651-8.96 9.899-15.061 14.464-15.744 14.933-19.2 13.184-24.32 39.381-11.349 58.837 8.192 12.288 21.589 18.944 35.499 19.029h579.669l243.499 243.499c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.667-938.667c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bell-off" + ], + "grid": 0 + }, + { + "id": 31, + "paths": [ + "M725.333 341.333c0 80.811 9.003 147.541 22.741 202.283 12.288 48.981 28.331 88.235 44.885 119.211 3.755 7.040 7.552 13.653 11.349 19.84h-584.619c3.797-6.187 7.552-12.8 11.349-19.84 16.555-30.976 32.597-70.187 44.885-119.211 13.739-54.741 22.741-121.472 22.741-202.283 0-28.971 5.76-56.491 16.128-81.579 10.795-26.069 26.667-49.579 46.336-69.291s43.221-35.541 69.291-46.336c25.088-10.368 52.608-16.128 81.579-16.128s56.491 5.76 81.579 16.128c26.069 10.795 49.579 26.624 69.291 46.336s35.541 43.221 46.336 69.291c10.368 25.088 16.128 52.608 16.128 81.579zM810.667 341.333c0-40.363-8.021-78.976-22.613-114.219-15.147-36.565-37.333-69.461-64.853-96.981s-60.373-49.707-96.981-64.853c-35.243-14.592-73.856-22.613-114.219-22.613s-78.976 8.021-114.219 22.613c-36.608 15.189-69.461 37.376-96.981 64.853s-49.664 60.373-64.853 96.981c-14.592 35.243-22.613 73.856-22.613 114.219 0 74.453-8.277 134.187-20.181 181.547-10.624 42.368-24.149 75.008-37.333 99.669-13.397 25.003-26.539 41.984-36.224 52.651-8.96 9.899-15.061 14.464-15.744 14.933-19.2 13.184-24.32 39.381-11.349 58.837 8.192 12.288 21.589 18.944 35.499 19.029h768c23.552 0 42.667-19.115 42.667-42.667 0-14.763-7.467-27.733-18.987-35.499 0.384 0.256-5.845-4.224-15.275-14.635-9.685-10.667-22.827-27.648-36.224-52.651-13.184-24.661-26.709-57.301-37.333-99.669-11.904-47.36-20.181-107.093-20.181-181.547zM548.907 874.581c-2.944 5.077-6.699 9.216-10.965 12.501-4.48 3.413-9.557 5.888-14.976 7.339s-11.093 1.835-16.64 1.067c-5.333-0.725-10.667-2.475-15.701-5.419-6.912-4.011-12.075-9.472-15.317-15.232-11.691-20.48-37.717-27.605-58.197-15.915s-27.605 37.717-15.915 58.197c10.667 18.731 26.581 35.115 46.635 46.763 14.933 8.661 30.976 13.995 47.232 16.171 16.853 2.261 33.792 1.109 49.877-3.2s31.36-11.691 44.885-22.016c13.013-9.941 24.32-22.571 32.981-37.461 11.819-20.395 4.864-46.507-15.488-58.325s-46.507-4.864-58.325 15.488z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bell" + ], + "grid": 0 + }, + { + "id": 32, + "paths": [ + "M554.667 409.003v-263.339l131.669 131.669zM554.667 614.997l131.669 131.669-131.669 131.669zM247.168 307.499l204.501 204.501-204.501 204.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l161.835-161.835v366.336c0 10.923 4.181 21.845 12.501 30.165 16.683 16.683 43.691 16.683 60.331 0l234.667-234.667c16.683-16.683 16.683-43.691 0-60.331l-204.501-204.501 204.501-204.501c16.683-16.683 16.683-43.691 0-60.331l-234.667-234.667c-7.723-7.723-18.389-12.501-30.165-12.501-23.552 0-42.667 19.115-42.667 42.667v366.336l-161.835-161.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bluetooth" + ], + "grid": 0 + }, + { + "id": 33, + "paths": [ + "M298.667 469.333v-256h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939s-3.456 33.92-9.685 48.939c-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685zM213.333 512v341.333c0 23.552 19.115 42.667 42.667 42.667h384c28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621s-5.717-56.405-16.171-81.579c-10.837-26.155-26.667-49.621-46.293-69.248-13.867-13.867-29.653-25.856-46.933-35.499 1.451-1.365 2.859-2.731 4.267-4.139 19.627-19.627 35.499-43.136 46.293-69.248 10.453-25.216 16.171-52.821 16.171-81.621s-5.717-56.405-16.171-81.579c-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-341.333c-23.552 0-42.667 19.115-42.667 42.667zM298.667 554.667h341.333c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939s-3.456 33.92-9.685 48.939c-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685h-341.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bold" + ], + "grid": 0 + }, + { + "id": 34, + "paths": [ + "M896 170.667v554.667h-256c-23.040 0-45.099 4.608-65.28 12.928-6.912 2.859-13.611 6.187-20.053 9.899v-449.493c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685zM469.333 748.16c-6.443-3.712-13.141-7.040-20.053-9.899-20.181-8.32-42.24-12.928-65.28-12.928h-256v-554.667h213.333c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM938.667 85.333h-256c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293-7.168 7.125-13.781 14.763-19.84 22.869-6.059-8.107-12.672-15.744-19.797-22.869-19.627-19.627-43.136-35.499-69.248-46.293-25.216-10.453-52.821-16.171-81.621-16.171h-256c-23.552 0-42.667 19.115-42.667 42.667v640c0 23.552 19.115 42.667 42.667 42.667h298.667c11.648 0 22.613 2.304 32.64 6.443 10.411 4.309 19.797 10.667 27.733 18.56 7.893 7.893 14.251 17.323 18.56 27.733 4.096 9.984 6.4 20.949 6.4 32.597 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667c0-11.648 2.304-22.613 6.443-32.64 4.309-10.411 10.667-19.797 18.56-27.733 7.893-7.893 17.323-14.251 27.733-18.56 9.984-4.096 20.949-6.4 32.597-6.4h298.667c23.552 0 42.667-19.115 42.667-42.667v-640c0-23.552-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "book-open" + ], + "grid": 0 + }, + { + "id": 35, + "paths": [ + "M277.333 42.667c-20.139 0-39.467 4.011-57.131 11.307-18.304 7.595-34.731 18.688-48.469 32.427s-24.832 30.165-32.427 48.469c-7.296 17.664-11.307 36.992-11.307 57.131v640c0 20.139 4.011 39.467 11.307 57.131 7.595 18.304 18.688 34.731 32.427 48.469s30.165 24.832 48.469 32.427c17.664 7.296 36.992 11.307 57.131 11.307h576c23.552 0 42.667-19.115 42.667-42.667v-853.333c0-23.552-19.115-42.667-42.667-42.667zM810.667 768v128h-533.333c-8.747 0-16.981-1.749-24.448-4.821-7.808-3.243-14.848-7.979-20.779-13.909s-10.667-13.013-13.909-20.779c-3.115-7.509-4.864-15.744-4.864-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.848 13.909-20.779s13.013-10.667 20.779-13.909c7.509-3.115 15.744-4.864 24.491-4.864zM277.333 128h533.333v554.667h-533.333c-20.139 0-39.467 4.011-57.131 11.307-2.304 0.981-4.608 2.005-6.869 3.072v-505.045c0-8.747 1.749-16.981 4.821-24.448 3.243-7.808 7.979-14.891 13.909-20.821s13.013-10.667 20.779-13.909c7.509-3.072 15.744-4.821 24.491-4.821z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "book" + ], + "grid": 0 + }, + { + "id": 36, + "paths": [ + "M785.877 930.731c6.869 4.949 15.488 7.936 24.789 7.936 23.552 0 42.667-19.115 42.667-42.667v-682.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-426.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c-0.043 8.491 2.56 17.237 7.936 24.789 13.696 19.157 40.363 23.637 59.52 9.899l273.877-195.584zM768 813.099l-231.211-165.163c-15.147-10.837-34.944-10.325-49.579 0l-231.211 165.163v-599.765c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h426.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bookmark" + ], + "grid": 0 + }, + { + "id": 37, + "paths": [ + "M569.045 23.723c-8.96-4.437-18.176-7.723-27.648-9.984-14.293-3.371-29.184-4.352-44.075-2.645-14.421 1.664-28.715 5.803-42.411 12.587l-341.461 170.752c-16.469 8.32-30.891 19.968-42.325 34.133-9.045 11.136-16.256 23.851-21.12 37.675-4.779 13.44-7.339 27.776-7.339 42.667v406.144c0 18.517 3.925 36.736 11.435 53.376 5.888 13.099 14.037 25.216 24.149 35.84 9.856 10.283 21.504 19.029 34.901 25.771l341.461 170.709c9.003 4.48 18.475 7.851 28.245 10.155 14.251 3.328 29.141 4.224 43.989 2.432 14.379-1.707 28.629-5.888 42.155-12.629l341.504-170.752c16.469-8.32 30.891-19.968 42.325-34.133 9.045-11.136 16.256-23.851 21.12-37.675 4.821-13.397 7.381-27.733 7.381-42.624v-406.912c-0.128-18.432-4.181-36.565-11.733-53.12-5.973-13.056-14.165-25.131-24.363-35.669-9.899-10.197-21.632-18.901-34.816-25.472zM843.051 256.128l-331.051 165.504-331.051-165.547 312.149-156.075c4.523-2.261 9.259-3.584 13.952-4.139 4.907-0.555 9.856-0.256 14.677 0.896 3.2 0.768 6.357 1.877 9.131 3.243zM469.333 495.701v417.024l-317.995-158.976c-4.395-2.219-8.235-5.12-11.435-8.448-3.328-3.456-6.016-7.467-8.021-11.904-2.517-5.632-3.883-11.861-3.883-18.261v-390.059zM554.667 912.512v-416.811l341.333-170.667v390.485c0 5.035-0.853 9.771-2.432 14.123-1.621 4.523-3.968 8.747-7.040 12.501-3.883 4.779-8.789 8.789-14.507 11.648z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "box" + ], + "grid": 0 + }, + { + "id": 38, + "paths": [ + "M384 256v-42.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v42.667zM384 853.333v-512h256v512zM298.667 341.333v512h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-426.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM725.333 256v-42.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-170.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v42.667h-128c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v426.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-426.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM725.333 853.333v-512h128c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v426.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "briefcase" + ], + "grid": 0 + }, + { + "id": 39, + "paths": [ + "M298.667 85.333v42.667h-85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-85.333v-42.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v42.667h-256v-42.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM853.333 384h-682.667v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h85.333v42.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-42.667h256v42.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-42.667h85.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM170.667 469.333h682.667v384c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "calendar" + ], + "grid": 0 + }, + { + "id": 40, + "paths": [ + "M384 170.667h233.173l72.661 109.013c8.192 12.245 21.589 18.901 35.499 18.987h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v398.507c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-398.507c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-147.84l-72.661-109.013c-7.765-11.52-20.736-18.987-35.499-18.987h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM352.512 412.843c-6.229 6.955-12.075 14.379-17.451 22.272-16.256 23.765-27.136 49.792-32.725 76.459-5.845 27.691-6.016 56.021-0.896 83.328s15.531 53.632 31.019 77.312c14.891 22.827 34.432 43.136 58.197 59.435 23.765 16.256 49.792 27.136 76.459 32.725 27.691 5.845 56.021 6.016 83.328 0.896s53.632-15.531 77.312-31.019c9.131-5.973 17.877-12.672 26.112-20.096l139.136 139.179h-665.003c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-469.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h110.336zM444.288 383.915l-371.456-371.413c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331l140.501 140.501h-25.003c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v469.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h750.336l72.832 72.832c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-566.059-566.059zM593.365 653.696c-3.925 3.285-8.021 6.315-12.245 9.088-14.165 9.259-29.995 15.531-46.421 18.603s-33.451 2.944-50.005-0.512c-15.915-3.328-31.488-9.813-45.867-19.669s-26.027-22.016-34.944-35.627c-9.216-14.165-15.531-29.995-18.603-46.421s-2.944-33.451 0.512-50.005c3.328-15.915 9.813-31.488 19.669-45.867 2.389-3.499 4.907-6.827 7.509-9.941z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "camera-off" + ], + "grid": 0 + }, + { + "id": 41, + "paths": [ + "M1024 810.667v-469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-147.84l-72.661-109.013c-7.765-11.52-20.736-18.987-35.499-18.987h-256c-13.909 0.085-27.307 6.741-35.499 18.987l-72.661 109.013h-147.84c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v469.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h768c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM938.667 810.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-768c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-469.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h170.667c14.763 0 27.733-7.467 35.499-18.987l72.661-109.013h210.347l72.661 109.013c8.192 12.245 21.589 18.901 35.499 18.987h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM725.333 554.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.499 43.136-46.293 69.248c-10.453 25.216-16.171 52.821-16.171 81.621s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM640 554.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "camera" + ], + "grid": 0 + }, + { + "id": 42, + "paths": [ + "M76.8 728.747c19.755 4.011 37.888 11.264 54.101 21.077 16.725 10.112 31.488 23.040 43.691 38.059 17.28 21.205 29.568 46.635 35.371 74.24 4.864 23.040 27.477 37.803 50.56 32.939s37.803-27.477 32.939-50.56c-8.619-40.96-26.837-78.805-52.693-110.507-18.304-22.485-40.448-41.899-65.707-57.173-24.448-14.805-51.755-25.685-81.195-31.701-23.083-4.693-45.611 10.197-50.347 33.28s10.197 45.611 33.28 50.347zM80.597 556.544c42.027 4.693 81.323 16.768 116.821 34.816 36.693 18.688 69.376 43.819 96.811 73.813 47.744 52.309 79.317 119.424 87.936 193.109 2.731 23.424 23.936 40.149 47.317 37.419s40.149-23.936 37.419-47.317c-10.709-91.733-50.091-175.445-109.653-240.725-34.219-37.461-75.093-68.907-121.088-92.331-44.501-22.656-93.739-37.76-146.091-43.605-23.424-2.603-44.501 14.251-47.104 37.675s14.251 44.501 37.675 47.104zM128 341.333v-85.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v512c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h256c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-682.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM128 853.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cast" + ], + "grid": 0 + }, + { + "id": 43, + "paths": [ + "M896 472.747v39.253c-0.043 52.053-10.411 101.632-29.141 146.816-19.456 46.933-48.043 89.216-83.499 124.629s-77.781 63.915-124.757 83.328c-45.184 18.688-94.763 29.013-146.859 28.971s-101.632-10.411-146.816-29.141c-46.933-19.456-89.216-48.043-124.629-83.499s-63.915-77.781-83.328-124.757c-18.688-45.141-29.013-94.72-28.971-146.816s10.411-101.632 29.141-146.816c19.456-46.933 48.043-89.216 83.499-124.629s77.781-63.915 124.757-83.328c45.184-18.688 94.763-29.013 146.859-28.971 56.747 0.043 110.336 12.331 155.691 33.067 21.419 9.813 46.763 0.341 56.533-21.077s0.341-46.763-21.077-56.533c-56.619-25.856-122.283-40.747-191.104-40.789-63.488-0.043-124.16 12.544-179.499 35.456-57.515 23.723-109.227 58.581-152.491 101.803s-78.165 94.848-101.973 152.32c-22.955 55.339-35.627 115.968-35.669 179.456s12.544 124.16 35.456 179.499c23.765 57.472 58.624 109.184 101.803 152.448s94.848 78.165 152.32 101.973c55.339 22.955 115.968 35.627 179.456 35.669s124.16-12.544 179.499-35.456c57.472-23.765 109.184-58.624 152.448-101.803 43.264-43.221 78.165-94.848 101.973-152.32 22.997-55.339 35.669-115.968 35.712-179.499v-39.253c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM908.501 140.501l-396.501 396.885-97.835-97.792c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l128 128c16.683 16.683 43.691 16.64 60.373 0l426.667-427.093c16.64-16.683 16.64-43.691-0.043-60.331s-43.691-16.64-60.331 0.043z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "check-circle" + ], + "grid": 0 + }, + { + "id": 44, + "paths": [ + "M353.835 499.499l128 128c16.683 16.683 43.691 16.683 60.331 0l426.667-426.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-396.501 396.501-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM853.333 512v298.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h469.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-469.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "check-square" + ], + "grid": 0 + }, + { + "id": 45, + "paths": [ + "M823.168 225.835l-439.168 439.168-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0l469.333-469.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "check" + ], + "grid": 0 + }, + { + "id": 46, + "paths": [ + "M225.835 414.165l256 256c16.683 16.683 43.691 16.683 60.331 0l256-256c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-225.835 225.835-225.835-225.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevron-down" + ], + "grid": 0 + }, + { + "id": 47, + "paths": [ + "M670.165 737.835l-225.835-225.835 225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-256 256c-16.683 16.683-16.683 43.691 0 60.331l256 256c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevron-left" + ], + "grid": 0 + }, + { + "id": 48, + "paths": [ + "M414.165 798.165l256-256c16.683-16.683 16.683-43.691 0-60.331l-256-256c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l225.835 225.835-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevron-right" + ], + "grid": 0 + }, + { + "id": 49, + "paths": [ + "M798.165 609.835l-256-256c-16.683-16.683-43.691-16.683-60.331 0l-256 256c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l225.835-225.835 225.835 225.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevron-up" + ], + "grid": 0 + }, + { + "id": 50, + "paths": [ + "M268.501 584.832l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-183.168 183.168-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM268.501 286.165l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-183.168 183.168-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevrons-down" + ], + "grid": 0 + }, + { + "id": 51, + "paths": [ + "M499.499 695.168l-183.168-183.168 183.168-183.168c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331zM798.165 695.168l-183.168-183.168 183.168-183.168c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevrons-left" + ], + "grid": 0 + }, + { + "id": 52, + "paths": [ + "M584.832 755.499l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l183.168 183.168-183.168 183.168c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM286.165 755.499l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l183.168 183.168-183.168 183.168c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevrons-right" + ], + "grid": 0 + }, + { + "id": 53, + "paths": [ + "M755.499 439.168l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l183.168-183.168 183.168 183.168c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331zM755.499 737.835l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l183.168-183.168 183.168 183.168c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chevrons-up" + ], + "grid": 0 + }, + { + "id": 54, + "paths": [ + "M315.819 428.075l-95.744-165.589c6.485-7.595 13.312-14.933 20.395-22.016 35.456-35.456 77.739-63.957 124.672-83.413 45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 17.749 17.749 33.792 37.248 47.829 58.197h-319.36c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293-19.072 19.072-34.56 41.728-45.355 66.944zM456.491 978.091c1.963 0.384 3.968 0.597 5.973 0.683 16.299 1.664 32.811 2.56 49.536 2.56 63.488 0 124.16-12.629 179.499-35.541 57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541s-12.629-124.16-35.541-179.499c-0.768-1.835-1.536-3.669-2.304-5.504-0.683-1.835-1.451-3.669-2.347-5.376-23.595-53.12-56.747-100.992-97.237-141.483-43.221-43.221-94.891-78.123-152.363-101.888-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931-15.616 15.616-30.123 32.299-43.435 49.963-1.749 2.005-3.328 4.096-4.693 6.357-21.461 29.568-39.595 61.781-53.803 96.043-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c38.997 16.128 80.597 27.179 123.989 32.299zM537.387 723.84l-95.616 165.76c-26.624-4.907-52.267-12.587-76.629-22.656-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c3.243-7.808 6.741-15.531 10.453-23.083l155.477 268.928c10.069 19.243 22.997 36.693 38.187 51.883 19.627 19.627 43.136 35.499 69.248 46.293 25.173 10.453 52.779 16.171 81.579 16.171 8.576 0 17.067-0.512 25.387-1.493zM624.512 573.056c-0.64 0.939-1.28 1.92-1.877 2.944l-3.243 5.589c-4.907 7.552-10.581 14.549-16.939 20.907-11.819 11.819-25.941 21.333-41.557 27.819-14.976 6.229-31.488 9.685-48.896 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819-7.083-7.083-13.355-14.976-18.603-23.552-0.469-1.024-1.024-2.005-1.579-2.987l-3.456-6.016c-1.493-2.944-2.901-5.973-4.181-9.045-6.229-14.976-9.685-31.488-9.685-48.896s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939s-3.456 33.92-9.685 48.939c-1.707 4.139-3.669 8.192-5.803 12.117zM537.088 895.189l155.307-269.227c6.485-10.24 12.117-21.035 16.811-32.384 10.411-25.173 16.128-52.779 16.128-81.579s-5.717-56.405-16.171-81.579c-6.912-16.683-15.829-32.256-26.496-46.421h191.488c14.123 39.979 21.845 83.072 21.845 128 0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-37.888 15.701-78.848 25.515-121.771 28.245z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chrome" + ], + "grid": 0 + }, + { + "id": 55, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "circle" + ], + "grid": 0 + }, + { + "id": 56, + "paths": [ + "M298.667 213.333c0 11.477 2.304 22.528 6.485 32.64 4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485h256c11.477 0 22.528-2.304 32.64-6.485 10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64h42.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM384 42.667c-11.477 0-22.528 2.304-32.64 6.485-10.496 4.352-19.883 10.667-27.691 18.517s-14.165 17.195-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64h-42.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-42.667c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485zM384 128h256v85.333h-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clipboard" + ], + "grid": 0 + }, + { + "id": 57, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM469.333 256v256c0 16.597 9.472 31.019 23.595 38.144l170.667 85.333c21.077 10.539 46.72 2.005 57.259-19.072s2.005-46.72-19.072-57.259l-147.115-73.515v-229.632c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clock" + ], + "grid": 0 + }, + { + "id": 58, + "paths": [ + "M298.667 810.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM298.667 554.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM640 810.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM640 554.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 896v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 640v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM870.443 746.496c31.701-13.867 59.264-33.451 81.92-57.045 23.509-24.491 41.685-53.205 53.845-84.267s18.261-64.469 17.579-98.432c-0.64-32.683-7.637-65.792-21.504-97.451-12.032-27.477-28.373-51.84-47.915-72.619-20.181-21.461-43.776-38.997-69.504-52.181-35.669-18.261-75.52-28.16-116.48-28.501h-22.571c-13.269-37.717-32.043-72.235-55.125-102.912-30.635-40.661-68.821-74.453-111.915-99.883s-91.136-42.453-141.568-49.536c-48.555-6.827-99.2-4.437-149.461 8.533s-95.744 35.413-134.912 64.896c-40.661 30.635-74.496 68.821-99.883 111.915s-42.453 91.136-49.536 141.525c-6.827 48.555-4.437 99.2 8.533 149.461 10.709 41.472 27.819 79.659 49.963 113.664 22.699 34.859 50.603 65.323 82.261 90.411 18.475 14.635 45.312 11.52 59.947-6.955s11.52-45.312-6.955-59.947c-24.448-19.371-46.123-42.965-63.744-70.059-17.152-26.368-30.507-56.107-38.869-88.491-10.155-39.253-11.989-78.592-6.656-116.224 5.504-39.125 18.773-76.544 38.571-110.123s46.123-63.275 77.696-87.083c30.379-22.784 65.664-40.235 104.917-50.347s78.592-11.989 116.224-6.656c39.125 5.504 76.544 18.773 110.123 38.571s63.275 46.123 87.083 77.696c22.869 30.379 40.32 65.664 50.432 104.917 4.907 18.517 21.547 31.957 41.301 31.957h53.419c27.605 0.213 54.4 6.912 78.251 19.115 17.195 8.832 32.896 20.48 46.251 34.731 12.971 13.739 23.808 29.952 31.872 48.384 9.301 21.248 13.909 43.264 14.336 64.939 0.469 22.571-3.627 44.885-11.733 65.664s-20.267 39.936-35.883 56.192c-15.019 15.659-33.365 28.715-54.613 38.016-21.589 9.472-31.403 34.603-21.973 56.192s34.603 31.403 56.192 21.973z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-drizzle" + ], + "grid": 0 + }, + { + "id": 59, + "paths": [ + "M819.157 762.88c33.92-6.869 64.981-20.224 92.075-38.485 28.16-18.987 51.968-43.221 70.4-70.997 18.389-27.776 31.445-59.179 37.931-92.501 6.272-32.085 6.4-65.92-0.469-99.797-6.315-31.189-18.133-60.032-34.261-85.589-16.683-26.411-37.931-49.28-62.464-67.712-21.931-16.512-46.507-29.483-72.747-38.315-25.813-8.661-53.248-13.355-81.451-13.483h-22.357c-13.269-37.717-32.043-72.235-55.168-102.912-30.635-40.661-68.821-74.453-111.915-99.84s-91.179-42.453-141.568-49.536c-48.555-6.827-99.2-4.437-149.461 8.576s-95.744 35.413-134.912 64.896c-40.661 30.549-74.453 68.736-99.84 111.829s-42.453 91.179-49.536 141.568c-6.827 48.555-4.437 99.2 8.576 149.461 12.501 48.341 33.664 92.203 61.525 130.347 28.885 39.595 64.896 72.875 105.643 98.56 6.101 3.84 12.331 7.509 18.645 11.008 20.608 11.435 46.592 3.968 58.027-16.64s3.968-46.592-16.64-57.984c-4.907-2.731-9.771-5.589-14.507-8.576-31.787-20.011-59.776-45.952-82.219-76.672-21.589-29.568-38.059-63.659-47.829-101.376-10.197-39.253-12.032-78.549-6.741-116.224 5.504-39.125 18.773-76.544 38.571-110.123s46.123-63.275 77.696-87.083c30.379-22.869 65.664-40.32 104.917-50.432s78.592-11.989 116.224-6.699c39.125 5.504 76.544 18.773 110.123 38.571s63.275 46.123 87.083 77.696c22.869 30.379 40.32 65.664 50.432 104.917 4.907 18.56 21.547 32 41.301 32h53.589c18.944 0.085 37.333 3.243 54.571 9.045 17.579 5.931 34.005 14.592 48.683 25.6 16.384 12.331 30.507 27.52 41.557 45.056 10.667 16.939 18.56 36.053 22.784 57.003 4.608 22.741 4.48 45.227 0.341 66.517-4.309 22.144-13.013 43.093-25.301 61.696s-28.203 34.773-46.933 47.36c-18.005 12.117-38.656 21.035-61.397 25.643-23.083 4.693-38.016 27.221-33.323 50.304s27.221 38.016 50.304 33.323zM519.168 445.653l-170.667 256c-13.056 19.627-7.765 46.080 11.819 59.179 7.339 4.907 15.659 7.211 23.68 7.168h176.256l-126.464 189.653c-13.056 19.627-7.765 46.080 11.819 59.179s46.080 7.765 59.179-11.819l170.667-256c4.523-6.656 7.211-14.848 7.211-23.68 0-23.552-19.115-42.667-42.667-42.667h-176.256l126.464-189.653c13.056-19.627 7.765-46.080-11.819-59.179s-46.080-7.765-59.179 11.819z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-lightning" + ], + "grid": 0 + }, + { + "id": 60, + "paths": [ + "M1003.989 739.84c13.483-31.872 20.053-65.024 20.309-97.707 0.256-33.963-6.272-67.285-18.773-98.219-12.501-30.891-31.061-59.392-54.869-83.584-22.955-23.296-50.731-42.539-82.603-56.064-33.067-13.995-67.712-20.608-100.523-20.267h-21.589c-11.136-32-26.24-61.781-44.672-88.875-25.216-37.12-56.576-69.12-92.331-94.976-55.381-40.021-121.344-65.323-191.445-71.979-23.467-2.219-44.288 14.976-46.507 38.443s14.976 44.288 38.443 46.507c54.869 5.205 106.368 25.003 149.547 56.192 27.861 20.139 52.181 45.013 71.765 73.771 18.901 27.861 33.408 59.435 42.197 94.080 4.779 18.645 21.461 32.171 41.301 32.171h54.229c20.907-0.213 43.947 4.011 66.347 13.525 21.376 9.045 39.851 21.888 55.040 37.333 15.829 16.085 28.203 35.072 36.565 55.765s12.715 42.923 12.544 65.493c-0.171 21.717-4.523 43.776-13.568 65.109-9.173 21.717 0.939 46.72 22.656 55.936s46.72-0.939 55.936-22.656zM207.104 267.435l543.232 543.232h-366.763c-40.96 0.427-81.237-7.339-118.4-22.485-31.061-12.672-59.947-30.464-85.333-53.035-24.533-21.803-45.867-48.085-62.72-78.592-19.627-35.456-31.232-73.088-35.499-110.891-4.437-39.253-0.939-78.805 9.856-116.267s28.843-72.789 53.504-103.68c17.451-21.888 38.229-41.6 62.123-58.283zM12.501 72.832l133.504 133.547c-25.643 19.456-48.299 41.728-67.755 66.133-31.744 39.808-54.912 85.205-68.779 133.291s-18.347 98.859-12.629 149.461c5.504 48.725 20.48 97.152 45.611 142.592 21.589 39.040 49.024 72.917 80.725 101.077 32.725 29.099 69.931 52.011 109.781 68.267 47.701 19.413 99.2 29.312 151.467 28.8h383.573c20.864-0.043 41.259-2.56 60.459-7.211l122.709 122.709c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.667-938.667c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-off" + ], + "grid": 0 + }, + { + "id": 61, + "paths": [ + "M640 554.667v341.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM298.667 554.667v341.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 640v341.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM870.443 746.496c31.701-13.867 59.264-33.451 81.92-57.045 23.509-24.491 41.685-53.205 53.845-84.267s18.261-64.469 17.579-98.432c-0.64-32.683-7.637-65.792-21.504-97.451-12.032-27.477-28.373-51.84-47.915-72.619-20.181-21.461-43.776-38.997-69.504-52.181-35.669-18.261-75.52-28.16-116.48-28.501h-22.571c-13.269-37.717-32.043-72.235-55.125-102.912-30.635-40.661-68.821-74.453-111.915-99.883s-91.136-42.453-141.568-49.536c-48.555-6.827-99.2-4.437-149.461 8.533s-95.744 35.413-134.912 64.896c-40.661 30.635-74.496 68.821-99.883 111.915s-42.453 91.136-49.536 141.525c-6.827 48.555-4.437 99.2 8.533 149.461 10.709 41.472 27.819 79.659 49.963 113.664 22.699 34.859 50.603 65.323 82.261 90.411 18.475 14.635 45.312 11.52 59.947-6.955s11.52-45.312-6.955-59.947c-24.448-19.371-46.123-42.965-63.744-70.059-17.152-26.368-30.507-56.107-38.869-88.491-10.155-39.253-11.989-78.592-6.656-116.224 5.504-39.125 18.773-76.544 38.571-110.123s46.123-63.275 77.696-87.083c30.379-22.784 65.664-40.235 104.917-50.347s78.592-11.989 116.224-6.656c39.125 5.504 76.544 18.773 110.123 38.571s63.275 46.123 87.083 77.696c22.869 30.379 40.32 65.664 50.432 104.917 4.907 18.517 21.547 31.957 41.301 31.957h53.419c27.605 0.213 54.4 6.912 78.251 19.115 17.195 8.832 32.896 20.48 46.251 34.731 12.971 13.739 23.808 29.952 31.872 48.384 9.301 21.248 13.909 43.264 14.336 64.939 0.469 22.571-3.627 44.885-11.733 65.664s-20.267 39.936-35.883 56.192c-15.019 15.659-33.365 28.715-54.613 38.016-21.589 9.472-31.403 34.603-21.973 56.192s34.603 31.403 56.192 21.973z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-rain" + ], + "grid": 0 + }, + { + "id": 62, + "paths": [ + "M870.443 789.163c31.701-13.867 59.264-33.451 81.92-57.045 23.509-24.491 41.685-53.205 53.845-84.267s18.261-64.469 17.579-98.432c-0.64-32.683-7.637-65.792-21.504-97.451-12.032-27.477-28.373-51.84-47.915-72.619-20.181-21.461-43.776-38.997-69.504-52.181-35.669-18.261-75.52-28.16-116.48-28.501h-22.571c-13.269-37.717-32.043-72.235-55.125-102.912-30.635-40.661-68.821-74.453-111.915-99.883s-91.136-42.453-141.568-49.536c-48.555-6.827-99.2-4.437-149.461 8.533s-95.744 35.413-134.912 64.896c-40.661 30.635-74.496 68.821-99.883 111.915s-42.453 91.136-49.536 141.525c-6.827 48.555-4.437 99.2 8.533 149.461 10.709 41.472 27.819 79.659 49.963 113.664 22.699 34.859 50.603 65.323 82.261 90.411 18.475 14.635 45.312 11.52 59.947-6.955s11.52-45.312-6.955-59.947c-24.448-19.371-46.123-42.965-63.744-70.059-17.152-26.368-30.507-56.107-38.869-88.491-10.155-39.253-11.989-78.592-6.656-116.224 5.504-39.125 18.773-76.544 38.571-110.123s46.123-63.275 77.696-87.083c30.379-22.784 65.664-40.235 104.917-50.347s78.592-11.989 116.224-6.656c39.125 5.504 76.544 18.773 110.123 38.571s63.275 46.123 87.083 77.696c22.869 30.379 40.32 65.664 50.432 104.917 4.907 18.517 21.547 31.957 41.301 31.957h53.419c27.605 0.213 54.4 6.912 78.251 19.115 17.195 8.789 32.896 20.48 46.251 34.731 12.971 13.739 23.808 29.952 31.872 48.384 9.301 21.248 13.909 43.264 14.336 64.939 0.469 22.571-3.627 44.885-11.733 65.664s-20.267 39.936-35.883 56.192c-15.019 15.659-33.365 28.715-54.613 38.016-21.589 9.472-31.403 34.603-21.973 56.192s34.603 31.403 56.192 21.973zM384 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM384 853.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM554.667 768c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM554.667 938.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM725.333 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM725.333 853.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-snow" + ], + "grid": 0 + }, + { + "id": 63, + "paths": [ + "M768 469.333c23.211 0 45.227 4.608 65.237 12.928 20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237s-4.608 45.227-12.928 65.237c-8.619 20.864-21.333 39.637-37.077 55.424s-34.56 28.459-55.424 37.077c-20.011 8.32-42.027 12.928-65.237 12.928h-383.787c-62.379-0.341-121.728-19.669-170.88-53.632-28.971-20.053-54.357-45.141-74.752-74.368-19.712-28.288-34.859-60.501-44.032-96-10.112-39.253-11.989-78.592-6.656-116.224 5.504-39.125 18.773-76.544 38.571-110.123s46.123-63.275 77.696-87.083c30.379-22.869 65.664-40.277 104.917-50.432s78.592-11.989 116.224-6.656c39.125 5.504 76.544 18.773 110.123 38.571s63.275 46.123 87.083 77.696c22.869 30.379 40.32 65.664 50.432 104.917 4.907 18.56 21.547 32 41.301 32zM768 384h-22.187c-13.269-37.717-32.043-72.235-55.125-102.912-30.635-40.661-68.821-74.453-111.915-99.883s-91.136-42.453-141.568-49.579c-48.555-6.827-99.2-4.437-149.461 8.533s-95.744 35.413-134.912 64.939c-40.661 30.592-74.453 68.779-99.883 111.872s-42.453 91.179-49.536 141.568c-6.827 48.555-4.437 99.2 8.533 149.461 11.733 45.44 31.189 86.997 56.661 123.52 26.325 37.717 59.008 70.016 96.213 95.701 63.061 43.605 139.136 68.352 218.965 68.779h384.213c34.603 0 67.712-6.869 97.92-19.413 31.36-12.971 59.52-32 83.115-55.595s42.581-51.755 55.595-83.115c12.501-30.165 19.371-63.275 19.371-97.877s-6.869-67.712-19.413-97.92c-12.971-31.36-32-59.52-55.595-83.115s-51.755-42.581-83.115-55.595c-30.165-12.501-63.275-19.371-97.877-19.371z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud" + ], + "grid": 0 + }, + { + "id": 64, + "paths": [ + "M712.832 798.165l256-256c16.683-16.683 16.683-43.691 0-60.331l-256-256c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l225.835 225.835-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM311.168 225.835l-256 256c-16.683 16.683-16.683 43.691 0 60.331l256 256c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-225.835-225.835 225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "code" + ], + "grid": 0 + }, + { + "id": 65, + "paths": [ + "M224.256 512l-96.256 67.371v-134.784zM896 579.371l-96.256-67.371 96.256-67.371zM725.333 564.096l137.045 95.957-307.712 199.979v-176.512zM373.077 512l138.923-97.237 138.923 97.237-138.923 97.237zM161.621 660.011l137.045-95.915 170.667 119.467v176.512zM535.765 49.877c-6.784-4.565-14.976-7.211-23.765-7.211s-16.981 2.645-23.765 7.211l-426.155 276.992c-11.776 7.765-19.413 20.864-19.413 35.797v298.667c0.043 7.893 2.219 15.616 6.315 22.357 1.749 2.901 3.883 5.589 6.357 8.021 2.005 2.005 4.267 3.797 6.741 5.419l0.811 0.512 425.344 276.48c6.784 4.565 14.976 7.211 23.765 7.211s16.981-2.645 23.765-7.211l426.155-276.992c11.776-7.765 19.413-20.864 19.413-35.797v-298.667c-0.043-7.893-2.219-15.659-6.315-22.357-1.749-2.859-3.883-5.547-6.357-7.979-2.005-2.005-4.267-3.797-6.741-5.419l-0.811-0.512zM554.667 340.437v-176.469l307.712 200.021-137.045 95.915zM469.333 163.968v176.512l-170.667 119.467-137.045-95.957z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "codepen" + ], + "grid": 0 + }, + { + "id": 66, + "paths": [ + "M810.667 640v-256c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939s-3.456 33.92-9.685 48.939c-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685zM85.333 298.667c-23.552 0-42.667 19.115-42.667 42.667v384c0 28.8 5.717 56.405 16.171 81.579 10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213h341.333c28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621 28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621s-5.717-56.405-16.171-81.579c-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-42.667zM128 384h597.333v341.333c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685h-341.333c-17.408 0-33.92-3.456-48.939-9.685-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939zM213.333 42.667v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM384 42.667v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 42.667v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "coffee" + ], + "grid": 0 + }, + { + "id": 67, + "paths": [ + "M597.333 426.667v170.667h-170.667v-170.667zM768 170.667c11.648 0 22.613 2.304 32.64 6.443 10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597s-2.304 22.613-6.443 32.64c-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4h-85.333v-85.333c0-11.648 2.304-22.613 6.443-32.64 4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4zM597.333 341.333h-170.667v-85.333c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.517 28.416 55.424 37.077c20.181 8.32 42.24 12.928 65.28 12.928h85.333v170.667h-85.333c-23.040 0-45.099 4.608-65.28 12.928-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323v-85.333h170.667v85.333c0 23.040 4.608 45.099 12.928 65.28 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971h-85.333v-170.667h85.333c23.040 0 45.099-4.608 65.28-12.928 20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28zM341.333 341.333h-85.333c-11.648 0-22.613-2.304-32.64-6.443-10.411-4.309-19.797-10.667-27.691-18.56s-14.251-17.323-18.56-27.733c-4.139-9.984-6.443-20.949-6.443-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM341.333 682.667v85.333c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4zM682.667 682.667h85.333c11.648 0 22.613 2.304 32.64 6.443 10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597s-2.304 22.613-6.443 32.64c-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "command" + ], + "grid": 0 + }, + { + "id": 68, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM733.397 344.576c2.816-8.363 3.072-17.835 0-26.965-7.467-22.357-31.616-34.432-53.973-26.965l-271.36 90.453c-12.373 4.181-22.571 13.781-26.965 26.965l-90.453 271.36c-2.816 8.363-3.072 17.835 0 26.965 7.467 22.357 31.616 34.432 53.973 26.965l271.36-90.453c12.373-4.181 22.571-13.781 26.965-26.965zM625.451 398.549l-56.747 170.155-170.155 56.747 56.704-170.155z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "compass" + ], + "grid": 0 + }, + { + "id": 69, + "paths": [ + "M469.333 341.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v384c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h384c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-384c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM469.333 426.667h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v384c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-384c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-384c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM213.333 597.333h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-384c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v42.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-42.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-384c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v384c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h42.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "copy" + ], + "grid": 0 + }, + { + "id": 70, + "paths": [ + "M810.667 170.667v298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685h-409.003l140.501-140.501c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-213.333 213.333c-0.043 0.043-0.085 0.128-0.171 0.171-4.011 4.053-7.040 8.704-9.088 13.653-4.309 10.453-4.309 22.229 0 32.683 2.048 4.949 5.077 9.6 9.088 13.653 0.043 0.043 0.085 0.128 0.171 0.171l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-140.501-140.501h409.003c28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-down-left" + ], + "grid": 0 + }, + { + "id": 71, + "paths": [ + "M128 170.667v298.667c0 28.8 5.717 56.405 16.171 81.579 10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213h409.003l-140.501 140.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l213.333-213.333c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l140.501 140.501h-409.003c-17.408 0-33.92-3.456-48.939-9.685-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-down-right" + ], + "grid": 0 + }, + { + "id": 72, + "paths": [ + "M853.333 128h-298.667c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.336 69.291c-10.453 25.173-16.171 52.779-16.171 81.579v409.003l-140.501-140.501c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l213.333 213.333c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 5.675 0.043 11.349-1.024 16.64-3.243 5.035-2.091 9.771-5.163 13.867-9.259l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-140.501 140.501v-409.003c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h298.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-left-down" + ], + "grid": 0 + }, + { + "id": 73, + "paths": [ + "M853.333 810.667h-298.667c-17.408 0-33.92-3.456-48.939-9.685-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939v-409.003l140.501 140.501c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-213.333-213.333c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-5.035 2.091-9.728 5.163-13.824 9.259l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l140.501-140.501v409.003c0 28.8 5.717 56.405 16.171 81.579 10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213h298.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-left-up" + ], + "grid": 0 + }, + { + "id": 74, + "paths": [ + "M170.667 213.333h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v409.003l-140.501-140.501c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l213.333 213.333c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 5.675 0.043 11.349-1.024 16.64-3.243 5.035-2.091 9.771-5.163 13.867-9.259l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-140.501 140.501v-409.003c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-298.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-right-down" + ], + "grid": 0 + }, + { + "id": 75, + "paths": [ + "M170.667 896h298.667c28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621v-409.003l140.501 140.501c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-213.333-213.333c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.6 5.077-13.653 9.088-0.043 0.043-0.128 0.085-0.171 0.171l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l140.501-140.501v409.003c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685h-298.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-right-up" + ], + "grid": 0 + }, + { + "id": 76, + "paths": [ + "M896 853.333v-298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-409.003l140.501-140.501c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-213.333 213.333c-0.043 0.043-0.085 0.128-0.171 0.171-4.011 4.053-7.040 8.704-9.088 13.653-4.309 10.453-4.309 22.229 0 32.683 2.091 5.035 5.163 9.728 9.259 13.824l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-140.501-140.501h409.003c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v298.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-up-left" + ], + "grid": 0 + }, + { + "id": 77, + "paths": [ + "M213.333 853.333v-298.667c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h409.003l-140.501 140.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l213.333-213.333c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l140.501 140.501h-409.003c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.291 46.293s-35.499 43.136-46.293 69.248c-10.453 25.216-16.171 52.821-16.171 81.621v298.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "corner-up-right" + ], + "grid": 0 + }, + { + "id": 78, + "paths": [ + "M256 213.333h512c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v512c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-512c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM384 341.333c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h256c23.552 0 42.667-19.115 42.667-42.667v-256c0-23.552-19.115-42.667-42.667-42.667zM426.667 426.667h170.667v170.667h-170.667zM42.667 640h85.333v128c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h85.333v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h170.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h85.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-128h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-128h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-85.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-85.333v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-170.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h85.333v128h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cpu" + ], + "grid": 0 + }, + { + "id": 79, + "paths": [ + "M128 128c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v512c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h768c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM938.667 384h-853.333v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h768c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM85.333 469.333h853.333v298.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-768c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "credit-card" + ], + "grid": 0 + }, + { + "id": 80, + "paths": [ + "M301.952 301.952l381.099-3.285c5.504 0 10.965 1.152 15.915 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v384h-384c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-15.915zM43.051 304.213l173.568-1.493-3.285 379.563c0 17.621 3.456 34.176 9.728 49.323 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.272 31.659 9.728 48.939 9.728h384v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667v-384c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-49.323-9.685l-379.563 3.285 1.493-173.568c0.213-23.595-18.731-42.837-42.283-43.051s-42.837 18.731-43.051 42.283l-1.536 175.061-175.061 1.536c-23.552 0.213-42.496 19.456-42.283 43.051s19.456 42.496 43.051 42.283z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "crop" + ], + "grid": 0 + }, + { + "id": 81, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM554.667 893.653v-125.653c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v125.653c-36.565-4.053-71.509-13.184-104.192-26.709-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-13.525-32.683-22.699-67.627-26.709-104.192h125.653c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-125.653c4.053-36.565 13.184-71.509 26.709-104.192 19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c32.683-13.525 67.627-22.699 104.192-26.709v125.653c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-125.653c36.565 4.053 71.509 13.184 104.192 26.709 46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 13.525 32.683 22.699 67.627 26.709 104.192h-125.653c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h125.653c-4.053 36.565-13.184 71.509-26.709 104.192-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-32.683 13.525-67.627 22.699-104.192 26.709z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "crosshair" + ], + "grid": 0 + }, + { + "id": 82, + "paths": [ + "M853.333 213.419c-0.043 0.213-0.683 4.437-9.088 12.117-3.925 3.627-9.173 7.637-15.915 11.819-10.411 6.485-23.68 13.056-39.808 19.328-26.155 10.197-58.581 19.2-95.957 26.24-52.821 9.984-114.475 15.744-180.565 15.744s-127.744-5.76-180.565-15.701c-37.419-7.040-69.803-16.085-96-26.283-16.128-6.272-29.355-12.843-39.765-19.328-6.741-4.181-11.989-8.192-15.915-11.819-8.363-7.68-9.045-11.861-9.088-12.117 0-0.085 0.256-4.139 9.088-12.288 3.925-3.627 9.173-7.637 15.915-11.819 10.411-6.485 23.637-13.056 39.765-19.328 26.197-10.197 58.581-19.243 96-26.283 52.821-9.941 114.475-15.701 180.565-15.701s127.744 5.76 180.565 15.701c37.419 7.040 69.803 16.043 95.957 26.24 16.128 6.272 29.397 12.843 39.808 19.328 6.741 4.224 11.989 8.192 15.915 11.819 8.491 7.851 9.088 12.032 9.088 12.331zM853.333 620.032v190.763c0 0-0.085 0.725-0.725 2.091-1.024 2.048-3.371 5.632-8.491 10.325-3.968 3.627-9.216 7.637-15.957 11.819-10.368 6.443-23.595 12.971-39.637 19.243-25.984 10.112-58.155 19.072-95.36 26.027-52.864 9.941-114.645 15.701-181.163 15.701s-128.299-5.76-181.12-15.701c-37.205-6.997-69.376-15.915-95.36-26.027-16.085-6.272-29.269-12.757-39.637-19.243-6.741-4.224-11.989-8.192-15.957-11.819-5.12-4.693-7.509-8.277-8.491-10.325-0.683-1.365-0.768-2.219-0.768-2.219v-190.635c10.624 5.419 21.973 10.453 33.877 15.104 32 12.459 69.461 22.656 110.549 30.379 58.539 11.008 125.653 17.152 196.907 17.152s138.368-6.144 196.907-17.152c41.088-7.723 78.549-17.92 110.549-30.379 11.947-4.651 23.296-9.685 33.877-15.104zM853.333 321.152v190.421c0 0.171 0 0.341 0 0.555-8.064 11.349-8.661 11.861-9.259 12.416-3.968 3.627-9.216 7.637-15.957 11.819-10.368 6.443-23.595 12.971-39.637 19.243-25.984 10.112-58.155 19.072-95.36 26.027-52.821 9.941-114.603 15.701-181.12 15.701s-128.299-5.76-181.12-15.701c-37.205-6.997-69.376-15.915-95.36-26.027-16.085-6.272-29.269-12.757-39.637-19.243-6.741-4.224-11.989-8.192-15.957-11.819-5.12-4.693-7.509-8.277-8.491-10.325-0.683-1.365-0.768-2.091-0.768-2.091 0-0.256 0-0.512 0-0.725v-190.251c10.581 5.376 21.888 10.411 33.792 15.061 32.171 12.544 69.845 22.827 111.189 30.592 58.539 11.008 125.483 17.195 196.352 17.195s137.813-6.187 196.352-17.195c41.301-7.765 79.019-18.048 111.189-30.635 11.904-4.651 23.211-9.643 33.792-15.061zM85.333 213.333v597.333c0 2.475 0.085 4.949 0.299 7.424 0.981 11.648 4.309 22.528 9.173 32.469 6.784 13.781 16.512 25.557 27.392 35.541 8.576 7.851 18.219 14.976 28.544 21.419 15.829 9.856 34.005 18.603 53.803 26.325 32 12.459 69.461 22.656 110.549 30.379 58.539 10.965 125.653 17.109 196.907 17.109s138.368-6.144 196.907-17.152c41.088-7.723 78.549-17.92 110.549-30.379 19.755-7.68 37.931-16.427 53.803-26.325 10.325-6.443 20.011-13.568 28.544-21.419 10.88-9.984 20.608-21.76 27.392-35.541 4.864-9.941 8.192-20.821 9.173-32.469 0.213-2.432 0.299-4.907 0.299-7.381v-597.333c0-2.389-0.085-4.779-0.299-7.168-0.981-11.605-4.224-22.443-9.088-32.341-6.741-13.739-16.427-25.515-27.264-35.499-8.533-7.851-18.176-14.976-28.544-21.419-15.872-9.899-34.133-18.688-53.973-26.453-32.171-12.544-69.888-22.827-111.189-30.635-58.496-10.965-125.44-17.152-196.309-17.152s-137.813 6.187-196.352 17.195c-41.344 7.765-79.019 18.048-111.189 30.592-19.84 7.765-38.101 16.555-53.973 26.453-10.325 6.443-19.968 13.568-28.501 21.419-10.837 9.984-20.565 21.717-27.264 35.499-4.864 9.899-8.107 20.736-9.088 32.341-0.213 2.389-0.299 4.779-0.299 7.168z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "database" + ], + "grid": 0 + }, + { + "id": 83, + "paths": [ + "M896 213.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v512c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-535.296l-261.333-298.667 261.333-298.667zM896 128h-554.667c-12.8 0-24.235 5.632-32.128 14.549l-298.667 341.333c-14.208 16.213-13.909 40.192 0 56.192l298.667 341.333c8.448 9.643 20.224 14.549 32.128 14.592h554.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM481.835 414.165l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "delete" + ], + "grid": 0 + }, + { + "id": 84, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM682.667 512c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM597.333 512c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "disc" + ], + "grid": 0 + }, + { + "id": 85, + "paths": [ + "M469.333 469.333h-64c-14.507 0-28.288-2.901-40.789-8.064-13.013-5.376-24.789-13.312-34.645-23.168s-17.792-21.632-23.168-34.645c-5.163-12.501-8.064-26.283-8.064-40.789s2.901-28.288 8.064-40.789c5.376-13.013 13.312-24.789 23.168-34.645s21.632-17.792 34.645-23.168c12.501-5.163 26.283-8.064 40.789-8.064h64zM554.667 554.667h64c14.507 0 28.288 2.901 40.789 8.064 13.013 5.376 24.789 13.312 34.645 23.168s17.792 21.632 23.168 34.645c5.163 12.501 8.064 26.283 8.064 40.789s-2.901 28.288-8.064 40.789c-5.376 13.013-13.312 24.789-23.168 34.645s-21.632 17.792-34.645 23.168c-12.501 5.163-26.283 8.064-40.789 8.064h-64zM725.333 170.667h-170.667v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v128h-64c-25.941 0-50.773 5.163-73.429 14.549-23.552 9.771-44.672 24.021-62.336 41.685s-31.915 38.784-41.685 62.336c-9.387 22.656-14.549 47.488-14.549 73.429s5.163 50.773 14.549 73.429c9.728 23.509 24.021 44.672 41.685 62.336s38.784 31.915 62.336 41.685c22.656 9.387 47.488 14.549 73.429 14.549h64v213.333h-213.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h213.333v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128h64c25.941 0 50.773-5.163 73.429-14.549 23.509-9.728 44.672-24.021 62.336-41.685s31.915-38.784 41.685-62.336c9.387-22.656 14.549-47.488 14.549-73.429s-5.163-50.773-14.549-73.429c-9.728-23.509-24.021-44.672-41.685-62.336s-38.784-31.915-62.336-41.685c-22.656-9.387-47.488-14.549-73.429-14.549h-64v-213.333h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "dollar-sign" + ], + "grid": 0 + }, + { + "id": 86, + "paths": [ + "M469.333 512v281.003l-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l170.667 170.667c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 5.675 0.043 11.349-1.024 16.64-3.243 5.035-2.091 9.771-5.163 13.867-9.259l170.667-170.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835v-281.003c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM915.413 806.741c28.288-19.883 51.413-44.587 68.949-72.192 18.176-28.672 30.293-60.416 36.011-93.269s5.077-66.816-2.347-99.968c-7.125-31.915-20.565-62.933-40.448-91.264-17.28-24.576-38.144-45.227-61.483-61.739-24.149-17.067-50.816-29.611-78.763-37.419-22.485-6.229-45.739-9.472-69.163-9.557h-22.315c-13.227-37.717-31.915-72.277-54.955-102.955-30.549-40.704-68.693-74.581-111.744-100.053s-91.093-42.581-141.483-49.792c-48.555-6.869-99.2-4.565-149.504 8.363s-95.787 35.243-134.997 64.683c-40.704 30.549-74.581 68.693-100.053 111.744s-42.581 91.093-49.792 141.483c-6.912 48.555-4.608 99.2 8.32 149.504 15.829 61.696 45.867 116.267 84.608 159.317 15.787 17.493 42.752 18.944 60.245 3.157s18.944-42.752 3.157-60.245c-29.525-32.811-52.992-75.093-65.408-123.435-10.069-39.253-11.861-78.592-6.485-116.224 5.589-39.125 18.901-76.501 38.741-110.080s46.251-63.275 77.867-86.997c30.421-22.827 65.749-40.192 105.003-50.261s78.592-11.861 116.267-6.485c39.125 5.589 76.501 18.901 110.080 38.741s63.232 46.208 86.955 77.824c22.827 30.421 40.192 65.749 50.261 105.003 4.864 18.56 21.504 32.043 41.301 32.043h53.589c15.787 0.043 31.445 2.219 46.507 6.443 18.731 5.248 36.48 13.611 52.48 24.917 15.445 10.923 29.355 24.661 40.96 41.131 13.355 18.987 22.229 39.637 26.965 60.8 4.949 22.016 5.376 44.715 1.536 66.645s-11.904 43.136-24.021 62.208c-11.605 18.304-26.965 34.731-45.952 48.085-19.285 13.568-23.893 40.149-10.368 59.435s40.149 23.893 59.435 10.368z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "download-cloud" + ], + "grid": 0 + }, + { + "id": 87, + "paths": [ + "M853.333 640v170.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v170.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 537.003v-409.003c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v409.003l-140.501-140.501c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l213.333 213.333c0.085 0.085 0.171 0.171 0.256 0.256 4.053 3.968 8.661 6.955 13.568 9.003 5.12 2.133 10.624 3.2 16.085 3.243 0.171 0 0.341 0 0.469 0 5.461-0.043 10.965-1.109 16.085-3.243 5.035-2.091 9.728-5.163 13.824-9.259l213.333-213.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "download" + ], + "grid": 0 + }, + { + "id": 88, + "paths": [ + "M542.165 84.608c-16.683-16.683-43.733-16.64-60.373 0.043l-241.195 241.621c-36.693 36.736-64.469 79.147-83.243 124.459-19.456 47.019-29.184 97.109-29.141 147.115s9.771 100.096 29.227 147.115c18.773 45.312 46.592 87.68 83.328 124.373s79.147 64.469 124.459 83.243c47.019 19.456 97.109 29.184 147.115 29.141s100.096-9.771 147.115-29.227c45.312-18.773 87.68-46.592 124.373-83.328s64.469-79.147 83.243-124.459c19.456-47.019 29.184-97.109 29.141-147.115s-9.771-100.096-29.227-147.115c-18.773-45.312-46.592-87.68-83.285-124.373zM512.043 175.147l211.285 211.285c28.672 28.672 50.261 61.611 64.811 96.725 15.147 36.523 22.741 75.477 22.741 114.432 0 38.997-7.552 77.952-22.656 114.475-14.549 35.115-36.096 68.096-64.725 96.768-28.672 28.672-61.611 50.261-96.725 64.811-36.523 15.147-75.477 22.741-114.432 22.741-38.997 0-77.952-7.552-114.475-22.656-35.115-14.549-68.096-36.096-96.768-64.725s-50.261-61.611-64.811-96.725c-15.147-36.523-22.741-75.477-22.741-114.432s7.552-77.952 22.656-114.475c14.549-35.115 36.096-68.096 64.725-96.768z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "droplet" + ], + "grid": 0 + }, + { + "id": 89, + "paths": [ + "M695.168 97.835l-576 576c-4.992 4.949-8.96 11.435-11.008 18.944l-64 234.667c-1.963 6.955-2.091 14.763 0 22.443 6.187 22.741 29.653 36.139 52.395 29.952l234.667-64c6.784-1.792 13.44-5.504 18.944-11.008l576-576c15.573-15.573 27.435-33.621 35.413-52.949 8.277-20.011 12.416-41.301 12.416-62.549s-4.139-42.539-12.416-62.549c-7.979-19.285-19.84-37.333-35.413-52.949s-33.621-27.435-52.949-35.413c-20.011-8.32-41.301-12.416-62.549-12.416s-42.539 4.139-62.549 12.416c-19.285 7.979-37.333 19.84-52.949 35.413zM755.499 158.165c7.509-7.509 16.128-13.141 25.259-16.939 9.515-3.925 19.712-5.931 29.909-5.931s20.395 2.005 29.909 5.931c9.131 3.797 17.707 9.387 25.259 16.939s13.141 16.128 16.939 25.259c3.925 9.515 5.888 19.712 5.888 29.909s-2.005 20.395-5.931 29.909c-3.797 9.131-9.387 17.707-16.939 25.259l-567.936 567.979-151.723 41.387 41.387-151.68z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "edit-2" + ], + "grid": 0 + }, + { + "id": 90, + "paths": [ + "M512 896h384c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-384c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM673.835 119.168l-533.333 533.333c-5.205 5.163-9.259 11.947-11.221 19.84l-42.667 170.667c-1.664 6.4-1.792 13.568 0 20.693 5.717 22.869 28.885 36.779 51.755 31.061l170.667-42.667c7.125-1.749 14.080-5.504 19.84-11.221l533.333-533.333c12.715-12.715 22.357-27.435 28.885-43.179 6.784-16.341 10.112-33.707 10.112-50.987s-3.371-34.688-10.112-50.987c-6.528-15.744-16.171-30.464-28.885-43.179s-27.435-22.357-43.179-28.885c-16.341-6.784-33.707-10.155-51.029-10.155s-34.688 3.371-50.987 10.112c-15.744 6.528-30.464 16.171-43.179 28.885zM734.165 179.499c4.651-4.651 9.899-8.064 15.488-10.368 5.803-2.432 12.075-3.627 18.347-3.627s12.544 1.237 18.347 3.627c5.589 2.304 10.837 5.76 15.488 10.368s8.064 9.899 10.368 15.488c2.432 5.803 3.627 12.075 3.627 18.347s-1.237 12.544-3.627 18.347c-2.304 5.589-5.76 10.837-10.368 15.488l-524.971 524.971-90.24 22.571 22.571-90.24z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "edit-3" + ], + "grid": 0 + }, + { + "id": 91, + "paths": [ + "M469.333 128h-298.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v298.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h298.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM759.168 76.501l-405.333 405.333c-5.205 5.163-9.259 11.947-11.221 19.84l-42.667 170.667c-1.664 6.4-1.792 13.568 0 20.693 5.717 22.869 28.885 36.779 51.755 31.061l170.667-42.667c7.125-1.749 14.080-5.504 19.84-11.221l405.333-405.333c12.715-12.715 22.357-27.435 28.885-43.179 6.784-16.341 10.112-33.707 10.112-50.987s-3.371-34.688-10.112-50.987c-6.528-15.744-16.171-30.464-28.885-43.179s-27.435-22.357-43.179-28.885c-16.341-6.784-33.707-10.155-51.029-10.155s-34.688 3.371-50.987 10.112c-15.744 6.528-30.464 16.171-43.179 28.885zM819.499 136.832c4.651-4.651 9.899-8.064 15.488-10.368 5.803-2.432 12.075-3.627 18.347-3.627s12.544 1.237 18.347 3.627c5.589 2.304 10.837 5.76 15.488 10.368s8.064 9.899 10.368 15.488c2.432 5.803 3.627 12.075 3.627 18.347s-1.237 12.544-3.627 18.347c-2.304 5.589-5.76 10.837-10.368 15.488l-396.971 396.971-90.197 22.571 22.571-90.197z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "edit" + ], + "grid": 0 + }, + { + "id": 92, + "paths": [ + "M725.333 554.667v256c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-469.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-469.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v469.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h469.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM456.832 627.499l396.501-396.501v153.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-0.128 0-0.213 0-0.341-0.043-5.632-1.195-11.008-3.2-15.957-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h153.003l-396.501 396.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "external-link" + ], + "grid": 0 + }, + { + "id": 93, + "paths": [ + "M432.128 222.464c27.776-6.485 55.253-9.429 79.36-9.131 34.133 0 65.963 4.864 96 13.397 36.779 10.453 71.083 26.453 102.784 46.037 47.744 29.525 89.131 66.859 123.477 104.448 27.477 30.080 50.133 59.947 67.627 85.376 13.781 20.053 24.235 37.163 31.232 49.323-23.296 40.619-49.493 77.696-75.819 108.416-15.317 17.877-13.269 44.843 4.608 60.16s44.843 13.269 60.16-4.608c34.901-40.704 68.736-90.112 97.408-143.787 6.315-11.904 6.955-26.368 0.555-39.211-0.64-1.237-16.896-33.664-47.787-78.635-19.243-27.989-44.288-61.099-74.965-94.635-38.144-41.771-85.504-84.779-141.611-119.467-37.376-23.125-78.891-42.667-124.331-55.552-37.205-10.539-76.885-16.597-118.315-16.597-31.317-0.384-65.707 3.371-99.84 11.349-22.955 5.376-37.205 28.331-31.829 51.285s28.331 37.205 51.285 31.829zM427.819 488.192l107.989 107.989c-7.765 2.56-15.872 4.011-24.021 4.309-11.179 0.384-22.357-1.365-32.939-5.333-10.155-3.797-19.755-9.6-28.245-17.536s-14.976-17.109-19.456-26.965c-4.693-10.24-7.253-21.291-7.637-32.469-0.341-10.155 1.067-20.352 4.309-30.037zM255.275 315.605l108.928 108.928c-6.187 9.856-11.307 20.224-15.275 30.891-7.936 21.205-11.435 43.605-10.667 65.792s5.888 44.288 15.275 64.853c9.045 19.84 22.101 38.272 38.955 53.973s36.139 27.392 56.576 35.029c21.205 7.936 43.605 11.435 65.792 10.667s44.288-5.888 64.853-15.275c6.784-3.072 13.397-6.656 19.797-10.667l99.072 99.072c-21.931 12.715-44.672 23.040-67.883 31.019-39.040 13.44-79.317 20.267-119.168 20.736-33.109 0-64.939-4.864-94.976-13.397-36.779-10.453-71.083-26.453-102.784-46.037-47.744-29.525-89.131-66.859-123.477-104.448-27.477-30.080-50.133-59.947-67.627-85.376-13.739-20.011-24.192-37.12-31.232-49.28 44.757-77.739 101.376-144.128 163.84-196.523zM12.501 72.832l182.229 182.229c-73.856 63.104-139.477 143.275-189.653 236.757-6.315 11.904-6.997 26.411-0.555 39.253 0.64 1.237 16.896 33.664 47.787 78.635 19.243 27.989 44.288 61.099 74.965 94.635 38.144 41.771 85.504 84.779 141.611 119.467 37.376 23.125 78.891 42.667 124.331 55.552 37.163 10.581 76.843 16.64 119.296 16.64 48.896-0.597 98.261-8.96 145.92-25.387 35.243-12.16 69.589-28.672 102.144-49.664l190.549 190.549c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.752-938.752c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye-off" + ], + "grid": 0 + }, + { + "id": 94, + "paths": [ + "M4.523 492.928c-5.803 11.691-6.229 25.728 0 38.144 0.64 1.237 16.896 33.664 47.787 78.635 19.243 27.989 44.288 61.099 74.965 94.635 38.144 41.771 85.504 84.779 141.611 119.467 37.376 23.125 78.891 42.667 124.331 55.552 37.163 10.581 76.843 16.64 118.784 16.64s81.621-6.059 118.784-16.64c45.44-12.885 86.912-32.427 124.331-55.552 56.107-34.688 103.467-77.696 141.611-119.467 30.635-33.536 55.723-66.645 74.965-94.635 30.891-44.971 47.189-77.397 47.787-78.635 5.803-11.691 6.229-25.728 0-38.144-0.64-1.237-16.896-33.664-47.787-78.635-19.243-27.989-44.288-61.099-74.965-94.635-38.144-41.771-85.504-84.779-141.611-119.467-37.376-23.125-78.891-42.667-124.331-55.552-37.163-10.581-76.843-16.64-118.784-16.64s-81.621 6.059-118.784 16.64c-45.44 12.885-86.912 32.427-124.331 55.552-56.107 34.688-103.467 77.696-141.611 119.467-30.677 33.536-55.723 66.603-74.965 94.635-30.891 44.971-47.189 77.397-47.787 78.635zM91.349 512c7.040-12.117 17.493-29.312 31.317-49.408 17.493-25.429 40.107-55.296 67.627-85.376 34.347-37.589 75.733-74.923 123.477-104.448 31.701-19.584 66.005-35.627 102.784-46.037 29.995-8.533 61.824-13.397 95.445-13.397s65.451 4.864 95.488 13.397c36.779 10.453 71.083 26.453 102.784 46.037 47.744 29.525 89.131 66.859 123.477 104.448 27.477 30.080 50.133 59.947 67.627 85.376 13.781 20.096 24.277 37.248 31.317 49.408-7.040 12.117-17.493 29.312-31.317 49.408-17.493 25.429-40.107 55.296-67.627 85.376-34.347 37.589-75.733 74.923-123.477 104.448-31.701 19.584-66.005 35.627-102.784 46.037-30.037 8.533-61.867 13.397-95.488 13.397s-65.451-4.864-95.488-13.397c-36.779-10.453-71.083-26.453-102.784-46.037-47.744-29.525-89.131-66.859-123.477-104.448-27.477-30.080-50.133-59.947-67.627-85.376-13.781-20.096-24.277-37.291-31.275-49.408zM682.667 512c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM597.333 512c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye" + ], + "grid": 0 + }, + { + "id": 95, + "paths": [ + "M725.333 128v85.333h-85.333c-11.477 0-22.528 2.304-32.64 6.485-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64v128c0 23.552 19.115 42.667 42.667 42.667h116.011l-21.333 85.333h-94.677c-23.552 0-42.667 19.115-42.667 42.667v298.667h-85.333v-298.667c0-23.552-19.115-42.667-42.667-42.667h-85.333v-85.333h85.333c23.552 0 42.667-19.115 42.667-42.667v-128c0-23.211 4.608-45.227 12.928-65.237 8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928zM768 42.667h-128c-34.603 0-67.712 6.869-97.92 19.413-31.36 12.971-59.52 32-83.115 55.595s-42.581 51.712-55.552 83.072c-12.544 30.208-19.413 63.317-19.413 97.92v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667v170.667c0 23.552 19.115 42.667 42.667 42.667h85.333v298.667c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-298.667h85.333c19.883 0 36.608-13.611 41.387-32.299l42.667-170.667c5.717-22.869-8.192-46.037-31.061-51.755-3.541-0.896-7.125-1.323-10.325-1.28h-128v-85.333h128c23.552 0 42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "facebook" + ], + "grid": 0 + }, + { + "id": 96, + "paths": [ + "M597.333 723.413v-422.827l271.829 211.413zM128 723.413v-422.827l271.829 211.413zM111.531 844.331l384-298.667c10.709-8.32 16.341-20.736 16.469-33.28v298.283c0 23.552 19.115 42.667 42.667 42.667 9.899 0 19.029-3.371 26.197-9.003l384-298.667c18.603-14.464 21.931-41.259 7.467-59.861-2.304-2.944-4.907-5.504-7.467-7.467l-384-298.667c-18.603-14.464-45.397-11.136-59.861 7.467-6.101 7.851-9.045 17.109-9.003 26.197v298.24c-0.085-9.003-3.029-18.091-9.003-25.771-2.304-2.944-4.907-5.504-7.467-7.467l-384-298.667c-18.603-14.464-45.397-11.136-59.861 7.467-6.101 7.851-9.045 17.109-9.003 26.197v597.333c0 23.552 19.115 42.667 42.667 42.667 9.899 0 19.029-3.371 26.197-9.003z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "fast-forward" + ], + "grid": 0 + }, + { + "id": 97, + "paths": [ + "M772.736 673.792c2.944-2.261 5.547-4.907 7.808-7.851l113.195-113.536c28.544-28.544 50.176-61.568 64.811-96.811 15.147-36.608 22.741-75.563 22.741-114.475s-7.552-77.867-22.741-114.475c-14.592-35.243-36.224-68.267-64.811-96.811-28.544-28.544-61.568-50.176-96.811-64.811-36.608-15.147-75.563-22.741-114.475-22.741s-77.867 7.552-114.475 22.741c-35.243 14.592-68.267 36.224-96.811 64.811l-288 288c-8.32 8.32-12.501 19.243-12.501 30.165v345.003l-115.499 115.499c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l115.499-115.499h345.003c11.819 0 22.485-4.779 30.208-12.544zM401.664 682.667h241.707l-85.077 85.333h-241.963zM728.448 597.333h-241.451l225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-396.501 396.501v-242.005l275.499-275.499c20.523-20.523 44.032-35.925 69.12-46.293 26.069-10.795 53.931-16.213 81.792-16.213s55.723 5.419 81.792 16.213c25.088 10.411 48.64 25.813 69.12 46.293s35.925 44.032 46.293 69.12c10.795 26.069 16.213 53.931 16.213 81.792s-5.419 55.723-16.213 81.792c-10.411 25.088-25.813 48.64-46.293 69.12z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "feather" + ], + "grid": 0 + }, + { + "id": 98, + "paths": [ + "M554.667 128h106.667c14.507 0 28.288 2.901 40.789 8.064 13.013 5.376 24.789 13.312 34.645 23.168s17.792 21.632 23.168 34.645c5.163 12.501 8.064 26.283 8.064 40.789s-2.901 28.288-8.064 40.789c-5.376 13.013-13.312 24.789-23.168 34.645s-21.632 17.792-34.645 23.168c-12.501 5.163-26.283 8.064-40.789 8.064h-106.667zM554.667 533.333c0-14.507 2.901-28.288 8.064-40.789 5.376-13.013 13.312-24.789 23.168-34.645s21.632-17.792 34.645-23.168c12.501-5.163 26.283-8.064 40.789-8.064s28.288 2.901 40.789 8.064c13.013 5.376 24.789 13.312 34.645 23.168s17.792 21.632 23.168 34.645c5.163 12.501 8.064 26.283 8.064 40.789s-2.901 28.288-8.064 40.789c-5.376 13.013-13.312 24.789-23.168 34.645s-21.632 17.792-34.645 23.168c-12.501 5.163-26.283 8.064-40.789 8.064s-28.288-2.901-40.789-8.064c-13.013-5.376-24.789-13.312-34.645-23.168s-17.792-21.632-23.168-34.645c-5.163-12.501-8.064-26.283-8.064-40.789zM469.333 341.333h-106.667c-14.507 0-28.288-2.901-40.789-8.064-13.013-5.376-24.789-13.312-34.645-23.168s-17.792-21.632-23.168-34.645c-5.163-12.501-8.064-26.283-8.064-40.789s2.901-28.288 8.064-40.789c5.376-13.013 13.312-24.789 23.168-34.645s21.632-17.792 34.645-23.168c12.501-5.163 26.283-8.064 40.789-8.064h106.667zM469.333 725.333v106.667c0 14.507-2.901 28.288-8.064 40.789-5.376 13.013-13.312 24.789-23.168 34.645s-21.632 17.792-34.645 23.168c-12.501 5.163-26.283 8.064-40.789 8.064s-28.288-2.901-40.789-8.064c-13.013-5.376-24.789-13.312-34.645-23.168s-17.792-21.632-23.168-34.645c-5.163-12.501-8.064-26.283-8.064-40.789s2.901-28.288 8.064-40.789c5.376-13.013 13.312-24.789 23.168-34.645s21.632-17.792 34.645-23.168c12.501-5.163 26.283-8.064 40.789-8.064zM170.667 533.333c0 25.941 5.163 50.773 14.549 73.429 9.728 23.509 24.021 44.672 41.685 62.336 4.779 4.779 9.813 9.301 15.061 13.568-5.248 4.267-10.283 8.789-15.061 13.568-17.664 17.664-31.915 38.784-41.685 62.336-9.387 22.656-14.549 47.488-14.549 73.429s5.163 50.773 14.549 73.429c9.728 23.509 24.021 44.672 41.685 62.336s38.784 31.915 62.336 41.685c22.656 9.387 47.488 14.549 73.429 14.549s50.773-5.163 73.429-14.549c23.509-9.728 44.672-24.021 62.336-41.685s31.915-38.784 41.685-62.336c9.387-22.656 14.549-47.488 14.549-73.429v-139.008c10.411 6.955 21.504 12.928 33.237 17.792 22.656 9.387 47.488 14.549 73.429 14.549s50.773-5.163 73.429-14.549c23.509-9.728 44.672-24.021 62.336-41.685s31.915-38.784 41.685-62.336c9.387-22.656 14.549-47.488 14.549-73.429s-5.163-50.773-14.549-73.429c-9.728-23.509-24.021-44.672-41.685-62.336-4.779-4.779-9.813-9.301-15.061-13.568 5.248-4.267 10.283-8.789 15.061-13.568 17.664-17.664 31.915-38.784 41.685-62.336 9.387-22.656 14.549-47.488 14.549-73.429s-5.163-50.773-14.549-73.429c-9.728-23.509-24.021-44.672-41.685-62.336s-38.784-31.915-62.336-41.685c-22.656-9.387-47.488-14.549-73.429-14.549h-298.667c-25.941 0-50.773 5.163-73.429 14.549-23.552 9.771-44.672 24.021-62.336 41.685s-31.915 38.784-41.685 62.336c-9.387 22.656-14.549 47.488-14.549 73.429s5.163 50.773 14.549 73.429c9.771 23.552 24.021 44.672 41.685 62.336 4.779 4.779 9.813 9.301 15.061 13.568-5.248 4.267-10.283 8.789-15.061 13.568-17.664 17.664-31.915 38.784-41.685 62.336-9.387 22.656-14.549 47.488-14.549 73.429zM256 533.333c0-14.507 2.901-28.288 8.064-40.789 5.376-13.013 13.312-24.789 23.168-34.645s21.632-17.792 34.645-23.168c12.501-5.163 26.283-8.064 40.789-8.064h106.667v213.333h-106.667c-14.507 0-28.288-2.901-40.789-8.064-13.013-5.376-24.789-13.312-34.645-23.168s-17.792-21.632-23.168-34.645c-5.163-12.501-8.064-26.283-8.064-40.789z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "figma" + ], + "grid": 0 + }, + { + "id": 99, + "paths": [ + "M750.336 298.667h-110.336v-110.336zM883.328 310.997l-255.829-255.829c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-5.205-2.176-10.795-3.243-16.341-3.243h-341.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-11.776-4.779-22.443-12.501-30.165zM554.667 128v213.333c0 23.552 19.115 42.667 42.667 42.667h213.333v469.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM384 682.667h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-minus" + ], + "grid": 0 + }, + { + "id": 100, + "paths": [ + "M750.336 298.667h-110.336v-110.336zM883.328 310.997l-255.829-255.829c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-5.205-2.176-10.795-3.243-16.341-3.243h-341.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-11.776-4.779-22.443-12.501-30.165zM554.667 128v213.333c0 23.552 19.115 42.667 42.667 42.667h213.333v469.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM384 682.667h85.333v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-plus" + ], + "grid": 0 + }, + { + "id": 101, + "paths": [ + "M750.336 298.667h-110.336v-110.336zM883.328 310.997l-255.829-255.829c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-5.205-2.176-10.795-3.243-16.341-3.243h-341.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-512c0-11.776-4.779-22.443-12.501-30.165zM554.667 128v213.333c0 23.552 19.115 42.667 42.667 42.667h213.333v469.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM682.667 512h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM682.667 682.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM426.667 341.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-text" + ], + "grid": 0 + }, + { + "id": 102, + "paths": [ + "M571.008 45.909c-5.205-2.176-10.795-3.243-16.341-3.243h-298.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-469.333c0-11.776-4.779-22.443-12.501-30.165l-298.667-298.667c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088zM750.336 341.333h-153.003v-153.003zM512 128v256c0 23.552 19.115 42.667 42.667 42.667h256v426.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file" + ], + "grid": 0 + }, + { + "id": 103, + "paths": [ + "M682.667 469.333h-341.333v-341.333h341.333zM341.333 554.667h341.333v341.333h-341.333zM256 256h-128v-77.653c0-6.912 1.365-13.355 3.797-19.243 2.517-6.144 6.272-11.691 10.965-16.341s10.24-8.405 16.341-10.965c5.888-2.432 12.331-3.797 19.243-3.797h77.653zM128 341.333h128v128h-128zM256 682.667h-128v-128h128zM128 768h128v128h-77.653c-6.912 0-13.355-1.365-19.243-3.797-6.144-2.517-11.691-6.272-16.341-10.965s-8.405-10.24-10.965-16.341c-2.432-5.888-3.797-12.331-3.797-19.243zM896 682.667h-128v-128h128zM768 768h128v77.653c0 6.912-1.365 13.355-3.797 19.243-2.517 6.144-6.272 11.691-10.965 16.341s-10.24 8.405-16.341 10.965c-5.888 2.432-12.331 3.797-19.243 3.797h-77.653zM896 256h-128v-128h77.653c6.912 0 13.355 1.365 19.243 3.797 6.144 2.517 11.691 6.272 16.341 10.965s8.405 10.24 10.965 16.341c2.432 5.888 3.797 12.331 3.797 19.243zM981.333 298.667v-120.32c0-18.304-3.627-35.84-10.283-51.883-6.912-16.64-16.981-31.573-29.44-44.032s-27.392-22.571-44.032-29.44c-16.085-6.699-33.621-10.325-51.925-10.325h-667.307c-18.304 0-35.84 3.627-51.883 10.283-16.64 6.912-31.573 16.981-44.032 29.44s-22.571 27.392-29.44 44.032c-6.699 16.085-10.325 33.621-10.325 51.925v667.307c0 18.304 3.627 35.84 10.283 51.883 6.912 16.64 16.981 31.573 29.44 44.032s27.392 22.571 44.032 29.44c16.085 6.699 33.621 10.325 51.925 10.325h667.307c18.304 0 35.84-3.627 51.883-10.283 16.64-6.912 31.573-16.981 44.032-29.44s22.571-27.392 29.44-44.032c6.699-16.085 10.325-33.621 10.325-51.925zM768 341.333h128v128h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "film" + ], + "grid": 0 + }, + { + "id": 104, + "paths": [ + "M846.72 170.667l-281.984 333.397c-6.272 7.381-10.069 17.024-10.069 27.563v295.339l-85.333-42.667v-252.672c0.043-9.685-3.285-19.499-10.069-27.563l-281.984-333.397zM938.667 85.333h-853.333c-23.552 0-42.667 19.115-42.667 42.667 0 10.539 3.797 20.181 10.069 27.563l331.264 391.68v263.424c0 16.597 9.472 31.019 23.595 38.144l170.667 85.333c21.077 10.539 46.72 2.005 57.259-19.072 3.072-6.229 4.523-12.843 4.48-19.072v-348.757l331.264-391.68c15.232-18.005 12.971-44.928-5.035-60.117-8.064-6.827-17.877-10.155-27.563-10.112z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "filter" + ], + "grid": 0 + }, + { + "id": 105, + "paths": [ + "M213.333 571.605v-420.651c2.517-1.237 5.376-2.56 8.661-3.925 20.779-8.576 58.411-19.029 119.339-19.029 16.981 0 33.195 1.749 49.109 4.821 10.453 2.005 20.821 4.608 31.232 7.637 7.168 2.091 14.379 4.437 21.589 6.955 17.408 6.016 34.603 12.885 52.907 20.224 12.715 5.077 25.984 10.411 39.509 15.445 5.632 2.091 11.392 4.181 17.195 6.187 8.277 2.859 16.768 5.632 25.472 8.149 12.587 3.669 25.6 6.955 39.125 9.557 20.565 3.968 42.197 6.357 65.195 6.357 55.125 0 96.981-7.381 128-16.939v420.651c-2.517 1.237-5.376 2.56-8.661 3.925-20.779 8.576-58.411 19.029-119.339 19.029-16.981 0-33.195-1.749-49.109-4.821-10.496-2.005-20.864-4.608-31.232-7.637-7.168-2.091-14.379-4.437-21.589-6.955-5.077-1.749-10.155-3.584-15.275-5.504-12.373-4.608-24.704-9.557-37.589-14.72-17.963-7.168-37.035-14.805-56.704-21.632-8.277-2.859-16.768-5.632-25.472-8.149-12.587-3.669-25.6-6.955-39.125-9.557-20.608-3.968-42.24-6.357-65.237-6.357-55.125 0-96.981 7.381-128 16.939zM213.333 938.667v-275.712c2.517-1.237 5.376-2.56 8.661-3.925 20.779-8.576 58.411-19.029 119.339-19.029 16.981 0 33.195 1.749 49.109 4.821 10.496 2.005 20.864 4.608 31.232 7.637 7.168 2.091 14.379 4.437 21.589 6.955 17.408 6.016 34.603 12.885 52.907 20.224 12.715 5.077 25.984 10.411 39.509 15.445 5.632 2.091 11.392 4.181 17.195 6.187 8.277 2.859 16.768 5.632 25.472 8.149 12.587 3.669 25.6 6.955 39.125 9.557 20.565 3.968 42.197 6.357 65.195 6.357 70.827 0 119.765-12.16 151.979-25.515 30.421-12.587 45.099-25.899 48.853-29.653 8.32-8.32 12.501-19.243 12.501-30.165v-512c0-23.552-19.115-42.667-42.667-42.667-11.307 0-21.589 4.395-29.227 11.563-1.109 0.896-7.808 6.144-22.144 12.075-20.736 8.576-58.368 19.029-119.296 19.029-16.981 0-33.195-1.749-49.109-4.821-10.496-2.005-20.864-4.608-31.232-7.637-7.168-2.091-14.379-4.437-21.589-6.955-5.077-1.749-10.155-3.584-15.275-5.504-12.373-4.608-24.704-9.557-37.589-14.72-17.963-7.168-37.035-14.805-56.704-21.632-8.277-2.859-16.768-5.632-25.472-8.149-12.587-3.669-25.6-6.955-39.125-9.557-20.608-3.968-42.24-6.357-65.237-6.357-70.827 0-119.765 12.16-151.979 25.515-30.421 12.587-45.099 25.899-48.853 29.653-8.32 8.32-12.501 19.243-12.501 30.165v810.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flag" + ], + "grid": 0 + }, + { + "id": 106, + "paths": [ + "M981.333 810.667v-469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-361.173l-72.661-109.013c-7.765-11.52-20.736-18.987-35.499-18.987h-213.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM896 810.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h190.507l72.661 109.013c8.192 12.245 21.589 18.901 35.499 18.987h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM384 640h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-minus" + ], + "grid": 0 + }, + { + "id": 107, + "paths": [ + "M981.333 810.667v-469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-361.173l-72.661-109.013c-7.765-11.52-20.736-18.987-35.499-18.987h-213.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM896 810.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h190.507l72.661 109.013c8.192 12.245 21.589 18.901 35.499 18.987h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM384 640h85.333v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-plus" + ], + "grid": 0 + }, + { + "id": 108, + "paths": [ + "M981.333 810.667v-469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-361.173l-72.661-109.013c-7.765-11.52-20.736-18.987-35.499-18.987h-213.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM896 810.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h190.507l72.661 109.013c8.192 12.245 21.589 18.901 35.499 18.987h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder" + ], + "grid": 0 + }, + { + "id": 109, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM716.8 657.067c-1.28-1.749-14.677-19.285-38.741-39.381-14.379-11.989-32.939-25.173-55.339-36.395-16.341-8.149-34.816-15.317-55.296-20.181-17.195-4.053-35.712-6.443-55.424-6.443s-38.229 2.389-55.424 6.443c-20.48 4.821-38.997 11.989-55.296 20.181-22.4 11.221-40.96 24.405-55.339 36.395-24.064 20.096-37.461 37.632-38.741 39.381-14.123 18.859-10.325 45.611 8.533 59.733 18.816 14.080 45.44 10.325 59.605-8.363 0.213-0.256 2.901-3.712 7.851-8.96 4.267-4.48 10.112-10.197 17.408-16.299 10.368-8.661 23.424-17.877 38.827-25.6 11.179-5.589 23.467-10.283 36.779-13.44 11.136-2.603 23.083-4.139 35.797-4.139s24.661 1.536 35.797 4.181c13.312 3.157 25.6 7.851 36.779 13.44 15.36 7.68 28.459 16.939 38.827 25.6 16.341 13.611 25.216 25.131 25.259 25.216 14.165 18.731 40.832 22.485 59.605 8.363 18.859-14.123 22.656-40.875 8.533-59.733z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "frown" + ], + "grid": 0 + }, + { + "id": 110, + "paths": [ + "M469.333 554.667v341.333h-256v-341.333zM554.667 896v-341.333h256v341.333zM320 256c-8.747 0-16.981-1.749-24.448-4.821-7.808-3.243-14.891-7.979-20.821-13.909s-10.667-13.013-13.909-20.779c-3.072-7.509-4.821-15.744-4.821-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.891 13.909-20.821s13.013-10.667 20.779-13.909c7.509-3.072 15.744-4.821 24.491-4.821 12.8 0 24.192 2.133 34.432 5.803 11.861 4.267 22.869 10.752 33.152 19.2 16.043 13.141 29.909 30.635 41.685 50.219 10.283 17.109 18.475 35.029 24.747 51.328zM569.429 256c6.869-17.749 15.019-35.669 25.301-52.779 11.733-19.584 25.643-37.077 41.643-50.219 10.283-8.448 21.291-14.933 33.152-19.157 10.283-3.712 21.675-5.845 34.475-5.845 8.747 0 16.981 1.749 24.448 4.821 7.808 3.243 14.848 7.979 20.779 13.909s10.667 13.013 13.909 20.779c3.115 7.509 4.864 15.744 4.864 24.491s-1.749 16.981-4.821 24.448c-3.243 7.808-7.979 14.848-13.909 20.779s-13.013 10.667-20.779 13.909c-7.509 3.115-15.744 4.864-24.491 4.864zM469.333 341.333v128h-341.333v-128h192zM838.955 256c1.067-2.261 2.091-4.565 3.072-6.869 7.296-17.664 11.307-36.992 11.307-57.131s-4.011-39.467-11.307-57.131c-7.595-18.304-18.688-34.731-32.427-48.469s-30.165-24.832-48.469-32.427c-17.664-7.296-36.992-11.307-57.131-11.307-22.656 0-43.776 3.883-63.147 10.795-22.315 7.979-41.728 19.755-58.539 33.536-25.728 21.077-45.568 47.061-60.715 72.277-3.413 5.675-6.613 11.349-9.6 16.981-2.987-5.632-6.187-11.307-9.6-16.981-15.147-25.216-34.987-51.2-60.715-72.277-16.768-13.781-36.224-25.557-58.539-33.536-19.371-6.912-40.491-10.795-63.147-10.795-20.139 0-39.467 4.011-57.131 11.307-18.304 7.595-34.731 18.688-48.469 32.427s-24.832 30.165-32.427 48.469c-7.296 17.664-11.307 36.992-11.307 57.131s4.011 39.467 11.307 57.131c0.981 2.304 2.005 4.608 3.072 6.869h-99.712c-23.552 0-42.667 19.115-42.667 42.667v213.333c0 23.552 19.115 42.667 42.667 42.667h42.667v384c0 23.552 19.115 42.667 42.667 42.667h682.667c23.552 0 42.667-19.115 42.667-42.667v-384h42.667c23.552 0 42.667-19.115 42.667-42.667v-213.333c0-23.552-19.115-42.667-42.667-42.667zM554.667 341.333h341.333v128h-341.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "gift" + ], + "grid": 0 + }, + { + "id": 111, + "paths": [ + "M853.333 256c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM341.333 768c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM723.371 420.779c-3.499 32.896-11.691 64.384-23.893 93.739-17.28 41.728-42.624 79.317-74.155 110.848s-69.12 56.875-110.848 74.155c-29.397 12.16-60.843 20.352-93.739 23.893-1.92-7.083-4.267-13.952-7.040-20.651-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.459-55.381-37.12c-7.296-3.029-14.848-5.547-22.613-7.552v-474.709c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v474.709c-7.765 2.005-15.317 4.523-22.613 7.552-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c3.243-7.808 5.931-15.915 7.979-24.235 44.032-3.84 86.229-14.421 125.483-30.677 52.224-21.632 99.2-53.333 138.539-92.629s70.997-86.272 92.629-138.539c16.256-39.253 26.837-81.451 30.677-125.483 8.32-2.048 16.427-4.736 24.235-7.979 20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.405-20.267 13.013-42.325 13.013-65.365s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c6.699 2.773 13.568 5.12 20.651 7.040z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "git-branch" + ], + "grid": 0 + }, + { + "id": 112, + "paths": [ + "M640 512c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM725.76 554.667h253.867c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-253.867c-1.579 0-3.115 0.085-4.651 0.256-2.731-13.568-6.741-26.667-11.947-39.168-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.499 43.136-46.293 69.248c-5.163 12.501-9.216 25.6-11.947 39.125-1.365-0.085-2.816-0.171-4.224-0.171h-253.867c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h253.867c1.408 0 2.859-0.085 4.224-0.213 2.731 13.525 6.741 26.624 11.947 39.125 10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c5.205-12.501 9.216-25.643 11.947-39.168 1.536 0.171 3.072 0.256 4.651 0.256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "git-commit" + ], + "grid": 0 + }, + { + "id": 113, + "paths": [ + "M853.333 768c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM341.333 256c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM298.667 896v-255.957c12.117 16.171 25.387 31.403 39.637 45.653 39.296 39.296 86.272 70.997 138.539 92.629 39.253 16.256 81.451 26.837 125.483 30.677 2.048 8.32 4.736 16.427 7.979 24.235 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.181 8.405 42.24 13.013 65.28 13.013s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.373 34.475-37.035 55.381c-2.773 6.699-5.12 13.568-7.040 20.651-32.896-3.499-64.384-11.691-93.739-23.893-41.728-17.28-79.317-42.624-110.848-74.155s-56.875-69.12-74.155-110.848c-12.16-29.397-20.352-60.843-23.893-93.739 7.083-1.92 13.952-4.267 20.651-7.040 20.907-8.661 39.68-21.333 55.381-37.035s28.416-34.432 37.077-55.339c8.32-20.181 12.928-42.24 12.928-65.28s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.517 28.416 55.424 37.077c7.296 3.029 14.848 5.547 22.613 7.552v474.709c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "git-merge" + ], + "grid": 0 + }, + { + "id": 114, + "paths": [ + "M853.333 768c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM341.333 256c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM554.667 298.667h128c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v261.376c-7.765 2.005-15.317 4.523-22.613 7.552-20.907 8.661-39.68 21.333-55.381 37.035s-28.373 34.475-37.035 55.381c-8.363 20.224-12.971 42.283-12.971 65.323s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-7.296-3.029-14.848-5.547-22.613-7.552l-0.043-261.419c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM213.333 421.291v474.709c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-474.709c7.765-2.005 15.317-4.523 22.613-7.552 20.907-8.661 39.68-21.333 55.381-37.035s28.416-34.517 37.077-55.424c8.32-20.181 12.928-42.24 12.928-65.28s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.517 28.416 55.424 37.077c7.296 3.029 14.848 5.547 22.613 7.552z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "git-pull-request" + ], + "grid": 0 + }, + { + "id": 115, + "paths": [ + "M341.333 777.515c-17.067 3.499-31.659 4.821-44.075 4.736-14.336-0.085-25.984-2.133-35.712-5.205-9.856-3.115-18.432-7.509-26.197-12.885-5.077-3.499-9.984-7.552-14.763-12.032-3.328-3.115-6.571-6.443-9.813-9.984-7.851-8.533-15.061-17.707-23.168-27.989-4.437-5.632-9.173-11.648-14.037-17.493-1.963-2.347-3.968-4.736-6.059-7.125-2.859-3.243-5.845-6.528-9.003-9.771-4.267-4.395-8.875-8.747-13.867-12.928-6.869-5.717-14.549-11.136-23.211-15.701-7.893-4.181-16.469-7.552-25.771-9.899-22.869-5.717-46.037 8.192-51.755 31.061s8.192 46.037 31.061 51.755c2.176 0.555 4.352 1.365 6.613 2.56 2.517 1.323 5.291 3.2 8.448 5.803 2.304 1.92 4.736 4.181 7.339 6.869 1.92 1.963 3.883 4.139 5.973 6.485 1.493 1.707 3.029 3.541 4.608 5.419 4.011 4.821 8.064 9.984 12.629 15.787 7.808 9.899 16.939 21.547 27.307 32.811 4.437 4.864 9.173 9.728 14.293 14.549 7.381 6.955 15.531 13.739 24.661 20.011 14.165 9.771 30.336 18.176 49.024 24.064 18.389 5.803 38.571 9.003 60.8 9.173 14.080 0.085 28.971-1.024 44.672-3.413v74.496c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-165.12c0-0.896-0.043-1.92-0.085-2.944-0.256-3.584-0.299-7.168-0.171-10.709 0.427-12.373 3.115-24.619 7.979-36.096 4.693-11.008 11.477-21.419 20.395-30.677 6.187-6.357 10.496-14.805 11.691-24.405 2.901-23.381-13.696-44.715-37.077-47.616-14.507-1.792-28.885-4.011-42.923-6.784-33.707-6.656-64.768-16.427-91.605-31.275-14.891-8.235-28.416-18.005-40.363-29.696-10.624-10.368-20.267-22.443-28.629-36.608-8.875-15.061-16.469-32.768-22.187-53.845-7.339-26.709-11.691-59.051-11.691-98.347-0.128-24.064 4.267-48.171 13.312-70.869 8.661-21.888 21.589-42.539 38.912-60.715 10.965-11.691 14.933-28.672 9.088-44.373-7.68-20.565-11.52-42.539-10.965-64.811 0.299-12.373 1.92-24.832 4.992-37.205 7.467 1.621 16.981 4.395 28.715 8.96 20.949 8.149 48.853 22.101 84.181 45.781 10.027 6.699 22.741 9.131 35.2 5.675 85.205-23.765 180.992-25.685 276.053 0.085 11.563 3.115 24.277 1.408 34.901-5.76 35.371-23.68 63.232-37.632 84.181-45.781 11.733-4.565 21.291-7.296 28.715-8.917 2.987 12.117 4.651 24.576 4.992 37.205 0.512 21.461-2.944 43.392-10.965 64.896-5.504 15.019-2.475 32.213 9.088 44.373 16.469 17.28 29.696 37.675 38.699 60.203 8.747 21.547 13.568 45.269 13.568 70.229 0 39.936-4.48 72.875-11.989 100.139-5.888 21.291-13.611 39.168-22.613 54.272-8.491 14.208-18.219 26.24-28.971 36.565-12.117 11.605-25.728 21.248-40.747 29.355-26.581 14.336-57.301 23.68-90.581 29.867-13.739 2.56-27.776 4.523-41.941 6.101-8.96 0.981-17.835 4.864-24.917 11.733-16.939 16.384-17.408 43.392-1.024 60.331 2.859 2.944 5.547 6.101 8.021 9.344 7.424 9.771 13.056 20.864 16.512 32.725 3.328 11.392 4.736 23.637 3.712 36.309 0 1.024-0.043 2.176-0.128 3.328v165.077c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-163.541c1.579-22.571-0.981-44.544-7.040-65.237-1.579-5.376-3.413-10.667-5.419-15.872 33.152-7.296 66.688-18.219 98.005-35.115 20.992-11.349 41.045-25.387 59.221-42.837 16.213-15.531 30.805-33.579 43.264-54.443 13.227-22.187 23.851-47.189 31.616-75.307 9.899-35.84 15.061-76.587 15.061-122.795 0-36.011-6.955-70.485-19.627-102.101-9.856-24.576-23.125-47.36-39.211-67.797 6.315-23.936 9.045-48.128 8.448-71.979-0.811-33.323-8.064-65.835-20.949-95.829-5.205-12.075-15.317-20.565-27.093-24.064-6.229-1.835-21.803-4.907-47.488-1.024-14.891 2.261-32.939 6.827-54.443 15.189-23.851 9.259-51.968 23.211-84.651 43.904-96.981-23.083-194.432-21.717-283.563-0.085-32.64-20.651-60.715-34.56-84.523-43.819-21.504-8.363-39.552-12.928-54.443-15.189-25.685-3.925-41.259-0.853-47.488 1.024-12.629 3.755-22.229 12.8-27.093 24.107-13.355 31.232-20.139 63.744-20.907 95.872-0.555 24.491 2.347 48.683 8.491 71.936-16.768 21.205-29.952 44.331-39.552 68.565-13.099 33.024-19.499 67.968-19.285 102.827 0 45.056 5.035 85.077 14.635 120.363 7.595 27.861 18.005 52.651 31.019 74.709 12.245 20.779 26.624 38.827 42.624 54.4 17.963 17.493 37.76 31.659 58.539 43.179 32.085 17.749 66.56 29.227 100.565 36.992-7.936 20.011-12.245 41.045-12.971 62.165-0.213 6.016-0.128 12.032 0.213 18.048z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "github" + ], + "grid": 0 + }, + { + "id": 116, + "paths": [ + "M991.488 648.491c11.136-7.253 20.992-19.029 27.093-32.555 3.712-8.32 6.016-17.323 6.656-26.624 0.64-9.045-0.299-18.304-3.499-29.056l-54.016-159.957-103.765-319.317c-2.901-9.771-8.32-18.901-16.939-27.179-5.547-5.035-11.904-8.96-18.603-11.605-6.912-2.731-14.208-4.139-21.504-4.267-7.595-0.128-15.275 1.195-22.571 3.968-7.083 2.688-13.696 6.699-19.072 11.605-7.040 6.272-12.629 14.251-16.128 23.339-0.256 0.64-0.512 1.408-0.768 2.219l-94.592 290.859h-283.605l-94.165-288.981c-2.901-9.771-8.32-18.901-16.939-27.179-5.547-5.077-11.861-8.96-18.603-11.605-6.912-2.731-14.208-4.181-21.504-4.267-7.595-0.128-15.275 1.152-22.571 3.925-7.083 2.688-13.696 6.699-19.072 11.648-7.083 6.229-12.629 14.251-16.171 23.296-0.256 0.683-0.512 1.451-0.768 2.219l-104.149 320.512-52.053 161.28c-4.523 14.037-4.992 28.843-1.621 42.837 2.133 8.789 5.76 17.28 10.923 25.003 4.992 7.509 11.307 14.208 19.029 19.883l454.4 330.24c14.763 10.581 34.901 11.093 50.176 0zM938.795 581.291l-426.795 310.187-425.131-308.992 131.072-404.48 80.683 247.808c5.845 17.92 22.4 29.312 40.576 29.44h345.6c18.859 0 34.859-12.245 40.576-29.483l80.469-247.637 80.725 248.491z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "gitlab" + ], + "grid": 0 + }, + { + "id": 117, + "paths": [ + "M723.243 469.333c-5.461-68.352-21.077-136.32-47.019-201.387-17.621-44.16-39.936-86.912-67.072-127.573 17.067 4.437 33.621 10.027 49.664 16.683 46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 13.525 32.683 22.699 67.627 26.709 104.192zM609.237 883.584c26.197-39.296 48.512-81.536 66.389-126.080 25.344-63.147 41.728-130.987 47.488-202.837h170.539c-4.053 36.565-13.184 71.509-26.709 104.192-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-16 6.613-32.555 12.203-49.579 16.64zM300.757 554.667c5.461 68.352 21.077 136.32 47.019 201.387 17.621 44.16 39.936 86.912 67.072 127.573-17.067-4.437-33.621-10.027-49.664-16.683-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-13.568-32.683-22.741-67.627-26.752-104.192zM414.763 140.416c-26.197 39.296-48.512 81.493-66.389 126.080-25.344 63.147-41.728 130.987-47.531 202.837h-170.496c4.053-36.565 13.184-71.509 26.709-104.192 19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c16-6.613 32.555-12.203 49.579-16.64zM511.872 42.667c-63.445 0-124.075 12.629-179.371 35.541-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.296 22.912 115.883 35.541 179.328 35.541 0.043 0 0.128 0 0.171 0s0.085 0 0.128 0c63.445 0 124.032-12.629 179.371-35.541 57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.997-55.339 35.627-116.011 35.627-179.499s-12.629-124.16-35.541-179.499c-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.339-22.912-115.925-35.541-179.371-35.584-0.043 0-0.128 0-0.171 0s-0.085 0-0.128 0zM637.653 554.667c-5.419 59.52-19.627 117.248-41.216 171.051-21.333 53.163-49.877 102.571-84.395 146.859-35.925-46.165-64.256-96.128-84.992-148.181-21.888-54.869-35.413-112.128-40.661-169.771zM511.957 151.424c35.925 46.165 64.256 96.128 84.992 148.181 21.888 54.869 35.413 112.128 40.661 169.771h-251.264c5.419-59.52 19.627-117.248 41.216-171.051 21.333-53.163 49.877-102.571 84.395-146.859z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "globe" + ], + "grid": 0 + }, + { + "id": 118, + "paths": [ + "M128 85.333c-23.552 0-42.667 19.115-42.667 42.667v298.667c0 23.552 19.115 42.667 42.667 42.667h298.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-23.552-19.115-42.667-42.667-42.667zM170.667 170.667h213.333v213.333h-213.333zM597.333 85.333c-23.552 0-42.667 19.115-42.667 42.667v298.667c0 23.552 19.115 42.667 42.667 42.667h298.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-23.552-19.115-42.667-42.667-42.667zM640 170.667h213.333v213.333h-213.333zM597.333 554.667c-23.552 0-42.667 19.115-42.667 42.667v298.667c0 23.552 19.115 42.667 42.667 42.667h298.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-23.552-19.115-42.667-42.667-42.667zM640 640h213.333v213.333h-213.333zM128 554.667c-23.552 0-42.667 19.115-42.667 42.667v298.667c0 23.552 19.115 42.667 42.667 42.667h298.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-23.552-19.115-42.667-42.667-42.667zM170.667 640h213.333v213.333h-213.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "grid" + ], + "grid": 0 + }, + { + "id": 119, + "paths": [ + "M896 554.667v213.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-213.333zM270.677 237.141c2.816-5.589 6.827-10.496 11.605-14.379 3.755-3.029 7.979-5.419 12.501-7.040 4.352-1.536 9.088-2.389 14.123-2.389h405.888c6.4 0.043 12.587 1.451 18.176 4.011 4.395 2.005 8.405 4.736 11.861 8.064 3.328 3.243 6.187 7.083 8.491 11.733l116.267 232.192h-715.179zM194.389 198.912l-147.2 293.973c-1.195 2.347-2.133 4.779-2.816 7.211-1.195 4.011-1.749 8.021-1.707 11.904v256c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-256c0 0 0 0 0 0 0-6.443-1.408-12.501-3.883-17.792-0.213-0.427-0.427-0.896-0.64-1.323l-0.384-0.768-146.816-293.205c-6.571-13.184-15.232-24.917-25.429-34.816-10.539-10.197-22.656-18.389-35.669-24.363-16.597-7.552-34.688-11.605-53.12-11.733h-406.485c-14.891 0-29.227 2.56-42.624 7.296-13.867 4.907-26.539 12.117-37.717 21.163-14.123 11.435-25.813 25.856-34.176 42.453zM298.667 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM469.333 682.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hard-drive" + ], + "grid": 0 + }, + { + "id": 120, + "paths": [ + "M606.549 426.667l-18.944 170.667h-170.155l18.944-170.667zM640.256 123.307l-24.235 218.027h-170.155l23.168-208.64c2.603-23.424-14.293-44.501-37.675-47.104s-44.501 14.293-47.104 37.717l-24.235 218.027h-189.355c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h179.883l-18.944 170.667h-160.939c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h151.424l-23.168 208.64c-2.603 23.424 14.293 44.501 37.675 47.104s44.501-14.293 47.104-37.675l24.277-218.069h170.155l-23.168 208.64c-2.603 23.424 14.293 44.501 37.675 47.104s44.501-14.293 47.104-37.675l24.235-218.069h189.355c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-179.883l18.944-170.667h160.939c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-151.424l23.168-208.64c2.603-23.424-14.293-44.501-37.675-47.104s-44.501 14.293-47.104 37.675z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hash" + ], + "grid": 0 + }, + { + "id": 121, + "paths": [ + "M853.333 810.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h85.333v128zM170.667 810.667v-170.667h85.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v128c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299zM85.333 810.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h42.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-128c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-85.333v-42.667c0-46.293 9.216-90.368 25.813-130.517 17.28-41.728 42.624-79.317 74.155-110.848s69.12-56.875 110.848-74.155c40.149-16.597 84.224-25.813 130.517-25.813s90.368 9.216 130.517 25.813c41.728 17.28 79.317 42.624 110.848 74.155s56.875 69.12 74.155 110.848c16.597 40.149 25.813 84.224 25.813 130.517v42.667h-85.333c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v128c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h42.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-298.667c0-57.728-11.477-112.853-32.341-163.157-21.632-52.224-53.333-99.2-92.629-138.539-39.296-39.296-86.272-70.997-138.539-92.629-50.304-20.864-105.429-32.341-163.157-32.341s-112.853 11.477-163.157 32.341c-52.267 21.632-99.243 53.333-138.539 92.629s-70.997 86.272-92.629 138.539c-20.864 50.304-32.341 105.429-32.341 163.157v256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "headphones" + ], + "grid": 0 + }, + { + "id": 122, + "paths": [ + "M859.008 226.859c18.517 18.517 32.384 39.723 41.685 62.251 9.728 23.467 14.592 48.512 14.592 73.6s-4.907 50.133-14.592 73.6c-9.344 22.571-23.211 43.733-41.643 62.165l-347.051 347.008-347.008-347.008c-18.432-18.432-32.299-39.637-41.643-62.208-9.728-23.467-14.592-48.512-14.592-73.6s4.864-50.133 14.592-73.6c9.344-22.571 23.211-43.733 41.643-62.208s39.637-32.299 62.208-41.643c23.467-9.728 48.512-14.592 73.6-14.592s50.133 4.864 73.6 14.592c22.571 9.344 43.733 23.211 62.208 41.643l45.227 45.227c16.683 16.683 43.691 16.683 60.331 0l45.312-45.312c18.432-18.432 39.637-32.299 62.208-41.643 23.467-9.728 48.512-14.592 73.6-14.592s50.133 4.907 73.6 14.592c22.571 9.344 43.733 23.211 62.165 41.643zM919.339 166.528c-26.496-26.496-57.131-46.592-89.856-60.16-33.963-14.080-70.101-21.12-106.24-21.12s-72.277 6.997-106.24 21.077c-32.725 13.568-63.36 33.621-89.899 60.117l-15.104 15.147-15.061-15.061c-26.496-26.496-57.131-46.592-89.899-60.16-33.963-14.080-70.144-21.077-106.24-21.077s-72.277 6.997-106.24 21.077c-32.725 13.568-63.36 33.621-89.899 60.16s-46.592 57.131-60.16 89.899c-14.080 33.963-21.077 70.101-21.077 106.24s7.040 72.277 21.077 106.24c13.568 32.725 33.621 63.36 60.16 89.899l377.173 377.173c16.683 16.683 43.691 16.683 60.331 0l377.173-377.173c26.496-26.496 46.592-57.131 60.16-89.856 14.080-33.963 21.12-70.101 21.12-106.24s-6.997-72.277-21.077-106.24c-13.568-32.725-33.621-63.36-60.203-89.941z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "heart" + ], + "grid": 0 + }, + { + "id": 123, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM428.075 398.165c3.883-10.965 9.685-20.565 16.896-28.629 7.509-8.405 16.64-15.147 26.709-20.011s21.035-7.68 32.299-8.32c10.795-0.597 21.931 0.896 32.896 4.736 9.685 3.413 18.261 8.32 25.685 14.379 7.637 6.272 14.080 13.824 19.115 22.272 7.808 13.099 12.16 28.373 12.203 44.245 0 3.115-0.384 6.272-1.109 9.344-0.811 3.456-2.091 6.912-3.883 10.496-3.413 6.827-8.661 13.867-15.701 20.907-8.747 8.747-19.413 16.683-30.635 23.595-9.301 5.76-18.517 10.496-26.453 14.208-5.589 2.603-10.453 4.651-14.123 6.144-2.261 0.896-4.053 1.579-5.248 2.005-24.021 8.064-36.139 32.213-28.672 54.571s31.616 34.432 53.973 26.965c1.067-0.341 13.269-4.48 30.251-12.416 10.24-4.779 22.443-11.093 35.115-18.901 15.019-9.259 31.403-21.163 46.080-35.84 11.904-11.904 23.253-26.197 31.701-43.093 4.48-8.96 8.149-18.645 10.581-29.056 2.219-9.301 3.413-19.029 3.413-29.355-0.171-31.189-8.704-61.312-24.192-87.424-10.027-16.853-22.955-32-38.4-44.672-14.976-12.288-32.256-22.144-51.413-28.885-21.717-7.637-44.075-10.624-65.877-9.472-22.613 1.237-44.544 6.955-64.555 16.555s-38.144 23.168-53.291 40.064c-14.549 16.213-26.197 35.541-33.835 57.259-7.851 22.229 3.84 46.592 26.069 54.4s46.592-3.883 54.4-26.069zM554.667 725.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "help-circle" + ], + "grid": 0 + }, + { + "id": 124, + "paths": [ + "M101.803 350.336c-10.069 7.851-16.469 20.011-16.469 33.664v469.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-469.333c-0.043-12.8-5.717-25.301-16.469-33.664l-384-298.667c-15.275-11.733-36.736-12.16-52.395 0zM682.667 896v-384c0-23.552-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667v384h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-448.469l341.333-265.472 341.333 265.472v448.469c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2zM426.667 896v-341.333h170.667v341.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "home" + ], + "grid": 0 + }, + { + "id": 125, + "paths": [ + "M469.333 362.667c0-14.379-2.859-28.16-8.107-40.789-5.419-13.099-13.355-24.832-23.168-34.603s-21.504-17.749-34.603-23.168c-12.629-5.248-26.411-8.107-40.789-8.107s-28.16 2.859-40.789 8.107c-13.099 5.419-24.832 13.312-34.645 23.125s-17.707 21.547-23.125 34.645c-5.248 12.629-8.107 26.411-8.107 40.789s2.859 28.16 8.107 40.789c5.419 13.099 13.355 24.832 23.168 34.603s21.547 17.707 34.603 23.168c12.629 5.248 26.411 8.107 40.789 8.107s28.16-2.859 40.789-8.107c13.099-5.419 24.832-13.355 34.603-23.168s17.707-21.547 23.168-34.603c5.248-12.629 8.107-26.411 8.107-40.789zM384 362.667c0 2.987-0.597 5.675-1.579 8.149-1.067 2.56-2.688 4.949-4.651 6.955s-4.395 3.584-6.955 4.651c-2.475 0.981-5.163 1.579-8.149 1.579s-5.675-0.597-8.149-1.579c-2.56-1.067-4.949-2.688-6.955-4.651s-3.584-4.395-4.651-6.955c-0.981-2.475-1.579-5.163-1.579-8.149s0.597-5.675 1.579-8.149c1.067-2.56 2.688-4.949 4.651-6.955s4.352-3.584 6.955-4.651c2.475-0.981 5.163-1.579 8.149-1.579s5.675 0.597 8.149 1.579c2.56 1.067 4.949 2.688 6.955 4.651s3.584 4.352 4.651 6.955c0.981 2.475 1.579 5.163 1.579 8.149zM316.331 853.333l366.336-366.336 170.667 170.667v153.003c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2zM213.376 938.667h597.291c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-597.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.896 9.685 0 0 0.043 0 0.043 0zM853.333 537.003l-140.501-140.501c-16.683-16.683-43.691-16.683-60.331 0l-454.144 454.144c-0.427-0.171-0.896-0.341-1.323-0.512-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "image" + ], + "grid": 0 + }, + { + "id": 126, + "paths": [ + "M896 554.667v213.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-213.333h190.507l72.661 109.013c7.765 11.52 20.736 18.987 35.499 18.987h170.667c13.909-0.085 27.307-6.741 35.499-18.987l72.661-109.013zM270.677 237.141c2.816-5.589 6.827-10.496 11.605-14.379 3.755-3.029 7.979-5.419 12.501-7.040 4.352-1.536 9.088-2.389 14.123-2.389h405.888c6.4 0.043 12.587 1.451 18.176 4.011 4.395 2.005 8.405 4.736 11.861 8.064 3.328 3.243 6.187 7.083 8.491 11.733l116.267 232.192h-186.923c-14.763 0-27.733 7.467-35.499 18.987l-72.661 109.013h-125.013l-72.661-109.013c-8.192-12.245-21.589-18.901-35.499-18.987h-186.923zM194.389 198.912l-147.2 293.973c-1.195 2.347-2.133 4.779-2.816 7.211-1.195 4.011-1.749 8.021-1.707 11.904v256c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-256c0 0 0 0 0 0 0-6.443-1.408-12.501-3.883-17.792-0.213-0.427-0.427-0.896-0.64-1.323l-0.384-0.768-146.816-293.205c-6.571-13.184-15.232-24.917-25.429-34.816-10.539-10.197-22.656-18.389-35.669-24.363-16.597-7.552-34.688-11.605-53.12-11.733h-406.485c-14.891 0-29.227 2.56-42.624 7.296-13.867 4.907-26.539 12.117-37.717 21.163-14.123 11.435-25.813 25.856-34.176 42.453z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "inbox" + ], + "grid": 0 + }, + { + "id": 127, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM554.667 682.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM554.667 341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "info" + ], + "grid": 0 + }, + { + "id": 128, + "paths": [ + "M298.667 42.667c-34.603 0-67.712 6.869-97.92 19.413-31.36 12.971-59.52 32-83.115 55.552s-42.581 51.755-55.552 83.115c-12.544 30.208-19.413 63.317-19.413 97.92v426.667c0 34.603 6.869 67.712 19.413 97.92 12.971 31.36 32 59.52 55.595 83.115s51.755 42.581 83.115 55.595c30.165 12.501 63.275 19.371 97.877 19.371h426.667c34.603 0 67.712-6.869 97.92-19.413 31.36-12.971 59.52-32 83.115-55.595s42.581-51.755 55.595-83.115c12.501-30.165 19.371-63.275 19.371-97.877v-426.667c0-34.603-6.869-67.712-19.413-97.92-12.971-31.36-32-59.52-55.595-83.115s-51.755-42.581-83.115-55.595c-30.165-12.501-63.275-19.371-97.877-19.371zM298.667 128h426.667c23.211 0 45.227 4.608 65.237 12.928 20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237v426.667c0 23.211-4.608 45.227-12.928 65.237-8.619 20.864-21.333 39.637-37.077 55.424s-34.56 28.459-55.424 37.077c-20.011 8.32-42.027 12.928-65.237 12.928h-426.667c-23.211 0-45.227-4.608-65.237-12.928-20.864-8.619-39.637-21.333-55.424-37.077s-28.459-34.56-37.077-55.424c-8.32-20.011-12.928-42.027-12.928-65.237v-426.667c0-23.211 4.608-45.227 12.928-65.237 8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928zM724.864 478.848c-6.4-41.472-24.363-79.232-50.944-109.525-16.341-18.645-35.925-34.475-58.112-46.592-21.461-11.733-45.269-19.84-70.272-23.552-19.797-3.157-41.387-3.285-63.019-0.085-28.501 4.224-54.955 13.952-78.336 27.947-24.277 14.549-45.184 33.664-61.696 55.979s-28.757 47.915-35.627 75.349c-6.613 26.453-8.235 54.571-4.011 83.072s13.952 54.955 27.947 78.336c14.549 24.277 33.664 45.184 55.979 61.696s47.872 28.757 75.307 35.669c26.453 6.613 54.571 8.235 83.072 4.011s54.955-13.952 78.336-27.947c24.277-14.549 45.184-33.664 61.696-55.979s28.757-47.872 35.669-75.307c6.613-26.453 8.235-54.571 4.011-83.072zM640.469 491.392c2.56 17.237 1.579 34.048-2.389 49.835-4.096 16.427-11.435 31.744-21.419 45.184s-22.528 24.917-37.035 33.621c-13.952 8.363-29.781 14.208-46.976 16.768s-34.048 1.579-49.835-2.389c-16.427-4.096-31.744-11.435-45.184-21.419s-24.917-22.528-33.621-37.035c-8.363-13.952-14.208-29.781-16.768-46.976s-1.579-34.048 2.389-49.835c4.096-16.427 11.435-31.744 21.419-45.184s22.528-24.917 37.035-33.621c13.952-8.363 29.781-14.208 46.976-16.768 13.355-1.963 26.24-1.792 37.12-0.085 15.787 2.347 30.037 7.253 42.795 14.208 13.227 7.211 24.96 16.683 34.773 27.904 16 18.261 26.88 41.088 30.72 65.835zM789.333 277.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "instagram" + ], + "grid": 0 + }, + { + "id": 129, + "paths": [ + "M578.432 213.333l-224 597.333h-141.099c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h384c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-151.765l224-597.333h141.099c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-384c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "italic" + ], + "grid": 0 + }, + { + "id": 130, + "paths": [ + "M810.667 230.997l67.669 67.669-89.003 89.003-67.669-67.669zM456.021 525.739c18.56 18.304 32.555 39.381 42.027 61.867 9.856 23.381 14.933 48.427 15.104 73.472s-4.523 50.133-14.080 73.685c-9.173 22.613-22.912 43.904-41.216 62.464s-39.424 32.555-61.909 42.069c-23.381 9.856-48.427 14.933-73.472 15.104-25.088 0.171-50.133-4.523-73.685-14.080-22.613-9.173-43.904-22.912-62.464-41.216l-1.792-1.792c-17.835-18.475-31.317-39.893-40.277-62.592-9.301-23.637-13.739-48.768-13.312-73.813s5.76-50.048 15.872-73.301c9.728-22.4 23.936-43.307 42.709-61.44 18.475-17.835 39.509-31.189 61.824-40.107 23.168-9.301 47.829-13.867 72.533-13.739 24.363 0.128 48.64 4.864 71.509 14.165 22.059 8.96 42.795 22.187 60.672 39.339zM865.835 55.168l-382.677 382.677c-17.621-12.459-36.267-22.613-55.68-30.507-33.024-13.44-68.096-20.267-103.168-20.437-35.541-0.213-71.168 6.4-104.747 19.883-32.384 12.971-62.805 32.299-89.344 57.941-26.965 26.027-47.573 56.32-61.696 88.789-14.677 33.707-22.315 69.76-22.912 105.856s5.76 72.363 19.243 106.581c12.971 32.981 32.512 63.915 58.88 91.179l2.645 2.645c26.667 26.325 57.429 46.208 90.283 59.52 34.048 13.824 70.272 20.608 106.368 20.395s72.235-7.509 106.112-21.803c32.64-13.781 63.147-34.048 89.472-60.757s46.208-57.429 59.52-90.283c13.824-34.048 20.608-70.272 20.395-106.368s-7.509-72.235-21.803-106.112c-8.405-19.925-19.243-39.083-32.512-56.96l117.12-117.077 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0l149.333-149.333c16.683-16.683 16.683-43.691 0-60.331l-97.835-97.835 55.168-55.168c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "key" + ], + "grid": 0 + }, + { + "id": 131, + "paths": [ + "M512 133.035l331.264 165.632-331.264 165.632-331.264-165.632zM492.928 47.189l-426.667 213.333c-21.077 10.539-29.611 36.139-19.072 57.216 4.309 8.661 11.136 15.189 19.072 19.072l426.667 213.333c12.459 6.229 26.453 5.803 38.144 0l426.667-213.333c21.077-10.539 29.611-36.181 19.072-57.259-4.309-8.619-11.179-15.147-19.072-19.072l-426.667-213.333c-12.459-6.229-26.453-5.803-38.144 0zM66.261 763.477l426.667 213.333c12.459 6.229 26.453 5.803 38.144 0l426.667-213.333c21.077-10.539 29.611-36.181 19.072-57.259s-36.181-29.611-57.259-19.072l-407.552 203.819-407.595-203.776c-21.077-10.539-46.72-2.005-57.259 19.072s-2.005 46.72 19.072 57.259zM66.261 550.144l426.667 213.333c12.459 6.229 26.453 5.803 38.144 0l426.667-213.333c21.077-10.539 29.611-36.181 19.072-57.259s-36.181-29.611-57.259-19.072l-407.552 203.819-407.595-203.776c-21.077-10.539-46.72-2.005-57.259 19.072s-2.005 46.72 19.072 57.259z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "layers" + ], + "grid": 0 + }, + { + "id": 132, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM853.333 341.333h-682.667v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM341.333 426.667v426.667h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-384zM426.667 853.333v-426.667h426.667v384c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "layout" + ], + "grid": 0 + }, + { + "id": 133, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM640 512c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM629.632 333.995c-11.307-7.467-23.381-13.909-36.053-19.157-25.173-10.453-52.779-16.171-81.579-16.171s-56.405 5.717-81.579 16.171c-12.715 5.248-24.747 11.691-36.053 19.157l-122.027-122.027c28.032-22.443 59.264-41.003 92.843-54.912 45.141-18.688 94.72-29.056 146.816-29.056s101.675 10.368 146.859 29.056c33.621 13.909 64.811 32.469 92.843 54.912zM690.005 394.368l122.027-122.027c22.443 28.032 41.003 59.264 54.912 92.843 18.688 45.141 29.056 94.72 29.056 146.816s-10.368 101.675-29.056 146.859c-13.909 33.621-32.469 64.811-54.912 92.843l-122.027-122.027c7.467-11.307 13.909-23.381 19.157-36.053 10.453-25.216 16.171-52.821 16.171-81.621s-5.717-56.405-16.171-81.579c-5.248-12.672-11.691-24.747-19.157-36.053zM333.995 629.632l-122.027 122.027c-22.443-28.032-41.003-59.264-54.912-92.843-18.688-45.141-29.056-94.72-29.056-146.816s10.368-101.675 29.056-146.859c13.909-33.621 32.469-64.811 54.912-92.843l122.027 122.027c-7.467 11.349-13.909 23.381-19.157 36.096-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c5.248 12.672 11.691 24.747 19.157 36.053zM272.299 812.032l122.027-122.027c11.307 7.467 23.381 13.909 36.053 19.157 25.216 10.453 52.821 16.171 81.621 16.171s56.405-5.717 81.579-16.171c12.672-5.248 24.747-11.691 36.053-19.157l122.027 122.027c-28.032 22.443-59.264 41.003-92.843 54.912-45.141 18.688-94.72 29.056-146.816 29.056s-101.675-10.368-146.859-29.056c-33.621-13.909-64.811-32.469-92.843-54.912z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "life-buoy" + ], + "grid": 0 + }, + { + "id": 134, + "paths": [ + "M640 341.333h128c23.211 0 45.227 4.608 65.237 12.928 20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237s-4.608 45.227-12.928 65.237c-8.619 20.864-21.333 39.637-37.077 55.424s-34.56 28.459-55.424 37.077c-20.011 8.32-42.027 12.928-65.237 12.928h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h128c34.603 0 67.712-6.869 97.92-19.413 31.36-12.971 59.52-32 83.115-55.595s42.581-51.755 55.595-83.115c12.501-30.165 19.371-63.275 19.371-97.877s-6.869-67.712-19.413-97.92c-12.971-31.36-32-59.52-55.595-83.115s-51.755-42.581-83.115-55.595c-30.165-12.501-63.275-19.371-97.877-19.371h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM384 682.667h-128c-23.211 0-45.227-4.608-65.237-12.928-20.864-8.619-39.637-21.333-55.424-37.077s-28.459-34.56-37.077-55.424c-8.32-20.011-12.928-42.027-12.928-65.237s4.608-45.227 12.928-65.237c8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128c-34.603 0-67.712 6.869-97.92 19.413-31.36 12.971-59.52 32-83.115 55.552s-42.581 51.755-55.552 83.115c-12.544 30.208-19.413 63.317-19.413 97.92s6.869 67.712 19.413 97.92c12.971 31.36 32 59.52 55.595 83.115s51.755 42.581 83.115 55.595c30.165 12.501 63.275 19.371 97.877 19.371h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM341.333 554.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "link-2" + ], + "grid": 0 + }, + { + "id": 135, + "paths": [ + "M392.491 580.224c20.736 27.691 46.080 50.091 74.197 66.773 29.184 17.323 61.312 28.501 94.293 33.28s66.944 3.115 99.84-5.291c31.701-8.064 62.336-22.4 90.027-43.093 10.197-7.637 19.84-16 27.947-24.235l127.787-127.744c24.533-25.429 42.581-54.016 54.571-84.437 12.459-31.573 18.347-65.067 17.749-98.389s-7.637-66.603-21.163-97.707c-13.056-29.995-32.043-57.941-56.96-81.963-24.491-23.637-52.565-41.515-82.475-53.504-31.019-12.416-63.872-18.517-96.683-18.347-32.384 0.171-64.725 6.485-95.232 18.859-29.483 11.989-57.131 29.653-81.28 52.907l-73.856 73.429c-16.725 16.597-16.811 43.648-0.171 60.331s43.648 16.811 60.331 0.171l72.917-72.491c16.171-15.616 34.603-27.349 54.229-35.328 20.309-8.277 41.899-12.459 63.573-12.587 21.931-0.128 43.861 3.968 64.469 12.203 19.84 7.936 38.528 19.797 54.955 35.669 16.683 16.128 29.312 34.731 37.973 54.613 9.003 20.693 13.739 42.88 14.123 65.195s-3.541 44.629-11.819 65.621c-7.936 20.181-19.925 39.211-35.541 55.381l-128.213 128.256c-4.864 4.949-11.221 10.539-18.261 15.787-18.56 13.909-38.955 23.381-59.989 28.757-21.888 5.589-44.501 6.699-66.603 3.499s-43.477-10.667-62.891-22.187c-18.645-11.093-35.541-25.941-49.408-44.501-14.123-18.859-40.832-22.741-59.733-8.619s-22.741 40.832-8.619 59.733zM631.509 443.776c-20.736-27.691-46.080-50.091-74.197-66.773-29.184-17.323-61.312-28.501-94.293-33.28s-66.944-3.115-99.84 5.291c-31.701 8.064-62.336 22.4-90.027 43.093-10.197 7.637-19.84 16-27.947 24.235l-127.787 127.744c-24.533 25.429-42.581 54.016-54.571 84.437-12.459 31.573-18.347 65.067-17.749 98.389s7.637 66.603 21.163 97.707c13.056 29.995 32.043 57.941 56.96 81.963 24.491 23.637 52.565 41.515 82.475 53.504 31.019 12.416 63.872 18.517 96.683 18.347 32.384-0.171 64.725-6.485 95.232-18.859 29.483-11.989 57.131-29.653 81.28-52.907l73.515-73.515c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-72.405 72.405c-16.171 15.616-34.603 27.349-54.229 35.328-20.309 8.235-41.899 12.459-63.573 12.587-21.931 0.128-43.861-3.968-64.469-12.203-19.84-7.936-38.528-19.797-54.955-35.669-16.683-16.128-29.312-34.731-37.973-54.613-9.003-20.693-13.739-42.88-14.123-65.195s3.541-44.629 11.819-65.621c7.936-20.181 19.925-39.211 35.541-55.381l128.213-128.256c4.864-4.949 11.221-10.539 18.261-15.787 18.56-13.909 38.955-23.381 59.989-28.757 21.888-5.589 44.501-6.699 66.603-3.499s43.477 10.667 62.891 22.187c18.645 11.093 35.541 25.941 49.408 44.501 14.123 18.859 40.832 22.741 59.733 8.619s22.741-40.832 8.619-59.733z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "link" + ], + "grid": 0 + }, + { + "id": 136, + "paths": [ + "M682.667 298.667c-40.363 0-78.976 8.021-114.219 22.613-36.565 15.147-69.461 37.333-96.981 64.853s-49.707 60.373-64.853 96.981c-14.592 35.243-22.613 73.856-22.613 114.219v298.667c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2s11.349 1.152 16.299 3.2c5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v298.667c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-298.667c0-40.363-8.021-78.976-22.613-114.219-15.147-36.565-37.333-69.461-64.853-96.981s-60.373-49.707-96.981-64.853c-35.243-14.592-73.856-22.613-114.219-22.613zM682.667 384c28.971 0 56.491 5.76 81.579 16.128 26.069 10.795 49.579 26.624 69.291 46.336s35.541 43.221 46.336 69.291c10.368 25.088 16.128 52.608 16.128 81.579v256h-85.333v-256c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685s-33.835 3.456-48.981 9.728c-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v256h-85.333v-256c0-28.971 5.76-56.491 16.128-81.579 10.795-26.069 26.624-49.579 46.336-69.291s43.221-35.541 69.291-46.336c25.088-10.368 52.608-16.128 81.579-16.128zM85.333 341.333c-23.552 0-42.667 19.115-42.667 42.667v512c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-512c0-23.552-19.115-42.667-42.667-42.667zM128 426.667h85.333v426.667h-85.333zM298.667 170.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.659-16-29.739-27.776-41.515s-25.856-21.291-41.515-27.776c-15.147-6.272-31.701-9.728-48.981-9.728s-33.835 3.456-48.981 9.728c-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981s3.456 33.835 9.728 48.981c6.485 15.659 16 29.739 27.776 41.515s25.856 21.291 41.515 27.776c15.147 6.272 31.701 9.728 48.981 9.728s33.835-3.456 48.981-9.728c15.659-6.485 29.739-16 41.515-27.776s21.291-25.856 27.776-41.515c6.272-15.147 9.728-31.701 9.728-48.981zM213.333 170.667c0 5.845-1.152 11.349-3.2 16.299-2.176 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2s-11.349-1.152-16.299-3.2c-5.205-2.176-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299s1.152-11.349 3.2-16.299c2.176-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2s11.349 1.152 16.299 3.2c5.205 2.176 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "linkedin" + ], + "grid": 0 + }, + { + "id": 137, + "paths": [ + "M341.333 298.667h554.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-554.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM341.333 554.667h554.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-554.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM341.333 810.667h554.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-554.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM170.667 256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM170.667 512c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM170.667 768c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "list" + ], + "grid": 0 + }, + { + "id": 138, + "paths": [ + "M469.333 85.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 768v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM180.181 240.512l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM662.741 723.072l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM85.333 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM768 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM240.512 843.819l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM723.072 361.259l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "loader" + ], + "grid": 0 + }, + { + "id": 139, + "paths": [ + "M213.333 512h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v298.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-298.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM768 426.667v-128c0-34.603-6.869-67.712-19.413-97.92-12.971-31.36-32-59.52-55.595-83.115s-51.755-42.581-83.115-55.595c-30.165-12.501-63.275-19.371-97.877-19.371s-67.712 6.869-97.92 19.413c-31.36 12.971-59.52 32-83.115 55.552s-42.581 51.755-55.552 83.115c-12.544 30.208-19.413 63.317-19.413 97.92v128h-42.667c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v298.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-298.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM341.333 426.667v-128c0-23.211 4.608-45.227 12.928-65.237 8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928s45.227 4.608 65.237 12.928c20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lock" + ], + "grid": 0 + }, + { + "id": 140, + "paths": [ + "M640 170.667h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h170.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM537.003 469.333h-409.003c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h409.003l-140.501 140.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l213.333-213.333c0.085-0.085 0.171-0.171 0.256-0.256 3.968-4.053 6.955-8.661 9.003-13.568 2.133-5.12 3.2-10.624 3.243-16.085 0-0.171 0-0.341 0-0.469-0.043-5.461-1.109-10.965-3.243-16.085-2.048-4.949-5.035-9.557-9.003-13.568-0.085-0.085-0.171-0.171-0.256-0.256l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "log-in" + ], + "grid": 0 + }, + { + "id": 141, + "paths": [ + "M384 853.333h-170.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM793.003 469.333h-409.003c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h409.003l-140.501 140.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l213.333-213.333c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "log-out" + ], + "grid": 0 + }, + { + "id": 142, + "paths": [ + "M128 337.963l359.552 251.691c14.507 10.027 33.92 10.496 48.939 0l359.509-251.691v430.037c0 5.717-1.152 11.136-3.2 16.085-2.176 5.205-5.376 9.984-9.387 13.995s-8.789 7.211-13.995 9.387c-4.949 2.048-10.368 3.2-16.085 3.2h-682.667c-5.717 0-11.136-1.152-16.085-3.2-5.205-2.176-9.984-5.376-13.995-9.387s-7.211-8.789-9.387-13.995c-2.048-4.949-3.2-10.368-3.2-16.085zM42.667 256.512v511.488c0 17.195 3.456 33.707 9.685 48.768 6.528 15.744 16.085 29.867 27.861 41.685s25.941 21.376 41.685 27.861c15.061 6.229 31.573 9.685 48.768 9.685h682.667c17.195 0 33.707-3.456 48.768-9.685 15.744-6.528 29.867-16.085 41.685-27.861s21.376-25.941 27.861-41.685c6.229-15.061 9.685-31.573 9.685-48.768v-512c0-0.256 0-0.512 0-0.725-0.085-16.939-3.541-33.152-9.685-48-6.528-15.744-16.085-29.867-27.861-41.685s-25.941-21.376-41.685-27.861c-15.061-6.272-31.573-9.728-48.768-9.728h-682.667c-17.195 0-33.707 3.456-48.768 9.685-15.744 6.528-29.867 16.085-41.685 27.861s-21.333 25.941-27.861 41.685c-6.144 14.848-9.6 31.104-9.685 48.043 0 0.213 0 0.469 0 0.725zM891.477 236.971l-379.477 265.6-379.477-265.6c2.048-4.053 4.779-7.808 8.021-11.051 4.053-4.053 8.789-7.253 14.037-9.387 4.949-2.048 10.368-3.2 16.085-3.2h682.667c5.717 0 11.136 1.152 16.085 3.2 5.205 2.176 9.984 5.376 13.995 9.387 3.243 3.243 5.973 6.997 8.021 11.051z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mail" + ], + "grid": 0 + }, + { + "id": 143, + "paths": [ + "M938.667 426.667c0-57.728-11.477-112.853-32.341-163.157-21.632-52.224-53.333-99.2-92.629-138.539-39.296-39.296-86.272-70.997-138.539-92.629-50.304-20.864-105.429-32.341-163.157-32.341s-112.853 11.477-163.157 32.341c-52.267 21.632-99.243 53.333-138.539 92.629s-70.997 86.272-92.629 138.539c-20.864 50.304-32.341 105.429-32.341 163.157 0 24.277 2.261 48.128 6.4 71.509 11.691 66.048 38.357 128.171 71.765 184.32 36.651 61.568 82.048 117.077 126.677 164.053 37.419 39.381 74.667 73.173 106.496 99.968 22.528 18.944 42.411 34.475 57.856 46.037 9.472 7.125 17.323 12.757 23.040 16.811 6.699 4.736 10.795 7.467 10.795 7.467 14.123 9.259 32.64 9.771 47.317 0 0 0 4.139-2.773 10.795-7.467 5.717-4.053 13.525-9.685 23.040-16.811 15.445-11.563 35.328-27.093 57.856-46.037 31.829-26.795 69.077-60.587 106.496-99.968 44.629-46.976 90.027-102.485 126.677-164.053 33.408-56.149 60.075-118.272 71.765-184.32 4.096-23.381 6.357-47.232 6.357-71.509zM853.333 426.667c0 18.944-1.749 37.845-5.077 56.661-9.429 53.333-31.445 105.728-61.099 155.563-32.512 54.613-73.557 105.088-115.2 148.907-34.773 36.608-69.632 68.224-99.584 93.44-21.163 17.792-39.808 32.341-54.101 43.093-2.219 1.664-4.309 3.2-6.272 4.693-2.005-1.451-4.096-3.029-6.272-4.693-14.336-10.752-32.981-25.301-54.101-43.093-29.952-25.216-64.768-56.832-99.584-93.44-41.643-43.819-82.688-94.293-115.2-148.907-29.653-49.835-51.669-102.229-61.099-155.563-3.328-18.816-5.077-37.717-5.077-56.661 0-46.293 9.216-90.368 25.813-130.517 17.323-41.728 42.667-79.36 74.155-110.848s69.12-56.875 110.848-74.155c40.149-16.597 84.224-25.813 130.517-25.813s90.368 9.216 130.517 25.813c41.728 17.28 79.317 42.624 110.848 74.155s56.875 69.12 74.155 110.848c16.597 40.149 25.813 84.224 25.813 130.517zM682.667 426.667c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM597.333 426.667c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "map-pin" + ], + "grid": 0 + }, + { + "id": 144, + "paths": [ + "M298.667 158.848v584.405l-213.333 121.899v-584.405zM725.333 865.152v-584.405l213.333-121.899v584.405zM682.667 981.333c7.381 0 14.677-1.963 21.163-5.632l0.64-0.384 298.027-170.283c13.696-7.808 21.419-22.101 21.504-37.035v-682.667c0-23.552-19.115-42.667-42.667-42.667-7.808 0-15.104 2.091-21.163 5.632l-278.827 159.317-320.896-160.469c-3.413-1.707-6.912-2.901-10.496-3.627-2.859-0.555-5.76-0.853-8.619-0.853-7.424 0-14.72 1.963-21.163 5.632l-0.64 0.341-298.027 170.325c-13.696 7.808-21.419 22.101-21.504 37.035v682.667c0 23.552 19.115 42.667 42.667 42.667 7.808 0 15.104-2.091 21.163-5.632l278.827-159.317 320.896 160.469c3.413 1.707 6.912 2.901 10.496 3.627 2.901 0.555 5.76 0.853 8.619 0.853zM640 282.368v587.264l-256-128v-587.264z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "map" + ], + "grid": 0 + }, + { + "id": 145, + "paths": [ + "M793.003 170.667l-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l225.835-225.835v153.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-0.128 0-0.213 0-0.341-0.043-5.632-1.195-11.008-3.2-15.957-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM230.997 853.333l225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-225.835 225.835v-153.003c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 0.128 0 0.256 0 0.384 0.043 5.632 1.195 11.008 3.2 15.915 4.309 10.453 12.672 18.816 23.168 23.168 4.907 2.005 10.325 3.157 15.957 3.2 0.128 0 0.213 0 0.341 0h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "maximize-2" + ], + "grid": 0 + }, + { + "id": 146, + "paths": [ + "M341.333 85.333h-128c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM938.667 341.333v-128c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h128c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM682.667 938.667h128c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v128c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM85.333 682.667v128c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "maximize" + ], + "grid": 0 + }, + { + "id": 147, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM341.333 682.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "meh" + ], + "grid": 0 + }, + { + "id": 148, + "paths": [ + "M128 554.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM128 298.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM128 810.667h768c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-768c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "menu" + ], + "grid": 0 + }, + { + "id": 149, + "paths": [ + "M938.667 490.539v-21.205c0-0.725-0.043-1.621-0.085-2.475-2.859-49.152-14.464-96.128-33.323-139.264-19.371-44.288-46.379-84.523-79.445-118.912-34.773-36.181-76.288-65.92-122.667-87.211-44.8-20.565-94.123-33.152-146.133-36.053-0.683-0.043-1.536-0.085-2.347-0.085h-20.864c-59.947-0.683-122.965 13.227-181.931 43.008-52.181 26.539-97.749 63.531-133.931 108.203-28.629 35.371-51.371 75.605-66.859 119.253-14.976 42.283-23.083 87.68-23.083 134.4-0.597 54.4 10.795 111.36 35.157 165.419l-75.605 226.859c-2.816 8.363-3.072 17.835 0 26.965 7.467 22.357 31.616 34.432 53.973 26.965l226.731-75.563c49.493 22.485 105.984 35.243 165.376 35.115 58.539-0.384 115.84-13.141 168.149-36.949 41.429-18.859 79.744-44.672 113.067-76.8 32.299-31.147 59.819-68.139 80.64-109.867 27.477-53.291 43.307-115.84 43.136-181.803zM853.333 490.795c0.128 52.267-12.459 101.333-33.664 142.464-16.981 33.92-38.699 63.061-64.043 87.552-26.24 25.301-56.448 45.653-89.173 60.587-41.387 18.859-86.869 28.971-133.376 29.312-52.096 0.128-101.163-12.459-142.293-33.664-10.624-5.504-22.528-6.059-33.067-2.56l-162.261 54.101 54.101-162.261c3.755-11.221 2.56-22.912-2.389-32.725-23.595-46.72-34.347-96.213-33.835-142.464 0-37.845 6.443-73.643 18.176-106.837 12.203-34.347 30.123-66.091 52.736-94.080 28.629-35.328 64.768-64.725 106.283-85.76 46.592-23.552 96.085-34.304 142.336-33.792h20.608c40.789 2.389 79.232 12.331 114.133 28.331 36.523 16.768 69.248 40.235 96.725 68.779 26.155 27.179 47.488 58.965 62.763 93.952 14.72 33.707 23.851 70.357 26.24 108.885z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "message-circle" + ], + "grid": 0 + }, + { + "id": 150, + "paths": [ + "M938.667 640v-426.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-597.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 10.923 4.181 21.845 12.501 30.165 16.683 16.683 43.691 16.683 60.331 0l158.165-158.165h494.336c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM853.333 640c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-11.776 0-22.443 4.779-30.165 12.501l-97.835 97.835v-579.669c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "message-square" + ], + "grid": 0 + }, + { + "id": 151, + "paths": [ + "M534.016 594.347c-6.955 1.835-14.336 2.859-21.931 2.859-11.648 0-22.613-2.304-32.64-6.4-10.411-4.309-19.84-10.624-27.733-18.517s-14.251-17.28-18.56-27.691c-4.139-10.027-6.485-20.992-6.485-32.597v-25.003zM682.667 398.507v-227.84c0.043-22.997-4.523-45.056-12.885-65.237-8.661-20.907-21.291-39.723-36.992-55.424s-34.475-28.416-55.381-37.12c-20.139-8.363-42.24-12.971-65.28-13.013-21.205 0-41.6 3.84-60.459 10.965-19.499 7.381-37.248 18.219-52.565 31.787-13.696 12.117-25.429 26.411-34.688 42.411-9.088 15.701-15.829 32.981-19.627 51.328-4.821 23.083 9.984 45.653 33.067 50.475s45.653-9.984 50.475-33.067c1.963-9.301 5.333-18.048 9.899-25.941 4.651-8.064 10.581-15.232 17.451-21.333 7.68-6.784 16.555-12.203 26.197-15.872 9.301-3.541 19.456-5.461 30.165-5.461 11.648 0 22.613 2.347 32.597 6.485 10.411 4.309 19.797 10.667 27.691 18.56s14.208 17.323 18.517 27.733c4.181 10.069 6.485 21.035 6.485 32.725v227.84c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM768 426.667v85.333c0 16.043-1.493 31.573-3.968 44.587-4.352 23.168 10.88 45.483 34.048 49.835s45.483-10.88 49.835-34.048c3.499-18.56 5.419-39.339 5.419-60.373v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM341.333 1024h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128v-85.803c31.189-3.925 61.952-12.117 91.307-24.661 26.965-11.52 52.736-26.624 76.587-45.312l228.608 228.608c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-249.088-249.088c-1.92-3.371-4.309-6.528-7.211-9.344-2.688-2.645-5.632-4.821-8.747-6.613l-673.621-673.621c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331l328.832 328.832v110.336c0 23.083 4.608 45.141 13.013 65.323 8.661 20.907 21.376 39.68 37.077 55.381s34.517 28.331 55.424 36.992c20.181 8.32 42.24 12.885 65.28 12.885 22.997 0 45.013-4.608 65.152-12.971 7.381-3.072 14.507-6.613 21.333-10.624l63.019 63.019c-15.531 11.221-32 20.437-49.109 27.733-28.075 11.947-57.899 18.688-87.979 20.181-3.968-1.28-8.192-1.92-12.544-1.92s-8.576 0.64-12.544 1.877c-28.331-1.365-56.448-7.424-83.115-18.176-30.208-12.16-58.667-30.336-83.499-54.656-24.192-23.68-42.581-50.901-55.339-80-13.269-30.251-20.395-62.592-21.376-95.189-0.128-3.157-0.171-6.357-0.128-9.557v-85.632c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.035c-0.043 4.267 0 8.491 0.171 12.757 1.323 43.307 10.795 86.443 28.501 126.848 17.067 38.912 41.685 75.264 73.771 106.709 32.981 32.299 70.912 56.619 111.36 72.875 27.435 11.051 55.979 18.347 84.864 21.973v85.803h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mic-off" + ], + "grid": 0 + }, + { + "id": 152, + "paths": [ + "M512 85.333c11.648 0 22.613 2.304 32.64 6.443 10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597v341.333c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597v-341.333c0-11.648 2.304-22.613 6.443-32.64 4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4zM512 0c-23.040 0-45.099 4.608-65.28 12.928-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28v341.333c0 23.040 4.608 45.099 12.928 65.28 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323v-341.333c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928zM341.333 1024h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128v-87.979c30.72-3.84 60.203-11.776 87.893-23.211 41.813-17.323 79.36-42.667 110.805-74.112s56.789-69.035 74.112-110.805c16.683-40.277 25.856-84.395 25.856-130.56v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333c0 34.773-6.912 67.797-19.371 97.877-12.971 31.275-31.957 59.477-55.595 83.115s-51.84 42.667-83.115 55.595c-30.123 12.501-63.147 19.413-97.92 19.413s-67.797-6.912-97.877-19.371c-31.275-12.971-59.477-31.957-83.115-55.595s-42.667-51.84-55.595-83.115c-12.501-30.123-19.413-63.147-19.413-97.92v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333c0 46.165 9.173 90.283 25.856 130.56 17.323 41.813 42.667 79.36 74.112 110.805s69.035 56.789 110.805 74.112c27.691 11.477 57.173 19.371 87.893 23.211v87.979h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mic" + ], + "grid": 0 + }, + { + "id": 153, + "paths": [ + "M700.331 384l225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-225.835 225.835v-153.003c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 5.803 1.152 11.307 3.243 16.341 2.133 5.077 5.205 9.685 9.003 13.568 0.171 0.171 0.341 0.341 0.512 0.512 3.883 3.797 8.448 6.869 13.568 9.003 5.035 2.091 10.539 3.243 16.341 3.243h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM158.165 926.165l225.835-225.835v153.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-5.803-1.152-11.307-3.243-16.341-2.133-5.077-5.205-9.685-9.003-13.568-0.171-0.171-0.341-0.341-0.512-0.512-3.883-3.797-8.448-6.869-13.568-9.003-5.035-2.091-10.539-3.243-16.341-3.243h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h153.003l-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minimize-2" + ], + "grid": 0 + }, + { + "id": 154, + "paths": [ + "M298.667 128v128c0 5.845-1.152 11.349-3.2 16.299-2.176 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h128c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.248-25.856 27.733-41.515c6.272-15.147 9.728-31.701 9.728-48.981v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM896 298.667h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v128c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM725.333 896v-128c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM128 725.333h128c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minimize" + ], + "grid": 0 + }, + { + "id": 155, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM341.333 554.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minus-circle" + ], + "grid": 0 + }, + { + "id": 156, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM213.333 170.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM341.333 554.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minus-square" + ], + "grid": 0 + }, + { + "id": 157, + "paths": [ + "M213.333 554.667h597.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-597.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minus" + ], + "grid": 0 + }, + { + "id": 158, + "paths": [ + "M512 682.667h-341.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-426.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v426.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2zM469.333 768v85.333h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128v-85.333h298.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-426.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-682.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v426.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "monitor" + ], + "grid": 0 + }, + { + "id": 159, + "paths": [ + "M938.496 549.632c0.939-9.941-1.621-20.309-8.021-29.056-13.867-19.029-40.576-23.211-59.605-9.344-40.533 29.568-87.851 46.336-136.021 49.579-29.696 2.005-59.691-1.152-88.619-9.643-27.989-8.192-55.040-21.376-79.957-39.765-27.947-20.651-50.389-45.824-67.2-73.771-17.451-29.013-28.843-61.099-33.792-94.123s-3.499-67.029 4.693-99.925c7.851-31.616 21.931-62.293 42.624-90.24 6.059-8.149 9.216-18.56 8.149-29.483-2.261-23.467-23.125-40.619-46.592-38.315-47.659 4.651-93.269 17.024-135.339 35.968-42.88 19.285-82.133 45.44-116.181 77.141-37.675 34.987-69.035 76.843-92.117 123.776-22.315 45.397-36.821 95.488-41.728 148.693-5.333 57.472 1.024 113.408 17.152 165.461 16.725 54.016 43.989 103.723 79.488 146.475 35.541 42.752 79.36 78.635 129.408 105.003 48.213 25.387 102.059 41.899 159.531 47.189s113.451-1.067 165.461-17.195c54.016-16.725 103.723-43.989 146.475-79.488s78.635-79.36 105.003-129.408c25.387-48.213 41.899-102.059 47.189-159.531zM834.859 626.091c-5.376 14.976-11.776 29.397-19.072 43.264-21.035 39.979-49.749 75.093-84.011 103.552s-74.027 50.261-117.205 63.616c-41.515 12.885-86.229 17.963-132.352 13.696s-89.131-17.493-127.573-37.717c-39.979-21.035-75.093-49.749-103.552-84.011s-50.261-74.027-63.616-117.205c-12.885-41.515-17.963-86.229-13.696-132.352 3.925-42.667 15.573-82.688 33.365-118.869 18.347-37.461 43.435-70.869 73.6-98.944 27.307-25.387 58.752-46.336 93.141-61.824 7.637-3.456 15.445-6.613 23.381-9.515-3.925 10.965-7.296 22.016-10.069 33.195-10.923 43.904-12.885 89.216-6.272 133.163s21.76 86.699 45.056 125.483c22.485 37.333 52.48 70.955 89.6 98.389 33.067 24.448 69.205 42.069 106.667 53.035 38.699 11.349 78.763 15.531 118.272 12.885 32.085-2.133 63.829-8.832 94.293-19.84z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "moon" + ], + "grid": 0 + }, + { + "id": 160, + "paths": [ + "M597.333 512c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM896 512c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM298.667 512c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "more-horizontal" + ], + "grid": 0 + }, + { + "id": 161, + "paths": [ + "M597.333 512c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM597.333 213.333c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM597.333 810.667c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "more-vertical" + ], + "grid": 0 + }, + { + "id": 162, + "paths": [ + "M207.232 207.232l524.117 218.368-208.341 70.741c-12.16 4.181-22.272 13.653-26.667 26.667l-70.741 208.341zM555.093 615.424l225.408 225.408c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-225.408-225.408 250.368-85.035c22.315-7.595 34.261-31.829 26.667-54.101-4.096-12.075-13.056-21.077-23.979-25.643l-724.053-301.653c-21.76-9.045-46.72 1.237-55.808 22.997-4.565 10.923-4.224 22.699 0 32.811l301.653 724.053c9.045 21.76 34.048 32.043 55.808 22.997 11.733-4.907 20.139-14.421 23.979-25.643z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mouse-pointer" + ], + "grid": 0 + }, + { + "id": 163, + "paths": [ + "M469.333 188.331v281.003h-281.003l55.168-55.168c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-128 128c-0.043 0.043-0.085 0.128-0.171 0.171-4.011 4.053-7.040 8.704-9.088 13.653-4.309 10.453-4.309 22.229 0 32.683 2.048 4.949 5.077 9.643 9.088 13.653 0.043 0.043 0.085 0.128 0.171 0.171l128 128c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-55.168-55.168h281.003v281.003l-55.168-55.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l128 128c4.096 4.096 8.832 7.168 13.867 9.259 5.12 2.091 10.539 3.2 15.957 3.243 5.675 0.043 11.349-1.024 16.64-3.243 5.035-2.091 9.771-5.163 13.867-9.259l128-128c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-55.168 55.168v-281.003h281.003l-55.168 55.168c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l128-128c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-128-128c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l55.168 55.168h-281.003v-281.003l55.168 55.168c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-128-128c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.643 5.077-13.653 9.088-0.043 0.043-0.128 0.085-0.171 0.171l-128 128c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "move" + ], + "grid": 0 + }, + { + "id": 164, + "paths": [ + "M341.333 768c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM938.667 682.667v-554.667c0-2.133-0.171-4.565-0.597-6.997-3.883-23.253-25.856-38.955-49.109-35.072l-512 85.333c-20.309 3.456-35.627 20.992-35.627 42.069v406.827c-6.443-3.712-13.141-7.040-20.053-9.899-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323v-518.528l426.667-71.125v356.48c-6.443-3.712-13.141-7.040-20.053-9.899-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.373 34.475-37.035 55.381c-8.363 20.224-12.971 42.283-12.971 65.323s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM853.333 682.667c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733 7.893-7.893 17.323-14.251 27.733-18.56 9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56 7.893 7.893 14.251 17.323 18.56 27.733 4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "music" + ], + "grid": 0 + }, + { + "id": 165, + "paths": [ + "M552.021 70.571c-4.224-11.349-13.099-20.821-25.301-25.301-22.101-8.149-46.635 3.157-54.784 25.301l-298.667 810.667c-4.181 11.477-3.499 24.576 2.987 35.925 11.691 20.48 37.76 27.563 58.197 15.872l277.547-158.549 277.504 158.549c10.624 6.016 23.637 7.509 35.925 2.987 22.101-8.149 33.451-32.683 25.301-54.784zM512 208.768l218.155 592.085-196.992-112.555c-13.525-7.723-29.483-7.253-42.325 0l-196.992 112.555z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "navigation-2" + ], + "grid": 0 + }, + { + "id": 166, + "paths": [ + "M109.739 430.763c-11.179 5.333-19.925 15.36-23.125 28.203-5.717 22.869 8.192 46.037 31.061 51.755l316.501 79.104 79.104 316.501c3.029 11.989 11.136 22.528 23.125 28.203 21.291 10.069 46.72 0.981 56.832-20.309l384-810.667c5.291-11.221 5.675-24.533 0-36.523-10.069-21.291-35.541-30.379-56.832-20.309zM253.995 456.875l594.987-281.856-281.856 594.987-56.405-225.707c-3.925-15.744-16.128-27.221-31.061-31.061z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "navigation" + ], + "grid": 0 + }, + { + "id": 167, + "paths": [ + "M335.36 42.667c-10.923 0-21.845 4.181-30.165 12.501l-250.027 250.027c-7.723 7.723-12.501 18.389-12.501 30.165v353.28c0 10.923 4.181 21.845 12.501 30.165l250.027 250.027c7.723 7.723 18.389 12.501 30.165 12.501h353.28c10.923 0 21.845-4.181 30.165-12.501l250.027-250.027c7.723-7.723 12.501-18.389 12.501-30.165v-353.28c0-10.923-4.181-21.845-12.501-30.165l-250.027-250.027c-7.723-7.723-18.389-12.501-30.165-12.501zM353.024 128h317.952l225.024 225.024v317.952l-225.024 225.024h-317.952l-225.024-225.024v-317.952z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "octagon" + ], + "grid": 0 + }, + { + "id": 168, + "paths": [ + "M469.333 495.701v417.024l-317.995-158.976c-4.395-2.219-8.235-5.12-11.435-8.448-3.328-3.456-6.016-7.467-8.021-11.904-2.517-5.632-3.883-11.861-3.883-18.261v-390.059zM283.264 109.525c-2.517 0.981-4.949 2.219-7.253 3.669l-162.56 81.28c-16.469 8.32-30.891 19.968-42.325 34.133-9.045 11.136-16.256 23.851-21.12 37.675-4.779 13.397-7.339 27.733-7.339 42.624v406.144c0 18.517 3.925 36.736 11.435 53.376 5.888 13.099 14.037 25.216 24.149 35.84 9.856 10.283 21.504 19.029 34.901 25.771l341.461 170.709c9.003 4.48 18.475 7.851 28.245 10.155 14.251 3.328 29.141 4.224 43.989 2.432 14.379-1.707 28.629-5.888 42.155-12.629l341.504-170.752c16.469-8.32 30.891-19.968 42.325-34.133 9.045-11.136 16.256-23.851 21.12-37.675 4.821-13.397 7.381-27.733 7.381-42.624v-406.912c-0.128-18.432-4.181-36.565-11.733-53.12-5.973-13.056-14.165-25.131-24.363-35.669-9.899-10.197-21.632-18.901-34.816-25.472l-341.333-170.667c-8.96-4.437-18.176-7.723-27.648-9.984-14.293-3.371-29.184-4.352-44.075-2.645-14.421 1.664-28.715 5.803-42.411 12.587zM725.333 314.965l-331.051-165.547 98.816-49.408c4.523-2.261 9.259-3.584 13.952-4.139 4.907-0.555 9.856-0.256 14.677 0.896 3.2 0.768 6.357 1.877 9.131 3.243l312.149 156.075zM744.875 400.597l151.125-75.563v390.485c0 5.035-0.853 9.771-2.432 14.123-1.621 4.523-3.968 8.747-7.040 12.501-3.883 4.779-8.789 8.789-14.507 11.648l-317.355 158.72v-416.811l189.355-94.677zM298.88 197.163l331.051 165.504-117.931 58.965-331.051-165.547z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "package" + ], + "grid": 0 + }, + { + "id": 169, + "paths": [ + "M884.608 441.301l-392.107 392.107c-20.523 20.523-44.032 35.925-69.12 46.293-26.069 10.795-53.931 16.213-81.792 16.213s-55.723-5.419-81.792-16.213c-25.088-10.411-48.64-25.813-69.12-46.293s-35.925-44.032-46.293-69.12c-10.795-26.069-16.213-53.931-16.213-81.792s5.419-55.723 16.213-81.792c10.411-25.088 25.813-48.64 46.293-69.12l392.107-392.107c12.331-12.331 26.453-21.547 41.472-27.776 15.659-6.485 32.341-9.728 49.109-9.728s33.451 3.243 49.109 9.728c15.019 6.229 29.141 15.445 41.472 27.776s21.547 26.453 27.776 41.472c6.485 15.659 9.728 32.341 9.728 49.109s-3.243 33.451-9.728 49.109c-6.229 15.019-15.445 29.141-27.776 41.472l-392.533 392.107c-4.224 4.053-8.917 7.125-13.909 9.173-5.205 2.133-10.752 3.243-16.384 3.243s-11.179-1.109-16.384-3.243c-4.992-2.048-9.685-5.12-13.824-9.259s-7.211-8.832-9.259-13.824c-2.133-5.205-3.243-10.752-3.243-16.384s1.109-11.179 3.243-16.384c2.048-4.992 5.12-9.685 9.259-13.824l362.24-361.813c16.683-16.64 16.683-43.648 0.043-60.331s-43.648-16.683-60.331-0.043l-362.24 361.813c-12.203 12.203-21.504 26.368-27.776 41.515-6.485 15.701-9.728 32.384-9.728 49.024s3.243 33.323 9.728 49.024c6.272 15.147 15.573 29.312 27.776 41.515s26.368 21.504 41.515 27.776c15.701 6.485 32.384 9.728 49.024 9.728s33.323-3.243 49.024-9.728c15.147-6.272 29.312-15.573 41.472-27.733l392.533-392.107c20.395-20.395 35.84-43.947 46.293-69.163 10.837-26.155 16.213-53.973 16.213-81.749s-5.419-55.595-16.213-81.749c-10.453-25.216-25.899-48.768-46.293-69.163s-43.947-35.84-69.163-46.293c-26.155-10.837-53.973-16.213-81.749-16.213s-55.595 5.419-81.749 16.213c-25.216 10.453-48.768 25.899-69.163 46.293l-392.107 392.107c-28.544 28.544-50.176 61.568-64.811 96.811-15.147 36.608-22.741 75.563-22.741 114.475s7.552 77.867 22.741 114.475c14.592 35.243 36.224 68.267 64.811 96.811 28.544 28.544 61.568 50.176 96.811 64.811 36.608 15.147 75.563 22.741 114.475 22.741s77.867-7.552 114.475-22.741c35.243-14.592 68.267-36.224 96.811-64.811l392.107-392.107c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paperclip" + ], + "grid": 0 + }, + { + "id": 170, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM469.333 640v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM640 640v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pause-circle" + ], + "grid": 0 + }, + { + "id": 171, + "paths": [ + "M256 128c-23.552 0-42.667 19.115-42.667 42.667v682.667c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-682.667c0-23.552-19.115-42.667-42.667-42.667zM298.667 213.333h85.333v597.333h-85.333zM597.333 128c-23.552 0-42.667 19.115-42.667 42.667v682.667c0 23.552 19.115 42.667 42.667 42.667h170.667c23.552 0 42.667-19.115 42.667-42.667v-682.667c0-23.552-19.115-42.667-42.667-42.667zM640 213.333h85.333v597.333h-85.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pause" + ], + "grid": 0 + }, + { + "id": 172, + "paths": [ + "M798.165 584.832l12.501-12.501 67.669 67.669-238.336 238.336-67.669-67.669 12.501-12.501zM43.691 94.677c0.043 0.213 0.128 0.469 0.171 0.683l0.171 0.725 149.163 617.941c4.011 16.555 17.195 28.544 33.109 31.829l245.973 49.195c-6.016 15.317-2.859 33.408 9.557 45.781l128 128c16.683 16.683 43.691 16.683 60.331 0l298.667-298.667c16.683-16.683 16.683-43.691 0-60.331l-128-128c-12.373-12.373-30.464-15.573-45.781-9.557l-49.195-245.973c-3.328-16.725-16.043-29.184-31.829-33.109l-618.667-149.333c-1.237-0.299-2.517-0.555-3.755-0.725-6.144-0.939-12.16-0.469-17.835 1.109-5.803 1.664-11.179 4.48-15.787 8.32-3.925 3.285-7.296 7.296-9.856 11.904-1.877 3.328-3.328 6.955-4.267 10.837-0.299 1.195-0.512 2.389-0.683 3.541-0.853 5.461-0.64 10.88 0.512 15.829zM597.333 469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685s-33.835 3.456-48.981 9.728c-2.091 0.853-4.139 1.792-6.187 2.773l-191.445-191.445 444.8 107.349 54.187 270.891-181.035 181.035-270.891-54.187-107.392-444.757 191.445 191.445c-0.981 2.048-1.92 4.096-2.773 6.187-6.272 15.147-9.728 31.701-9.728 48.981s3.456 33.835 9.728 48.981c6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685s33.835-3.456 48.981-9.728c15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM512 469.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2s-11.349-1.152-16.299-3.2c-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299s1.152-11.349 3.2-16.299c2.048-4.949 5.035-9.472 8.747-13.312 0.171-0.171 0.341-0.341 0.512-0.555s0.341-0.341 0.555-0.512c3.84-3.712 8.363-6.699 13.312-8.747 4.992-2.091 10.496-3.243 16.341-3.243s11.349 1.152 16.299 3.2c5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pen-tool" + ], + "grid": 0 + }, + { + "id": 173, + "paths": [ + "M780.501 183.168l-597.333 597.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l597.333-597.333c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0zM426.667 277.333c0-20.139-4.011-39.467-11.307-57.131-7.595-18.304-18.688-34.731-32.427-48.469s-30.165-24.832-48.469-32.427c-17.664-7.296-36.992-11.307-57.131-11.307s-39.467 4.011-57.131 11.307c-18.304 7.595-34.731 18.688-48.469 32.427s-24.832 30.165-32.427 48.469c-7.296 17.664-11.307 36.992-11.307 57.131s4.011 39.467 11.307 57.131c7.595 18.304 18.688 34.731 32.427 48.469s30.165 24.832 48.469 32.427c17.664 7.296 36.992 11.307 57.131 11.307s39.467-4.011 57.131-11.307c18.304-7.595 34.731-18.688 48.469-32.427s24.832-30.165 32.427-48.469c7.296-17.664 11.307-36.992 11.307-57.131zM341.333 277.333c0 8.747-1.749 16.981-4.821 24.448-3.243 7.808-7.979 14.891-13.909 20.821s-13.013 10.667-20.779 13.909c-7.509 3.072-15.744 4.821-24.491 4.821s-16.981-1.749-24.448-4.821c-7.808-3.243-14.891-7.979-20.821-13.909s-10.667-13.013-13.909-20.779c-3.072-7.509-4.821-15.744-4.821-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.891 13.909-20.821s13.013-10.667 20.779-13.909c7.509-3.072 15.744-4.821 24.491-4.821s16.981 1.749 24.448 4.821c7.808 3.243 14.891 7.979 20.821 13.909s10.667 13.013 13.909 20.779c3.072 7.509 4.821 15.744 4.821 24.491zM896 746.667c0-20.139-4.011-39.467-11.307-57.131-7.595-18.304-18.688-34.731-32.427-48.469s-30.165-24.832-48.469-32.427c-17.664-7.296-36.992-11.307-57.131-11.307s-39.467 4.011-57.131 11.307c-18.304 7.595-34.731 18.688-48.469 32.427s-24.832 30.165-32.427 48.469c-7.296 17.664-11.307 36.992-11.307 57.131s4.011 39.467 11.307 57.131c7.595 18.304 18.688 34.731 32.427 48.469s30.165 24.832 48.469 32.427c17.664 7.296 36.992 11.307 57.131 11.307s39.467-4.011 57.131-11.307c18.304-7.595 34.731-18.688 48.469-32.427s24.832-30.165 32.427-48.469c7.296-17.664 11.307-36.992 11.307-57.131zM810.667 746.667c0 8.747-1.749 16.981-4.821 24.448-3.243 7.808-7.979 14.848-13.909 20.779s-13.013 10.667-20.779 13.909c-7.509 3.115-15.744 4.864-24.491 4.864s-16.981-1.749-24.448-4.821c-7.808-3.243-14.848-7.979-20.779-13.909s-10.667-13.013-13.909-20.779c-3.115-7.509-4.864-15.744-4.864-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.848 13.909-20.779s13.013-10.667 20.779-13.909c7.509-3.115 15.744-4.864 24.491-4.864s16.981 1.749 24.448 4.821c7.808 3.243 14.848 7.979 20.779 13.909s10.667 13.013 13.909 20.779c3.115 7.509 4.864 15.744 4.864 24.491z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "percent" + ], + "grid": 0 + }, + { + "id": 174, + "paths": [ + "M633.984 255.189c19.84 3.883 38.144 11.008 54.443 20.736 16.853 10.027 31.744 22.912 44.117 37.888 17.92 21.76 30.592 47.957 36.352 76.501 4.651 23.083 27.179 38.016 50.261 33.365s38.016-27.179 33.365-50.261c-8.533-42.325-27.349-81.365-54.144-113.877-18.517-22.443-40.832-41.771-66.304-56.917-24.661-14.677-52.139-25.387-81.749-31.147-23.125-4.523-45.525 10.581-50.048 33.707s10.581 45.525 33.707 50.048zM637.44 85.077c42.027 4.651 81.323 16.768 116.779 34.773 36.693 18.645 69.376 43.776 96.768 73.771 47.701 52.224 79.275 119.211 87.979 192.811 2.773 23.381 23.979 40.149 47.36 37.376s40.149-23.979 37.376-47.36c-10.795-91.605-50.176-175.189-109.696-240.384-34.219-37.461-75.093-68.864-121.088-92.288-44.501-22.656-93.696-37.717-146.048-43.563-23.424-2.603-44.501 14.293-47.104 37.717s14.293 44.501 37.675 47.104zM981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-call" + ], + "grid": 0 + }, + { + "id": 175, + "paths": [ + "M640 256h238.336l-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l170.667-170.667c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0.043-5.675-1.024-11.349-3.243-16.64-2.091-5.035-5.163-9.771-9.259-13.867l-170.667-170.667c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l97.835 97.835h-238.336c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-forwarded" + ], + "grid": 0 + }, + { + "id": 176, + "paths": [ + "M951.168 12.501l-225.835 225.835v-153.003c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 0.085 0 0.171 0 0.256 0.043 5.675 1.195 11.136 3.243 16.085 4.309 10.453 12.672 18.773 23.125 23.125 4.907 2.005 10.325 3.157 16 3.2 0.085 0 0.213 0 0.299 0h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-153.003l225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0zM981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-incoming" + ], + "grid": 0 + }, + { + "id": 177, + "paths": [ + "M695.168 72.832l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-missed" + ], + "grid": 0 + }, + { + "id": 178, + "paths": [ + "M257.237 523.349c-71.765-110.635-112.64-232.107-124.331-349.227-0.469-5.419 0.171-11.008 1.792-16.128 1.664-5.376 4.395-10.368 7.979-14.677s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l54.571-54.571c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.040-13.184-16.299-25.003-27.264-34.901-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.205 50.048 13.056 130.645 58.496 265.131 137.643 387.2 12.8 19.755 39.253 25.387 59.008 12.587s25.387-39.253 12.587-59.008zM396.16 688.213l60.629-60.629c39.637 35.328 81.835 65.323 123.691 88.533 16.939 9.429 37.547 6.059 50.901-7.125l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.693 0.683 9.685 2.432 14.080 4.907 4.608 2.603 8.661 6.016 12.032 10.112 3.157 3.84 5.632 8.235 7.296 12.971 1.621 4.608 2.517 9.557 2.475 14.72v128.256c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-38.699-24.576-74.155-51.627-104.832-79.189zM951.168 12.501l-521.856 521.813c-1.323 1.024-2.603 2.176-3.797 3.371-1.237 1.237-2.347 2.517-3.413 3.84l-409.6 409.6c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l262.912-262.912c35.115 31.915 75.264 62.72 118.187 89.984 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811v-127.744c0.085-15.189-2.475-29.952-7.339-43.733-4.992-14.123-12.373-27.136-21.76-38.571-9.984-12.16-22.187-22.528-36.181-30.421-13.525-7.595-28.629-12.843-45.611-15.232-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-25.984-16.512-52.096-36.011-77.227-58.069l494.208-494.251c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-off" + ], + "grid": 0 + }, + { + "id": 179, + "paths": [ + "M712.832 371.499l225.835-225.835v153.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-5.76-1.152-11.264-3.2-16.299-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h153.003l-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-outgoing" + ], + "grid": 0 + }, + { + "id": 180, + "paths": [ + "M981.333 721.92c0.341-14.507-2.133-29.525-6.912-43.563-4.949-14.379-12.331-27.691-21.803-39.339-9.984-12.288-22.229-22.741-36.309-30.72-13.611-7.68-28.8-12.971-45.824-15.36-34.133-4.181-72.32-13.397-110.336-27.563-18.475-6.784-38.101-9.301-57.344-7.424-14.336 1.408-28.501 5.205-41.813 11.435-12.928 6.059-25.003 14.379-35.755 25.045l-30.72 30.72c-75.52-47.659-143.36-113.792-195.541-195.797l30.976-30.976c13.739-14.080 24.021-30.976 30.165-49.323 4.565-13.653 6.869-28.16 6.613-42.88-0.256-14.293-2.944-28.672-8.277-42.923-12.331-32.128-22.101-70.144-27.477-110.72-2.176-15.061-6.955-29.227-13.824-42.112-7.083-13.141-16.341-24.96-27.307-34.859-11.691-10.539-25.344-18.944-40.363-24.619-14.507-5.504-30.251-8.448-46.123-8.277h-127.829c-3.755 0-7.765 0.171-11.648 0.512-17.195 1.536-33.365 6.485-47.872 14.080-15.061 7.893-28.203 18.645-38.869 31.403s-18.859 27.691-23.936 43.904c-4.864 15.659-6.827 32.427-5.248 49.92 12.8 131.243 58.24 266.368 137.216 388.352 64.085 102.955 155.648 197.248 268.715 269.056 109.568 72.405 242.517 122.112 387.669 137.856 3.925 0.384 8.149 0.555 12.288 0.555 17.28-0.085 33.792-3.584 48.939-9.899 15.659-6.571 29.696-16.128 41.429-27.947s21.163-25.941 27.605-41.643c6.229-15.147 9.6-31.744 9.515-48.811zM896 721.92v128c0.043 6.016-1.109 11.52-3.157 16.512-2.133 5.205-5.291 9.941-9.216 13.909s-8.661 7.168-13.824 9.344c-4.949 2.091-10.453 3.243-16.299 3.285-1.536 0-2.901-0.085-3.755-0.171-130.56-14.208-250.581-59.221-348.757-124.117-103.595-65.835-185.984-150.955-243.285-242.944-72.405-111.787-113.28-233.856-124.757-351.488-0.512-5.547 0.171-11.093 1.749-16.213 1.664-5.419 4.395-10.411 7.979-14.72s7.979-7.893 12.971-10.496c4.779-2.475 10.112-4.139 15.957-4.651 1.451-0.128 2.731-0.171 3.584-0.171h128.171c5.973-0.043 11.136 0.939 15.915 2.731 4.949 1.877 9.472 4.651 13.397 8.192 3.669 3.328 6.784 7.296 9.173 11.733 2.304 4.309 3.925 9.088 4.651 14.208 6.016 45.739 17.408 90.368 32.256 129.067 1.664 4.48 2.56 9.216 2.603 13.867 0.085 4.821-0.683 9.6-2.219 14.208-2.091 6.187-5.589 11.989-10.325 16.853l-53.803 53.803c-13.824 13.824-16.171 34.731-6.912 51.243 67.584 118.827 163.797 211.499 272.256 272.128 16.939 9.472 37.632 6.144 50.987-7.083l54.187-54.187c3.456-3.413 7.424-6.144 11.648-8.107 4.352-2.048 9.045-3.328 13.867-3.797 6.485-0.64 13.227 0.213 19.584 2.56 43.605 16.256 88.32 27.136 129.451 32.171 4.736 0.683 9.771 2.432 14.208 4.949 4.608 2.603 8.704 6.101 12.075 10.197 3.2 3.925 5.675 8.405 7.339 13.227 1.621 4.693 2.432 9.728 2.304 15.872z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone" + ], + "grid": 0 + }, + { + "id": 181, + "paths": [ + "M865.664 661.376c-20.309 47.957-49.109 89.6-83.968 123.904-36.181 35.669-78.933 63.531-125.397 82.347s-96.512 28.629-147.328 28.245c-48.896-0.341-98.603-10.112-146.56-30.421-47.957-20.267-89.6-49.109-123.904-83.968-35.669-36.181-63.531-78.933-82.347-125.397-18.816-46.421-28.629-96.512-28.245-147.328 0.341-48.896 10.112-98.603 30.421-146.56 19.627-46.421 47.275-86.912 80.555-120.576 34.56-34.901 75.264-62.549 119.509-81.835 21.589-9.429 31.488-34.56 22.101-56.149s-34.56-31.488-56.149-22.101c-54.101 23.637-103.851 57.387-146.133 100.139-40.789 41.216-74.581 90.752-98.517 147.328-24.704 58.496-36.693 119.253-37.163 179.157-0.469 62.208 11.52 123.349 34.517 180.053s57.003 108.885 100.651 153.216c42.069 42.667 93.013 77.909 151.467 102.656s119.253 36.736 179.157 37.163c62.208 0.469 123.349-11.52 180.053-34.517s108.885-57.003 153.216-100.651c42.667-42.069 77.909-93.013 102.656-151.467 9.173-21.717-0.981-46.72-22.699-55.936s-46.72 0.981-55.936 22.699zM893.653 469.333h-338.987v-338.987c36.565 4.053 71.509 13.184 104.192 26.709 46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 13.525 32.683 22.699 67.627 26.709 104.192zM981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584-23.552 0-42.667 19.115-42.667 42.667v426.667c0 23.552 19.115 42.667 42.667 42.667h426.667c23.552 0 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pie-chart" + ], + "grid": 0 + }, + { + "id": 182, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM450.347 305.835c-6.656-4.48-14.848-7.168-23.68-7.168-23.552 0-42.667 19.115-42.667 42.667v341.333c-0.043 8.021 2.261 16.341 7.168 23.68 13.056 19.627 39.552 24.917 59.179 11.819l256-170.667c4.395-2.901 8.533-6.912 11.819-11.819 13.056-19.627 7.765-46.080-11.819-59.179zM469.333 421.077l136.405 90.923-136.405 90.923z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "play-circle" + ], + "grid": 0 + }, + { + "id": 183, + "paths": [ + "M236.416 92.117c-6.528-4.267-14.507-6.784-23.083-6.784-23.552 0-42.667 19.115-42.667 42.667v768c-0.043 7.765 2.133 15.872 6.784 23.083 12.757 19.84 39.125 25.557 58.965 12.8l597.333-384c4.864-3.072 9.344-7.424 12.8-12.8 12.757-19.84 6.997-46.208-12.8-58.965zM256 206.165l475.776 305.835-475.776 305.835z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "play" + ], + "grid": 0 + }, + { + "id": 184, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM341.333 554.667h128v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v128h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "plus-circle" + ], + "grid": 0 + }, + { + "id": 185, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM213.333 170.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM341.333 554.667h128v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128h128c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-128v-128c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v128h-128c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "plus-square" + ], + "grid": 0 + }, + { + "id": 186, + "paths": [ + "M213.333 554.667h256v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "plus" + ], + "grid": 0 + }, + { + "id": 187, + "paths": [ + "M170.667 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v256c0 63.488 12.629 124.16 35.541 179.499 23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541v-256c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM170.667 170.667h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v256c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859v-256c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM311.168 456.832l170.667 170.667c16.683 16.683 43.691 16.683 60.331 0l170.667-170.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-140.501 140.501-140.501-140.501c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pocket" + ], + "grid": 0 + }, + { + "id": 188, + "paths": [ + "M753.195 313.472c32.725 32.768 57.387 70.4 74.027 110.549 17.28 41.728 25.941 86.272 25.941 130.816s-8.661 89.045-25.984 130.816c-16.64 40.149-41.301 77.824-74.027 110.549s-70.4 57.387-110.549 74.027c-41.728 17.28-86.272 25.941-130.816 25.941s-89.131-8.704-130.859-26.027c-40.149-16.64-77.824-41.301-110.549-74.027-32.725-32.768-57.387-70.4-73.984-110.549-17.28-41.728-25.941-86.229-25.941-130.773s8.661-89.045 25.941-130.773c16.64-40.149 41.259-77.781 73.984-110.549 16.64-16.683 16.64-43.691 0-60.331s-43.691-16.64-60.331 0c-40.789 40.789-71.637 87.893-92.501 138.197-21.632 52.224-32.427 107.819-32.427 163.413s10.795 111.189 32.427 163.413c20.821 50.304 51.712 97.408 92.501 138.24s87.893 71.68 138.24 92.544c52.224 21.632 107.861 32.469 163.456 32.469s111.232-10.795 163.456-32.427c50.347-20.821 97.451-51.712 138.24-92.501s71.68-87.893 92.544-138.24c21.632-52.224 32.469-107.861 32.469-163.456s-10.795-111.232-32.427-163.456c-20.821-50.347-51.712-97.451-92.501-138.24-16.64-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM469.333 85.333v426.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-426.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "power" + ], + "grid": 0 + }, + { + "id": 189, + "paths": [ + "M725.333 341.333h-426.667v-213.333h426.667zM213.333 810.667v128c0 23.552 19.115 42.667 42.667 42.667h512c23.552 0 42.667-19.115 42.667-42.667v-128h42.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-213.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-42.667v-256c0-23.552-19.115-42.667-42.667-42.667h-512c-23.552 0-42.667 19.115-42.667 42.667v256h-42.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v213.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685zM256 554.667c-23.552 0-42.667 19.115-42.667 42.667v128h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-213.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v213.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-42.667v-128c0-23.552-19.115-42.667-42.667-42.667zM298.667 640h426.667v256h-426.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "printer" + ], + "grid": 0 + }, + { + "id": 190, + "paths": [ + "M640 512c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685s-33.835 3.456-48.981 9.728c-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939s3.456 33.835 9.728 48.981c6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685s33.835-3.456 48.981-9.728c15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM554.667 512c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2s-11.349-1.152-16.299-3.2c-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299s1.152-11.349 3.2-16.299c2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2s11.349 1.152 16.299 3.2c5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299zM662.741 361.259c20.48 20.48 35.925 43.989 46.293 69.077 10.795 26.069 16.256 53.888 16.256 81.749s-5.376 55.68-16.171 81.749c-10.368 25.088-25.771 48.597-46.251 69.12-16.64 16.683-16.64 43.691 0.043 60.331s43.691 16.64 60.331-0.043c28.544-28.587 50.133-61.568 64.725-96.811 15.147-36.608 22.656-75.52 22.656-114.432s-7.595-77.824-22.784-114.389c-14.635-35.243-36.267-68.224-64.811-96.725-16.683-16.64-43.691-16.64-60.331 0.043s-16.64 43.691 0.043 60.331zM361.259 662.741c-20.48-20.48-35.925-43.989-46.293-69.077-10.837-26.069-16.256-53.888-16.299-81.749s5.376-55.68 16.171-81.749c10.368-25.088 25.771-48.597 46.251-69.12 16.64-16.683 16.64-43.691-0.043-60.331s-43.691-16.64-60.331 0.043c-28.544 28.587-50.133 61.568-64.725 96.811-15.147 36.565-22.656 75.477-22.656 114.389s7.595 77.824 22.784 114.389c14.635 35.243 36.267 68.224 64.811 96.725 16.683 16.64 43.691 16.64 60.331-0.043s16.64-43.691-0.043-60.331zM783.488 240.512c36.821 36.821 64.555 79.189 83.243 124.373 19.456 46.933 29.184 97.024 29.184 147.115s-9.728 100.181-29.184 147.115c-18.688 45.184-46.421 87.509-83.243 124.373-16.64 16.683-16.64 43.691 0 60.331s43.691 16.64 60.331 0c44.885-44.885 78.805-96.683 101.717-152.021 23.808-57.472 35.669-118.613 35.669-179.755s-11.861-122.325-35.669-179.797c-22.912-55.339-56.875-107.179-101.76-152.064-16.64-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM240.512 783.488c-36.821-36.821-64.555-79.189-83.243-124.373-19.456-46.933-29.184-97.024-29.184-147.115s9.728-100.181 29.184-147.115c18.731-45.184 46.421-87.552 83.243-124.373 16.64-16.683 16.64-43.691 0-60.331s-43.691-16.64-60.331 0c-44.885 44.885-78.848 96.683-101.76 152.021-23.765 57.472-35.669 118.656-35.669 179.797s11.861 122.325 35.669 179.797c22.912 55.339 56.875 107.179 101.76 152.064 16.64 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "radio" + ], + "grid": 0 + }, + { + "id": 191, + "paths": [ + "M914.475 369.749c-19.243-54.4-48.469-102.571-84.907-143.061-37.845-42.027-83.371-75.733-133.547-99.669s-105.003-38.144-161.493-41.131c-54.4-2.859-110.208 4.693-164.608 23.979-62.805 22.229-117.419 57.771-159.701 100.864l-124.885 117.333v-157.397c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-148.309l120.747-113.451c33.152-33.792 76.672-62.293 127.275-80.213 43.648-15.445 88.277-21.461 131.669-19.2 45.099 2.389 89.003 13.739 129.237 32.939s76.672 46.165 106.88 79.744c29.056 32.299 52.437 70.784 67.883 114.432 7.851 22.229 32.256 33.835 54.443 25.984s33.835-32.256 25.984-54.443zM873.6 640l-119.467 112.256c-33.707 33.707-71.339 58.325-111.488 74.965-41.728 17.28-86.229 25.984-130.773 25.984s-89.088-8.619-130.816-25.899c-40.149-16.64-77.824-41.259-110.592-73.984-37.973-37.973-64.981-82.389-80.341-127.189-7.68-22.272-31.915-34.133-54.229-26.496s-34.133 31.915-26.496 54.229c19.627 57.131 53.632 112.725 100.736 159.787 40.832 40.789 87.936 71.637 138.283 92.501 52.267 21.632 107.861 32.427 163.456 32.384s111.189-10.837 163.456-32.469c50.304-20.864 97.408-51.755 137.259-91.648l126.080-118.443v157.355c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-0.597 0-1.195-0.043-1.792-0.213-4.907-1.237-9.6-2.944-13.952-1.749-4.437-4.224-8.491-7.296-12.032-0.555-0.64-1.109-1.237-1.664-1.835-3.883-4.011-8.533-7.296-13.739-9.515-5.077-2.176-10.624-3.456-16.469-3.499-0.171 0-0.299 0-0.469 0h-256.043c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "refresh-ccw" + ], + "grid": 0 + }, + { + "id": 192, + "paths": [ + "M189.995 398.251c15.445-43.648 38.827-82.133 67.883-114.432 30.208-33.579 66.645-60.587 106.88-79.744s84.096-30.549 129.237-32.939c43.392-2.304 88.021 3.755 131.669 19.2 50.603 17.92 94.123 46.421 127.275 80.213l120.704 113.451h-148.309c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h256c0.171 0 0.299 0 0.469 0 5.845-0.043 11.435-1.323 16.469-3.499 5.205-2.261 9.856-5.504 13.739-9.515 0.555-0.597 1.152-1.195 1.664-1.835 3.072-3.541 5.547-7.637 7.296-12.032 1.749-4.352 2.773-9.045 2.944-13.952 0.085-0.64 0.085-1.237 0.085-1.835v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v157.397l-124.843-117.291c-42.325-43.093-96.896-78.635-159.701-100.864-54.4-19.243-110.208-26.837-164.608-23.979-56.491 2.987-111.317 17.195-161.493 41.131s-95.701 57.643-133.547 99.669c-36.437 40.491-65.664 88.619-84.907 143.061-7.851 22.229 3.755 46.592 25.984 54.443s46.592-3.755 54.443-25.984zM85.333 695.979l126.080 118.485c39.851 39.893 86.955 70.784 137.259 91.648 52.224 21.632 107.861 32.469 163.456 32.469s111.232-10.795 163.456-32.384c50.347-20.821 97.451-51.669 138.283-92.501 47.104-47.104 81.109-102.699 100.736-159.787 7.68-22.272-4.181-46.549-26.496-54.229s-46.549 4.181-54.229 26.496c-15.403 44.8-42.368 89.216-80.341 127.189-32.768 32.725-70.4 57.387-110.592 73.984-41.728 17.28-86.272 25.941-130.816 25.899s-89.045-8.704-130.773-25.984c-40.149-16.64-77.781-41.301-111.488-74.965l-119.467-112.299h148.267c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-0.171 0-0.299 0-0.469 0-5.845 0.043-11.435 1.323-16.469 3.499-5.205 2.261-9.899 5.547-13.781 9.6-0.555 0.555-1.067 1.152-1.579 1.749-3.072 3.584-5.589 7.68-7.339 12.117-1.707 4.352-2.731 9.003-2.944 13.909-0.085 0.597-0.085 1.195-0.085 1.792v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "refresh-cw" + ], + "grid": 0 + }, + { + "id": 193, + "paths": [ + "M170.667 469.333v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h494.336l-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l170.667-170.667c4.096-4.096 7.168-8.832 9.259-13.867 2.091-5.12 3.2-10.539 3.243-15.957 0-0.213 0-0.469 0-0.683-0.043-5.419-1.109-10.88-3.243-15.957-2.091-5.035-5.163-9.771-9.259-13.867l-170.667-170.667c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l97.835 97.835h-494.336c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM853.333 554.667v85.333c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685h-494.336l97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-170.667 170.667c-4.096 4.096-7.211 8.832-9.259 13.867-2.133 5.077-3.2 10.453-3.243 15.872 0 0.299 0 0.597 0 0.853 0.043 5.419 1.109 10.795 3.2 15.872 2.091 5.035 5.163 9.771 9.259 13.867l170.667 170.667c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.792-97.835h494.336c28.8 0 56.405-5.717 81.579-16.171 26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "repeat" + ], + "grid": 0 + }, + { + "id": 194, + "paths": [ + "M426.667 723.413l-271.829-211.413 271.829-211.413zM912.469 844.331c7.168 5.632 16.299 9.003 26.197 9.003 23.552 0 42.667-19.115 42.667-42.667v-597.333c0.043-9.088-2.901-18.347-9.003-26.197-14.464-18.603-41.259-21.931-59.861-7.467l-384 298.667c-2.603 1.963-5.205 4.523-7.467 7.467-5.973 7.68-8.917 16.725-9.003 25.771v-298.24c0.043-9.088-2.901-18.347-9.003-26.197-14.464-18.603-41.259-21.931-59.861-7.467l-384 298.667c-2.603 1.963-5.205 4.523-7.467 7.467-14.464 18.603-11.136 45.397 7.467 59.861l384 298.667c7.168 5.632 16.299 9.003 26.197 9.003 23.552 0 42.667-19.115 42.667-42.667v-298.283c0.128 12.587 5.76 24.96 16.469 33.28zM896 723.413l-271.829-211.413 271.829-211.413z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rewind" + ], + "grid": 0 + }, + { + "id": 195, + "paths": [ + "M109.525 654.165c19.157 54.443 48.341 102.613 84.693 143.147 37.76 42.069 83.285 75.861 133.419 99.883s104.96 38.315 161.451 41.344c54.4 2.944 110.208-4.565 164.651-23.723s102.613-48.341 143.147-84.693c42.069-37.76 75.861-83.285 99.883-133.419s38.315-104.96 41.344-161.451c2.944-54.4-4.565-110.208-23.723-164.651s-48.341-102.613-84.693-143.147c-37.76-42.069-83.285-75.861-133.419-99.883s-104.96-38.315-161.451-41.344c-54.4-2.944-110.251 4.565-164.651 23.765-62.891 22.144-117.547 57.643-159.957 100.736l-124.885 117.333v-157.397c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v256c0 0.597 0 1.195 0.043 1.792 0.213 4.907 1.237 9.6 2.944 13.909 1.749 4.48 4.267 8.533 7.339 12.117 0.512 0.597 1.024 1.152 1.579 1.749 3.883 4.053 8.576 7.339 13.781 9.6 5.077 2.176 10.624 3.456 16.469 3.499 0.171 0 0.299 0 0.469 0h256.043c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-148.309l120.704-113.451c33.237-33.792 76.8-62.251 127.445-80.085 43.691-15.403 88.277-21.333 131.669-18.987 45.099 2.432 89.003 13.867 129.195 33.109s76.587 46.293 106.752 79.915c29.013 32.341 52.352 70.827 67.755 114.517s21.333 88.277 18.987 131.669c-2.432 45.099-13.867 89.003-33.109 129.195s-46.293 76.587-79.915 106.752c-32.341 29.013-70.827 52.352-114.517 67.755s-88.277 21.333-131.669 18.987c-45.099-2.432-89.003-13.867-129.195-33.109s-76.587-46.293-106.752-79.915c-29.013-32.341-52.352-70.827-67.755-114.517-7.851-22.229-32.213-33.877-54.4-26.069s-33.877 32.213-26.069 54.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rotate-ccw" + ], + "grid": 0 + }, + { + "id": 196, + "paths": [ + "M834.005 625.792c-15.445 43.648-38.784 82.133-67.84 114.475-30.208 33.579-66.645 60.587-106.837 79.829-40.192 19.2-84.096 30.592-129.195 32.981-43.392 2.304-88.021-3.712-131.669-19.115s-82.133-38.784-114.475-67.84c-33.579-30.208-60.587-66.645-79.829-106.837s-30.592-84.096-32.981-129.195c-2.304-43.392 3.712-88.021 19.115-131.669s38.784-82.133 67.84-114.475c30.208-33.579 66.645-60.587 106.837-79.829s84.096-30.592 129.195-32.981c43.392-2.304 88.021 3.712 131.669 19.115 50.688 17.92 94.251 46.421 127.445 80.299l120.533 113.451h-148.48c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h256c0.171 0 0.299 0 0.469 0 5.845-0.043 11.392-1.28 16.427-3.499 5.248-2.261 9.941-5.547 13.824-9.6 0.512-0.512 0.981-1.067 1.493-1.621 3.115-3.584 5.675-7.723 7.467-12.245 1.749-4.352 2.773-9.088 2.944-13.995 0.043-0.555 0.043-1.109 0.043-1.707v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v157.227l-124.416-117.12c-42.368-43.179-97.024-78.763-159.915-100.949-54.4-19.243-110.251-26.752-164.608-23.893-56.448 2.987-111.317 17.237-161.451 41.216s-95.744 57.685-133.547 99.712c-36.437 40.533-65.621 88.661-84.864 143.104s-26.752 110.251-23.893 164.608c2.987 56.448 17.237 111.317 41.216 161.451s57.685 95.701 99.755 133.504c40.491 36.437 88.661 65.621 143.104 84.821s110.251 26.752 164.608 23.893c56.448-2.987 111.317-17.237 161.451-41.216s95.701-57.685 133.504-99.755c36.437-40.491 65.621-88.661 84.821-143.104 7.851-22.229-3.797-46.592-26.027-54.443s-46.592 3.797-54.443 26.027z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rotate-cw" + ], + "grid": 0 + }, + { + "id": 197, + "paths": [ + "M170.667 512c46.293 0 90.368 9.216 130.517 25.813 41.728 17.28 79.317 42.624 110.848 74.155s56.875 69.12 74.155 110.848c16.597 40.149 25.813 84.224 25.813 130.517 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667c0-57.728-11.477-112.853-32.341-163.157-21.632-52.224-53.333-99.2-92.629-138.539s-86.272-70.997-138.539-92.629c-50.304-20.864-105.429-32.341-163.157-32.341-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM170.667 213.333c86.784 0 169.387 17.237 244.736 48.469 78.293 32.427 148.779 79.957 207.829 139.008s106.581 129.536 139.008 207.829c31.189 75.307 48.427 157.909 48.427 244.693 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667c0-98.176-19.541-191.872-54.955-277.376-36.779-88.789-90.667-168.661-157.483-235.477s-146.688-120.704-235.477-157.483c-85.547-35.456-179.243-54.997-277.419-54.997-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM298.667 810.667c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rss" + ], + "grid": 0 + }, + { + "id": 198, + "paths": [ + "M810.667 938.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-469.333c0-10.923-4.181-21.845-12.501-30.165l-213.333-213.333c-7.723-7.723-18.389-12.501-30.165-12.501h-469.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685zM341.333 853.333v-256h341.333v256zM256 170.667v170.667c0 23.552 19.115 42.667 42.667 42.667h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-298.667v-128h323.669l188.331 188.331v451.669c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-42.667v-298.667c0-23.552-19.115-42.667-42.667-42.667h-426.667c-23.552 0-42.667 19.115-42.667 42.667v298.667h-42.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "save" + ], + "grid": 0 + }, + { + "id": 199, + "paths": [ + "M314.795 706.176c0.469 0.512 0.981 1.067 1.493 1.536s1.024 0.981 1.536 1.493c7.211 7.552 13.013 16.427 17.067 26.197 4.139 9.984 6.443 20.949 6.443 32.597s-2.304 22.613-6.443 32.64c-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c9.728 4.053 18.603 9.856 26.197 17.067zM587.264 648.021l235.947 235.52c16.683 16.64 43.691 16.64 60.331-0.043s16.64-43.691-0.043-60.331l-235.947-235.52c-16.683-16.64-43.691-16.64-60.331 0.043s-16.64 43.691 0.043 60.331zM317.824 314.795c-0.512 0.469-1.024 0.981-1.536 1.493s-1.024 1.024-1.493 1.536c-7.552 7.211-16.427 13.013-26.197 17.067-9.984 4.139-20.949 6.443-32.597 6.443s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597s-2.304 22.613-6.443 32.64c-4.053 9.728-9.856 18.603-17.067 26.197zM342.699 403.029l108.971 108.971-108.971 108.971c-6.827-4.053-13.995-7.637-21.419-10.709-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-3.072-7.424-6.656-14.592-10.709-21.419l480.469-480.469c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-311.168 311.168-108.971-108.971c4.053-6.827 7.637-13.995 10.709-21.419 8.32-20.181 12.928-42.24 12.928-65.28s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.517 28.416 55.424 37.077c20.181 8.32 42.24 12.928 65.28 12.928s45.099-4.608 65.28-12.928c7.424-3.072 14.592-6.656 21.419-10.709z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "scissors" + ], + "grid": 0 + }, + { + "id": 200, + "paths": [ + "M684.416 676.523c-1.451 1.109-2.859 2.347-4.224 3.712s-2.56 2.731-3.712 4.224c-26.752 25.771-58.24 46.549-93.013 60.971-35.072 14.507-73.6 22.571-114.133 22.571s-79.061-8.064-114.219-22.613c-36.523-15.104-69.419-37.291-96.981-64.896s-49.749-60.459-64.896-96.981c-14.507-35.115-22.571-73.643-22.571-114.176s8.064-79.061 22.613-114.219c15.104-36.48 37.291-69.419 64.853-96.981s60.501-49.749 96.981-64.853c35.157-14.549 73.685-22.613 114.219-22.613s79.061 8.064 114.219 22.613c36.523 15.104 69.419 37.291 96.981 64.896s49.749 60.459 64.896 96.981c14.507 35.115 22.571 73.643 22.571 114.176s-8.064 79.061-22.613 114.219c-14.421 34.773-35.2 66.261-60.971 93.013zM926.165 865.835l-156.8-156.8c22.4-27.989 40.96-59.179 54.869-92.843 18.773-45.312 29.099-94.933 29.099-146.859s-10.325-101.547-29.099-146.859c-19.456-47.019-48-89.301-83.371-124.672s-77.653-63.915-124.672-83.371c-45.312-18.773-94.933-29.099-146.859-29.099s-101.547 10.325-146.859 29.099c-47.019 19.456-89.301 48-124.672 83.371s-63.915 77.653-83.371 124.672c-18.773 45.312-29.099 94.933-29.099 146.859s10.325 101.547 29.099 146.859c19.456 47.019 48 89.301 83.371 124.672s77.653 63.915 124.672 83.371c45.312 18.773 94.933 29.099 146.859 29.099s101.547-10.325 146.859-29.099c33.621-13.952 64.853-32.512 92.843-54.869l156.8 156.8c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "search" + ], + "grid": 0 + }, + { + "id": 201, + "paths": [ + "M979.755 96.811c1.792-6.443 2.133-13.397 0.64-20.309-0.384-1.749-0.853-3.499-1.493-5.248-1.067-3.072-2.475-5.973-4.139-8.619-2.731-4.352-6.187-8.107-10.155-11.136-4.864-3.712-10.496-6.357-16.469-7.723-5.845-1.323-12.032-1.451-18.176-0.171-1.792 0.384-3.627 0.896-5.419 1.493l-0.896 0.299-852.395 298.325c-10.752 3.755-19.925 11.776-24.917 22.955-9.557 21.547 0.128 46.763 21.675 56.32l369.024 164.011 164.011 369.024c4.608 10.368 13.355 18.901 24.875 22.955 22.229 7.765 46.592-3.925 54.357-26.197l298.667-853.333c0.299-0.853 0.597-1.749 0.811-2.603zM459.904 503.765l-258.901-115.029 575.275-201.387zM836.651 247.723l-201.387 575.275-115.029-258.901z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "send" + ], + "grid": 0 + }, + { + "id": 202, + "paths": [ + "M170.667 42.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v170.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-170.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM170.667 128h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v170.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.176-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-170.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM170.667 554.667c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v170.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-170.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM170.667 640h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v170.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-170.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM298.667 256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667zM298.667 768c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "server" + ], + "grid": 0 + }, + { + "id": 203, + "paths": [ + "M682.667 512c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM597.333 512c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597zM866.773 657.237c1.963-4.48 4.779-8.149 8.192-10.965 2.432-2.005 5.205-3.584 8.107-4.651 2.816-1.024 5.931-1.621 9.088-1.621h3.84c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939s-3.456-33.835-9.728-48.981c-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-6.784c-4.693-0.043-9.173-1.195-13.141-3.243-2.816-1.451-5.333-3.371-7.424-5.675-2.005-2.133-3.712-4.693-4.992-7.637-0.128-1.237-0.171-2.517-0.171-3.797-1.963-4.523-2.731-9.344-2.304-13.995 0.256-3.157 1.067-6.229 2.389-9.088 1.237-2.731 3.029-5.376 5.035-7.424l2.645-2.645c12.203-12.203 21.461-26.368 27.733-41.515 6.485-15.701 9.728-32.384 9.728-49.024s-3.243-33.323-9.771-49.024c-6.272-15.147-15.573-29.269-27.861-41.557-12.203-12.203-26.368-21.461-41.515-27.733-15.701-6.485-32.384-9.728-49.024-9.728s-33.365 3.285-49.067 9.771c-15.147 6.272-29.269 15.573-41.472 27.776l-1.963 2.005c-3.541 3.413-7.808 5.803-12.288 7.083-3.072 0.853-6.229 1.195-9.344 0.981-3.029-0.213-6.101-0.939-9.301-2.347-4.309-1.877-7.979-4.693-10.795-8.107-2.005-2.432-3.584-5.205-4.651-8.107-1.067-2.859-1.664-5.973-1.664-9.131v-3.84c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685s-33.835 3.456-48.981 9.728c-15.701 6.485-29.781 16-41.557 27.776s-21.248 25.856-27.733 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v6.784c-0.043 4.693-1.195 9.173-3.243 13.141-1.451 2.816-3.371 5.333-5.675 7.424-2.133 2.005-4.693 3.712-7.637 4.992-1.237 0.128-2.517 0.171-3.797 0.171-4.523 1.963-9.344 2.731-13.995 2.304-3.2-0.256-6.272-1.067-9.131-2.347-2.731-1.28-5.376-3.029-7.424-5.035l-2.645-2.645c-12.203-12.203-26.368-21.461-41.515-27.733-15.701-6.485-32.384-9.728-49.024-9.728s-33.323 3.285-49.024 9.771c-15.104 6.272-29.269 15.573-41.515 27.861-12.203 12.203-21.461 26.368-27.733 41.515-6.485 15.701-9.728 32.384-9.728 49.024s3.285 33.323 9.771 49.024c6.272 15.104 15.573 29.269 27.776 41.429l2.005 2.005c3.413 3.541 5.803 7.808 7.083 12.288 0.853 3.072 1.195 6.229 0.981 9.344-0.213 3.029-0.939 6.101-2.304 9.173-0.256 0.683-0.555 1.451-0.896 2.219-1.749 4.651-4.608 8.661-8.149 11.733-2.432 2.091-5.12 3.712-8.064 4.864-2.816 1.109-5.931 1.749-8.277 1.835h-3.84c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.557c-6.272 15.104-9.728 31.659-9.728 48.939s3.456 33.835 9.728 48.981c6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h6.784c4.693 0.043 9.173 1.195 13.141 3.243 2.816 1.451 5.333 3.371 7.424 5.675 2.048 2.219 3.797 4.821 5.163 8.021 1.963 4.523 2.731 9.344 2.304 13.995-0.256 3.157-1.067 6.229-2.389 9.088-1.237 2.731-3.029 5.376-5.035 7.424l-2.645 2.645c-12.203 12.203-21.461 26.368-27.733 41.515-6.485 15.701-9.728 32.384-9.728 49.024s3.243 33.323 9.771 49.024c6.272 15.147 15.573 29.269 27.861 41.557 12.203 12.203 26.368 21.461 41.515 27.733 15.701 6.485 32.384 9.728 49.024 9.728s33.323-3.243 49.024-9.771c15.147-6.272 29.269-15.573 41.472-27.776l2.005-2.048c3.541-3.413 7.808-5.803 12.288-7.083 3.072-0.853 6.229-1.195 9.344-0.981 3.029 0.213 6.101 0.939 9.173 2.304 0.683 0.256 1.451 0.555 2.219 0.896 4.651 1.749 8.661 4.608 11.733 8.149 2.091 2.389 3.712 5.12 4.864 8.064 1.067 2.816 1.749 5.931 1.792 8.277v3.883c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.147 6.229 31.701 9.685 48.981 9.685s33.835-3.456 48.981-9.728c15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.272-15.147 9.728-31.701 9.728-48.981v-6.784c0.043-4.693 1.195-9.173 3.243-13.141 1.451-2.816 3.371-5.333 5.675-7.424 2.219-2.048 4.821-3.797 8.021-5.163 4.523-1.963 9.344-2.731 13.995-2.304 3.157 0.256 6.229 1.067 9.088 2.389 2.731 1.237 5.376 3.029 7.424 5.035l2.645 2.645c12.203 12.203 26.368 21.461 41.515 27.733 15.701 6.485 32.384 9.728 49.024 9.728s33.323-3.243 49.024-9.771c15.147-6.272 29.269-15.573 41.557-27.861 12.203-12.203 21.461-26.368 27.733-41.515 6.485-15.701 9.728-32.384 9.728-49.024s-3.243-33.323-9.771-49.024c-6.272-15.147-15.573-29.269-27.776-41.472l-2.048-2.005c-3.413-3.541-5.803-7.808-7.083-12.288-0.853-3.072-1.195-6.229-0.981-9.344 0.213-3.029 0.939-6.101 2.304-9.173zM788.693 622.763c-5.376 12.16-8.448 24.832-9.344 37.547-0.939 13.099 0.469 26.112 3.925 38.443 5.077 18.048 14.549 34.731 27.819 48.512l3.072 3.115c4.181 4.139 7.211 8.832 9.301 13.824 2.133 5.205 3.243 10.752 3.243 16.384s-1.067 11.179-3.243 16.384c-2.048 4.949-5.12 9.643-9.259 13.824-4.224 4.224-8.917 7.253-13.867 9.344-5.205 2.133-10.752 3.243-16.384 3.243s-11.179-1.067-16.384-3.243c-4.949-2.048-9.643-5.12-13.824-9.259l-2.603-2.603c-9.813-9.643-20.651-16.981-32.213-22.272-11.947-5.461-24.619-8.747-37.376-9.813-18.645-1.579-37.632 1.451-55.168 9.045-11.989 5.12-22.827 12.288-32.128 20.907-9.557 8.875-17.408 19.243-23.339 30.592-8.235 15.829-12.715 33.493-12.928 51.669v7.723c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.864 1.92-10.368 3.072-16.213 3.072s-11.349-1.152-16.299-3.2c-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-3.84c-0.341-14.293-2.944-27.093-7.509-38.997-4.736-12.245-11.52-23.467-19.925-33.152-12.117-13.995-27.563-24.96-45.141-31.787-11.819-5.077-24.149-7.979-36.437-8.875-13.099-0.939-26.112 0.469-38.443 3.925-18.048 5.077-34.731 14.549-48.512 27.819l-3.115 3.072c-4.224 4.181-8.875 7.253-13.867 9.301-5.205 2.133-10.752 3.243-16.384 3.243s-11.179-1.067-16.384-3.243c-4.949-2.048-9.643-5.12-13.824-9.259-4.224-4.224-7.253-8.917-9.344-13.867-2.133-5.205-3.243-10.752-3.243-16.384s1.067-11.179 3.243-16.384c2.048-4.949 5.12-9.643 9.259-13.824l2.603-2.603c9.643-9.813 16.981-20.651 22.272-32.213 5.461-11.947 8.747-24.619 9.813-37.376 1.579-18.645-1.451-37.632-9.045-55.168-5.12-11.989-12.288-22.827-20.907-32.128-8.875-9.6-19.243-17.408-30.592-23.339-15.829-8.235-33.493-12.715-51.669-12.928l-7.595 0.085c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299s1.152-11.349 3.2-16.299c2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h3.84c14.293-0.341 27.093-2.944 38.997-7.509 12.245-4.736 23.467-11.52 33.152-19.925 13.995-12.117 24.96-27.563 31.787-45.141 5.077-11.819 7.979-24.149 8.875-36.437 0.939-13.099-0.469-26.112-3.925-38.443-5.077-18.133-14.549-34.816-27.819-48.555l-3.115-3.115c-4.139-4.181-7.211-8.832-9.259-13.824-2.176-5.163-3.243-10.752-3.285-16.341s1.067-11.179 3.243-16.384c2.048-4.949 5.12-9.643 9.259-13.824 4.224-4.181 8.875-7.253 13.867-9.301 5.163-2.176 10.752-3.243 16.341-3.285s11.179 1.067 16.384 3.243c4.949 2.048 9.643 5.12 13.824 9.259l2.603 2.603c9.813 9.643 20.651 16.981 32.213 22.272 11.947 5.461 24.619 8.747 37.376 9.813 15.787 1.365 31.787-0.597 46.976-5.845 4.096-0.512 7.979-1.579 11.477-3.115 12.16-5.205 22.997-12.331 32.299-20.949 9.6-8.875 17.408-19.243 23.339-30.592 8.192-15.787 12.672-33.493 12.885-51.627v-7.723c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2s11.349 1.152 16.299 3.2c5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v3.84c0.043 13.397 2.389 26.155 6.699 38.059 4.437 12.288 10.923 23.552 19.072 33.408 11.349 13.781 25.899 24.789 42.496 32.043 11.989 5.291 24.704 8.363 37.376 9.259 13.099 0.939 26.112-0.469 38.443-3.925 18.048-5.077 34.731-14.549 48.512-27.819l3.115-3.072c4.139-4.181 8.832-7.211 13.824-9.301 5.205-2.133 10.752-3.243 16.384-3.243s11.179 1.067 16.384 3.243c4.949 2.048 9.643 5.12 13.824 9.259 4.224 4.224 7.253 8.917 9.344 13.867 2.133 5.205 3.243 10.752 3.243 16.384s-1.067 11.179-3.243 16.384c-2.048 4.949-5.12 9.643-9.259 13.824l-2.603 2.603c-9.643 9.813-16.981 20.651-22.272 32.213-5.461 11.947-8.747 24.619-9.813 37.376-1.365 15.787 0.597 31.787 5.845 46.976 0.512 4.096 1.579 7.979 3.115 11.477 5.205 12.16 12.331 22.997 20.949 32.299 8.875 9.557 19.243 17.408 30.592 23.339 15.829 8.235 33.493 12.715 51.669 12.928l7.637-0.085c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299s-1.152 11.349-3.2 16.299c-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-3.84c-13.397 0.043-26.155 2.389-38.059 6.699-12.288 4.437-23.552 10.923-33.408 19.072-13.781 11.349-24.789 25.899-32 42.368z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "settings" + ], + "grid": 0 + }, + { + "id": 204, + "paths": [ + "M691.84 772.139c1.067-1.408 2.048-2.859 2.944-4.437 0.853-1.493 1.621-2.987 2.304-4.523 3.115-4.608 6.656-8.917 10.581-12.843 7.893-7.893 17.323-14.251 27.733-18.56 9.984-4.139 20.949-6.443 32.597-6.443s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597s-2.304 22.613-6.443 32.64c-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c0.811-2.005 1.749-3.968 2.688-5.888zM695.509 258.389c-0.384-0.725-0.768-1.451-1.152-2.133-0.427-0.725-0.853-1.408-1.323-2.091-1.451-2.645-2.773-5.376-3.925-8.192-4.139-10.027-6.443-20.992-6.443-32.64s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597s-2.304 22.613-6.443 32.64c-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56-4.608-4.608-8.704-9.728-12.16-15.275zM328.491 466.944c0.384 0.725 0.768 1.451 1.152 2.133 0.427 0.725 0.853 1.408 1.323 2.091 1.451 2.645 2.773 5.376 3.925 8.192 4.139 10.027 6.443 20.992 6.443 32.64s-2.304 22.613-6.443 32.64c-1.152 2.816-2.475 5.547-3.925 8.192-0.469 0.683-0.896 1.408-1.323 2.133-0.427 0.683-0.811 1.408-1.152 2.091-3.456 5.547-7.552 10.667-12.16 15.275-7.893 7.893-17.323 14.251-27.733 18.56-9.984 4.139-20.949 6.443-32.597 6.443s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56 4.608 4.608 8.704 9.728 12.16 15.275zM603.733 259.755l-226.475 132.139c-0.171-0.213-0.384-0.384-0.597-0.597-15.701-15.659-34.475-28.373-55.381-37.035-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035 0.213-0.213 0.384-0.384 0.597-0.597l226.517 132.011c-4.181 14.805-6.443 30.421-6.443 46.549 0 23.040 4.608 45.099 12.928 65.28 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035-0.128 0.128-0.299 0.299-0.427 0.427l-226.645-132.011c4.181-14.763 6.4-30.336 6.4-46.379s-2.219-31.616-6.4-46.421l226.475-132.181c0.171 0.213 0.384 0.384 0.597 0.597 15.701 15.701 34.475 28.373 55.381 37.035 20.181 8.363 42.24 12.971 65.28 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323s-4.608-45.099-12.928-65.28c-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28 0 16.043 2.219 31.616 6.4 46.421z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "share-2" + ], + "grid": 0 + }, + { + "id": 205, + "paths": [ + "M128 512v341.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v341.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-341.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 188.331v451.669c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-451.669l97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-170.667-170.667c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.643 5.077-13.653 9.088-0.043 0.043-0.128 0.085-0.171 0.171l-170.667 170.667c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "share" + ], + "grid": 0 + }, + { + "id": 206, + "paths": [ + "M880.853 609.963c10.283-33.109 15.275-66.859 15.147-98.133v-298.496c0-18.176-11.392-33.707-27.691-39.936l-341.333-128c-10.069-3.755-20.651-3.499-29.909 0l-134.827 50.347c-22.059 8.192-33.28 32.768-25.045 54.869s32.811 33.28 54.912 25.045l119.851-44.757 298.709 112v269.269c0.085 22.272-3.499 47.403-11.307 72.533-6.997 22.485 5.589 46.421 28.117 53.376s46.421-5.589 53.376-28.117zM213.333 273.664l479.189 479.189c-50.389 51.2-111.445 98.389-180.565 137.259-16.213-9.216-41.429-24.32-70.571-44.715-28.971-20.309-61.525-45.568-92.8-75.136-36.224-34.261-70.016-73.6-94.72-116.864-3.371-5.845-6.528-11.776-9.515-17.792-10.581-21.163-18.816-43.051-24.149-65.536-4.437-18.859-6.869-38.187-6.869-58.069zM12.501 72.832l120.491 120.533c-3.2 6.059-4.949 12.885-4.992 19.968v298.667c0 26.837 3.285 52.779 9.131 77.653 6.997 29.653 17.664 57.728 30.848 84.096 3.712 7.467 7.637 14.763 11.776 21.973 30.037 52.608 69.845 98.389 110.165 136.533 34.987 33.109 70.955 60.971 102.485 83.072 56.064 39.253 99.285 60.928 100.48 61.525 12.715 6.357 27.136 5.76 38.997-0.427 84.693-44.672 159.573-100.949 220.928-163.2l198.315 198.315c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.624-938.709c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shield-off" + ], + "grid": 0 + }, + { + "id": 207, + "paths": [ + "M512 890.155c-16.213-9.173-41.472-24.32-70.613-44.757-28.971-20.309-61.525-45.568-92.8-75.136-36.224-34.261-70.016-73.6-94.72-116.864-3.371-5.845-6.528-11.776-9.515-17.792-10.581-21.163-18.816-43.051-24.149-65.536-4.437-18.859-6.869-38.187-6.869-58.069v-269.099l298.667-112 298.667 112v269.099c0 19.883-2.432 39.211-6.869 58.069-5.291 22.485-13.525 44.373-24.149 65.536-2.987 5.973-6.187 11.904-9.515 17.792-24.704 43.264-58.496 82.603-94.72 116.864-31.275 29.568-63.787 54.869-92.8 75.136-29.184 20.395-54.4 35.541-70.613 44.757zM531.072 976.811c1.195-0.597 44.416-22.272 100.48-61.525 31.573-22.101 67.541-49.963 102.485-83.072 40.32-38.144 80.128-83.925 110.165-136.533 4.096-7.168 8.021-14.507 11.776-21.973 13.184-26.368 23.851-54.443 30.848-84.096 5.888-24.832 9.173-50.773 9.173-77.611v-298.667c0-18.176-11.392-33.707-27.691-39.936l-341.333-128c-10.069-3.797-20.693-3.499-29.952 0l-341.333 128c-17.024 6.357-27.563 22.485-27.691 39.936v298.667c0 26.837 3.285 52.779 9.131 77.653 6.997 29.653 17.664 57.728 30.848 84.096 3.712 7.467 7.637 14.763 11.776 21.973 30.037 52.608 69.845 98.389 110.165 136.533 34.987 33.109 70.955 60.971 102.485 83.072 56.064 39.253 99.285 60.928 100.48 61.525 12.459 6.229 26.453 5.803 38.144 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shield" + ], + "grid": 0 + }, + { + "id": 208, + "paths": [ + "M810.667 213.333h-597.333l64-85.333h469.333zM929.877 230.059l-127.744-170.325c-8.363-11.136-21.077-17.024-34.133-17.067h-512c-13.909 0-26.283 6.656-34.133 17.067l-128 170.667c-1.792 2.389-3.285 4.864-4.48 7.467-2.773 5.803-4.096 12.032-4.053 18.133v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-9.344-3.029-18.005-8.064-24.96-0.171-0.213-0.299-0.427-0.469-0.64zM170.667 298.667h682.667v554.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299zM640 426.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939 0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667c0 28.8 5.717 56.405 16.171 81.579 10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621 0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shopping-bag" + ], + "grid": 0 + }, + { + "id": 209, + "paths": [ + "M469.333 896c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM938.667 896c0-11.477-2.304-22.528-6.485-32.64-4.352-10.496-10.667-19.84-18.517-27.691s-17.237-14.165-27.691-18.517c-10.112-4.181-21.163-6.485-32.64-6.485s-22.528 2.304-32.64 6.485c-10.496 4.352-19.84 10.667-27.691 18.517s-14.165 17.237-18.517 27.691c-4.181 10.112-6.485 21.163-6.485 32.64s2.304 22.528 6.485 32.64c4.352 10.496 10.667 19.84 18.517 27.691s17.237 14.165 27.691 18.517c10.112 4.181 21.163 6.485 32.64 6.485s22.528-2.304 32.64-6.485c10.496-4.352 19.84-10.667 27.691-18.517s14.165-17.237 18.517-27.691c4.181-10.112 6.485-21.163 6.485-32.64zM308.096 298.667h621.653l-58.496 306.816c-0.981 4.821-2.731 9.301-5.077 13.355-2.432 4.139-5.504 7.808-9.088 10.923-3.925 3.371-8.448 6.059-13.312 7.808-4.693 1.707-9.813 2.603-16.043 2.475h-415.317c-4.907 0.085-9.685-0.683-14.123-2.133-4.565-1.493-8.789-3.712-12.587-6.528-4.139-3.115-7.723-6.955-10.453-11.307-2.645-4.224-4.608-9.003-5.717-14.421zM42.667 85.333h135.68l35.499 177.28c0.171 1.195 0.427 2.432 0.725 3.584l71.296 356.139c3.115 15.659 9.003 30.208 17.152 43.136 8.405 13.355 19.115 24.875 31.488 34.133 11.264 8.448 23.893 15.019 37.376 19.413 13.227 4.309 27.349 6.528 41.771 6.315h414.123c15.189 0.299 30.677-2.347 45.056-7.552 14.848-5.376 28.373-13.355 40.064-23.467 10.624-9.173 19.755-20.139 26.965-32.384 7.040-12.032 12.203-25.301 15.061-39.467l68.352-358.485c4.395-23.168-10.752-45.483-33.92-49.92-2.816-0.512-5.589-0.768-8.021-0.725h-690.347l-35.84-179.029c-4.011-19.712-21.205-34.304-41.813-34.304h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shopping-cart" + ], + "grid": 0 + }, + { + "id": 210, + "paths": [ + "M200.832 883.499l652.501-652.501v110.336c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-213.333c0-0.128 0-0.213 0-0.341-0.043-5.632-1.195-11.008-3.2-15.957-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-213.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h110.336l-652.501 652.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM609.835 670.165l183.168 183.168h-110.336c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h213.333c0.128 0 0.213 0 0.341 0 5.632-0.043 11.008-1.195 15.957-3.2 10.453-4.309 18.816-12.715 23.168-23.168 2.048-4.907 3.157-10.283 3.2-15.957 0-0.128 0-0.213 0-0.341v-213.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v110.336l-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM140.501 200.832l213.333 213.333c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-213.333-213.333c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shuffle" + ], + "grid": 0 + }, + { + "id": 211, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM426.667 853.333v-682.667h384c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2zM341.333 170.667v682.667h-128c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sidebar" + ], + "grid": 0 + }, + { + "id": 212, + "paths": [ + "M784 886.656c7.253 5.803 16.555 9.344 26.667 9.344 23.552 0 42.667-19.115 42.667-42.667v-682.667c0.043-9.301-3.029-18.731-9.344-26.667-14.72-18.389-41.557-21.376-59.989-6.656l-426.667 341.333c-2.261 1.792-4.608 4.053-6.656 6.656-14.72 18.389-11.733 45.269 6.656 59.989zM768 764.544l-315.691-252.544 315.691-252.544zM256 810.667v-597.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v597.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "skip-back" + ], + "grid": 0 + }, + { + "id": 213, + "paths": [ + "M240 137.344c-7.253-5.803-16.555-9.344-26.667-9.344-23.552 0-42.667 19.115-42.667 42.667v682.667c-0.043 9.301 3.029 18.731 9.344 26.667 14.72 18.389 41.557 21.376 59.989 6.656l426.667-341.333c2.261-1.792 4.608-4.053 6.656-6.656 14.72-18.389 11.733-45.269-6.656-59.989zM256 259.456l315.691 252.544-315.691 252.544zM768 213.333v597.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-597.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "skip-forward" + ], + "grid": 0 + }, + { + "id": 214, + "paths": [ + "M618.667 469.333c14.379 0 28.203-2.859 40.875-8.107 13.056-5.419 24.789-13.312 34.56-23.125s17.707-21.504 23.125-34.56c5.248-12.672 8.107-26.496 8.107-40.875v-213.333c0-14.379-2.859-28.203-8.107-40.875-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107s-28.203 2.859-40.875 8.107c-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875v213.333c0 14.379 2.859 28.203 8.107 40.875 5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107zM618.667 384c-3.029 0-5.76-0.597-8.235-1.621-2.56-1.067-4.907-2.645-6.912-4.608s-3.541-4.309-4.608-6.912c-0.981-2.432-1.579-5.163-1.579-8.192v-213.333c0-3.029 0.597-5.76 1.621-8.235 1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579s5.76 0.597 8.235 1.621c2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192v213.333c0 3.029-0.597 5.76-1.621 8.235-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579zM874.667 469.333c14.379 0 28.203-2.859 40.875-8.107 13.056-5.419 24.789-13.312 34.56-23.125 9.813-9.813 17.707-21.504 23.125-34.56 5.248-12.672 8.107-26.496 8.107-40.875s-2.859-28.203-8.107-40.875c-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107s-28.203 2.859-40.875 8.107c-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875v64c0 23.552 19.115 42.667 42.667 42.667zM874.667 384h-21.333v-21.333c0-3.029 0.597-5.76 1.621-8.235 1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579s5.76 0.597 8.235 1.621c2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192s-0.597 5.76-1.621 8.235c-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579zM405.333 554.667c-14.379 0-28.203 2.859-40.875 8.107-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875v213.333c0 14.379 2.859 28.203 8.107 40.875 5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107s28.203-2.859 40.875-8.107c13.056-5.419 24.789-13.312 34.56-23.125 9.813-9.813 17.707-21.504 23.125-34.56 5.248-12.672 8.107-26.496 8.107-40.875v-213.333c0-14.379-2.859-28.203-8.107-40.875-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107zM405.333 640c3.029 0 5.76 0.597 8.235 1.621 2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192v213.333c0 3.029-0.597 5.76-1.621 8.235-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579s-5.76-0.597-8.235-1.621c-2.56-1.067-4.907-2.645-6.912-4.608s-3.541-4.309-4.608-6.912c-0.981-2.432-1.579-5.163-1.579-8.192v-213.333c0-3.029 0.597-5.76 1.621-8.235 1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579zM149.333 554.667c-14.379 0-28.203 2.859-40.875 8.107-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875s2.859 28.203 8.107 40.875c5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107s28.203-2.859 40.875-8.107c13.056-5.419 24.789-13.312 34.56-23.125s17.707-21.504 23.125-34.56c5.248-12.672 8.107-26.496 8.107-40.875v-64c0-23.552-19.115-42.667-42.667-42.667zM149.333 640h21.333v21.333c0 3.029-0.597 5.76-1.621 8.235-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579s-5.76-0.597-8.235-1.621c-2.56-1.067-4.907-2.645-6.912-4.608s-3.541-4.309-4.608-6.912c-0.981-2.432-1.579-5.163-1.579-8.192s0.597-5.76 1.621-8.235c1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579zM554.667 618.667c0 14.379 2.859 28.203 8.107 40.875 5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107h213.333c14.379 0 28.203-2.859 40.875-8.107 13.056-5.419 24.789-13.312 34.56-23.125 9.813-9.813 17.707-21.504 23.125-34.56 5.248-12.672 8.107-26.496 8.107-40.875s-2.859-28.203-8.107-40.875c-5.419-13.056-13.312-24.789-23.125-34.56-9.813-9.813-21.504-17.707-34.56-23.125-12.672-5.248-26.496-8.107-40.875-8.107h-213.333c-14.379 0-28.203 2.859-40.875 8.107-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875zM640 618.667c0-3.029 0.597-5.76 1.621-8.235 1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579h213.333c3.029 0 5.76 0.597 8.235 1.621 2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192s-0.597 5.76-1.621 8.235c-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579h-213.333c-3.029 0-5.76-0.597-8.235-1.621-2.56-1.067-4.907-2.645-6.912-4.608s-3.541-4.309-4.608-6.912c-0.981-2.432-1.579-5.163-1.579-8.192zM661.333 853.333c3.029 0 5.76 0.597 8.235 1.621 2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192s-0.597 5.76-1.621 8.235c-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.541-6.912 4.608c-2.432 0.981-5.163 1.579-8.192 1.579s-5.76-0.597-8.235-1.621c-2.56-1.067-4.907-2.645-6.912-4.608s-3.541-4.309-4.608-6.912c-0.981-2.432-1.579-5.163-1.579-8.192v-21.333zM661.333 768h-64c-23.552 0-42.667 19.115-42.667 42.667v64c0 14.379 2.859 28.203 8.107 40.875 5.419 13.056 13.312 24.789 23.125 34.56 9.813 9.813 21.504 17.707 34.56 23.125 12.672 5.248 26.496 8.107 40.875 8.107s28.203-2.859 40.875-8.107c13.056-5.419 24.789-13.312 34.56-23.125s17.707-21.504 23.125-34.56c5.248-12.672 8.107-26.496 8.107-40.875s-2.859-28.203-8.107-40.875c-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107zM384 405.333c0 3.029-0.597 5.76-1.621 8.235-1.067 2.56-2.645 4.907-4.608 6.912s-4.309 3.499-6.869 4.565c-2.475 1.024-5.205 1.621-8.235 1.621h-213.333c-3.029 0-5.76-0.597-8.235-1.621-2.56-1.067-4.907-2.645-6.912-4.608s-3.499-4.309-4.565-6.869c-1.024-2.475-1.621-5.205-1.621-8.235s0.597-5.76 1.621-8.235c1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579h213.333c3.029 0 5.76 0.597 8.235 1.621 2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192zM469.333 405.333c0-14.379-2.859-28.203-8.107-40.875-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107h-213.333c-14.379 0-28.203 2.859-40.875 8.107-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875s2.859 28.203 8.107 40.875c5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107h213.333c14.379 0 28.203-2.859 40.875-8.107 13.056-5.419 24.789-13.312 34.56-23.125s17.707-21.504 23.125-34.56c5.248-12.672 8.107-26.496 8.107-40.875zM362.667 170.667c-3.029 0-5.76-0.597-8.235-1.621-2.56-1.067-4.907-2.603-6.869-4.608s-3.541-4.309-4.608-6.869c-1.024-2.475-1.621-5.205-1.621-8.235s0.597-5.76 1.621-8.235c1.067-2.56 2.645-4.907 4.608-6.912s4.309-3.541 6.912-4.608c2.432-0.981 5.163-1.579 8.192-1.579s5.76 0.597 8.235 1.621c2.56 1.067 4.907 2.645 6.912 4.608s3.541 4.309 4.608 6.912c0.981 2.432 1.579 5.163 1.579 8.192v21.333zM362.667 256h64c23.552 0 42.667-19.115 42.667-42.667v-64c0-14.379-2.859-28.203-8.107-40.875-5.419-13.056-13.312-24.789-23.125-34.56s-21.504-17.707-34.56-23.125c-12.672-5.248-26.496-8.107-40.875-8.107s-28.203 2.859-40.875 8.107c-13.056 5.419-24.789 13.312-34.56 23.125s-17.707 21.504-23.125 34.56c-5.248 12.672-8.107 26.496-8.107 40.875s2.859 28.203 8.107 40.875c5.419 13.056 13.312 24.789 23.125 34.56s21.504 17.707 34.56 23.125c12.672 5.248 26.496 8.107 40.875 8.107z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "slack" + ], + "grid": 0 + }, + { + "id": 215, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM812.032 751.701l-539.733-539.733c28.032-22.443 59.264-41.003 92.843-54.912 45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859s-10.368 101.675-29.056 146.859c-13.909 33.621-32.469 64.811-54.912 92.843zM211.968 272.299l539.733 539.733c-28.032 22.443-59.264 41.003-92.843 54.912-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c13.909-33.621 32.469-64.811 54.912-92.843z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "slash" + ], + "grid": 0 + }, + { + "id": 216, + "paths": [ + "M213.333 426.667v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v298.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM554.667 896v-384c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v384c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM896 512v-384c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v384c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM42.667 640h85.333v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM384 384h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v170.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM725.333 725.333h85.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sliders" + ], + "grid": 0 + }, + { + "id": 217, + "paths": [ + "M298.667 42.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h426.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-682.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM298.667 128h426.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v682.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-426.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM554.667 768c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "smartphone" + ], + "grid": 0 + }, + { + "id": 218, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM307.2 622.933c1.28 1.749 14.677 19.285 38.741 39.381 14.379 11.989 32.939 25.173 55.339 36.395 16.341 8.149 34.816 15.317 55.296 20.181 17.195 4.053 35.712 6.443 55.424 6.443s38.229-2.389 55.424-6.443c20.48-4.821 38.997-11.989 55.296-20.181 22.4-11.221 40.96-24.405 55.339-36.395 24.064-20.053 37.419-37.632 38.741-39.381 14.123-18.859 10.325-45.611-8.533-59.733-18.816-14.080-45.44-10.325-59.605 8.363-0.213 0.256-2.901 3.712-7.851 8.96-4.267 4.48-10.112 10.197-17.408 16.299-10.368 8.661-23.424 17.877-38.827 25.6-11.179 5.589-23.467 10.283-36.779 13.44-11.136 2.603-23.083 4.139-35.797 4.139s-24.661-1.536-35.797-4.181c-13.312-3.157-25.6-7.851-36.779-13.44-15.232-7.595-28.203-16.725-38.528-25.344-24.32-23.893-25.515-25.429-25.557-25.472-14.165-18.688-40.789-22.443-59.605-8.363-18.859 14.123-22.656 40.875-8.533 59.733zM400.896 597.035c-10.667-8.875-18.005-16.683-22.016-21.248z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "smile" + ], + "grid": 0 + }, + { + "id": 219, + "paths": [ + "M256 42.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h512c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-682.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM256 128h512c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v682.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM725.333 597.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM640 597.333c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM554.667 256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "speaker" + ], + "grid": 0 + }, + { + "id": 220, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM213.333 170.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "square" + ], + "grid": 0 + }, + { + "id": 221, + "paths": [ + "M550.272 66.432c-3.925-8.064-10.581-15.019-19.371-19.371-21.12-10.411-46.72-1.749-57.131 19.371l-121.941 246.997-272.683 39.893c-8.875 1.237-17.536 5.419-24.363 12.416-16.469 16.896-16.085 43.904 0.768 60.331l197.248 192.128-46.549 271.445c-1.536 8.832-0.256 18.389 4.309 27.051 10.965 20.864 36.779 28.885 57.643 17.92l243.797-128.213 243.84 128.213c7.936 4.224 17.408 5.931 27.051 4.309 23.211-3.968 38.827-26.027 34.859-49.28l-46.549-271.445 197.248-192.128c6.443-6.229 11.051-14.677 12.459-24.405 3.413-23.296-12.715-44.971-36.053-48.384l-272.64-39.851zM512 181.717l93.568 189.611c6.443 13.013 18.603 21.291 32.085 23.339l209.323 30.592-151.424 147.499c-10.411 10.155-14.549 24.277-12.288 37.76l35.712 208.341-187.136-98.432c-12.843-6.741-27.605-6.315-39.723 0l-187.136 98.432 35.712-208.341c2.475-14.336-2.517-28.203-12.288-37.76l-151.424-147.499 209.365-30.635c14.336-2.091 25.984-11.093 32.085-23.296z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "star" + ], + "grid": 0 + }, + { + "id": 222, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM384 341.333c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h256c23.552 0 42.667-19.115 42.667-42.667v-256c0-23.552-19.115-42.667-42.667-42.667zM426.667 426.667h170.667v170.667h-170.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stop-circle" + ], + "grid": 0 + }, + { + "id": 223, + "paths": [ + "M768 512c0-34.603-6.869-67.712-19.413-97.92-12.971-31.36-32-59.52-55.595-83.115-23.552-23.552-51.755-42.581-83.115-55.595-30.165-12.501-63.275-19.371-97.877-19.371s-67.712 6.869-97.92 19.413c-31.36 12.971-59.52 32-83.115 55.552s-42.581 51.755-55.552 83.115c-12.544 30.208-19.413 63.317-19.413 97.92s6.869 67.712 19.413 97.92c12.971 31.36 32 59.52 55.595 83.115 23.552 23.552 51.755 42.581 83.115 55.595 30.165 12.501 63.275 19.371 97.877 19.371s67.712-6.869 97.92-19.413c31.36-12.971 59.52-32 83.115-55.595s42.581-51.755 55.595-83.115c12.501-30.165 19.371-63.275 19.371-97.877zM682.667 512c0 23.211-4.608 45.227-12.928 65.237-8.619 20.864-21.333 39.637-37.077 55.424s-34.56 28.459-55.424 37.077c-20.011 8.32-42.027 12.928-65.237 12.928s-45.227-4.608-65.237-12.928c-20.864-8.619-39.637-21.333-55.424-37.077s-28.459-34.56-37.077-55.424c-8.32-20.011-12.928-42.027-12.928-65.237s4.608-45.227 12.928-65.237c8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928s45.227 4.608 65.237 12.928c20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237zM469.333 42.667v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 896v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM149.888 210.219l60.587 60.587c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-60.587-60.587c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM753.195 813.525l60.587 60.587c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-60.587-60.587c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM42.667 554.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM896 554.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM210.219 874.112l60.587-60.587c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-60.587 60.587c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM813.525 270.805l60.587-60.587c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-60.587 60.587c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sun" + ], + "grid": 0 + }, + { + "id": 224, + "paths": [ + "M768 768c0-34.603-6.869-67.712-19.413-97.92-12.971-31.36-32-59.52-55.595-83.115-23.552-23.552-51.755-42.581-83.115-55.595-30.165-12.501-63.275-19.371-97.877-19.371s-67.712 6.869-97.92 19.413c-31.36 12.971-59.52 32-83.115 55.595s-42.581 51.755-55.595 83.115c-12.501 30.165-19.371 63.275-19.371 97.877 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667c0-23.211 4.608-45.227 12.928-65.237 8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928s45.227 4.608 65.237 12.928c20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM149.888 466.219l60.587 60.587c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-60.587-60.587c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM42.667 810.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM896 810.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM813.525 526.805l60.587-60.587c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-60.587 60.587c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM981.333 896h-938.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h938.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM371.499 286.165l97.835-97.835v195.669c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-195.669l97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-170.667-170.667c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.643 5.077-13.653 9.088-0.043 0.043-0.128 0.085-0.171 0.171l-170.667 170.667c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sunrise" + ], + "grid": 0 + }, + { + "id": 225, + "paths": [ + "M768 768c0-34.603-6.869-67.712-19.413-97.92-12.971-31.36-32-59.52-55.595-83.115-23.552-23.552-51.755-42.581-83.115-55.595-30.165-12.501-63.275-19.371-97.877-19.371s-67.712 6.869-97.92 19.413c-31.36 12.971-59.52 32-83.115 55.595s-42.581 51.755-55.595 83.115c-12.501 30.165-19.371 63.275-19.371 97.877 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667c0-23.211 4.608-45.227 12.928-65.237 8.619-20.864 21.333-39.637 37.077-55.424s34.56-28.459 55.424-37.077c20.011-8.32 42.027-12.928 65.237-12.928s45.227 4.608 65.237 12.928c20.864 8.619 39.637 21.333 55.424 37.077s28.459 34.56 37.077 55.424c8.32 20.011 12.928 42.027 12.928 65.237 0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM149.888 466.219l60.587 60.587c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-60.587-60.587c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM42.667 810.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM896 810.667h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM813.525 526.805l60.587-60.587c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-60.587 60.587c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM981.333 896h-938.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h938.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM652.501 183.168l-97.835 97.835v-195.669c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v195.669l-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l170.667 170.667c0.085 0.085 0.171 0.171 0.256 0.256 4.053 3.968 8.661 6.955 13.568 9.003 5.12 2.133 10.624 3.2 16.085 3.243 0.171 0 0.341 0 0.469 0 5.461-0.043 10.965-1.109 16.085-3.243 4.949-2.048 9.557-5.035 13.568-9.003 0.085-0.085 0.171-0.171 0.256-0.256l170.667-170.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sunset" + ], + "grid": 0 + }, + { + "id": 226, + "paths": [ + "M768 981.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-682.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-512c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v682.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685zM768 896h-512c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-682.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h512c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v682.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2zM554.667 768c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tablet" + ], + "grid": 0 + }, + { + "id": 227, + "paths": [ + "M908.672 602.325c12.203-12.288 21.419-26.368 27.605-41.429 6.443-15.616 9.685-32.213 9.685-48.768 0-16.597-3.2-33.237-9.643-48.896-6.229-15.104-15.445-29.227-27.648-41.515l-366.507-366.507c-7.723-7.765-18.389-12.544-30.165-12.544h-426.667c-23.552 0-42.667 19.115-42.667 42.667v426.667c0 10.923 4.181 21.845 12.501 30.208l366.592 366.165c12.203 12.203 26.368 21.461 41.515 27.733 15.701 6.485 32.384 9.728 49.024 9.728s33.323-3.243 49.024-9.771c15.147-6.272 29.269-15.573 41.472-27.776zM848.341 541.995l-305.92 305.92c-4.139 4.181-8.832 7.211-13.824 9.301-5.205 2.133-10.752 3.243-16.384 3.243s-11.179-1.067-16.384-3.243c-4.949-2.048-9.643-5.12-13.824-9.259l-354.005-353.664v-366.293h366.336l354.005 354.005c4.011 4.053 7.083 8.747 9.088 13.696 2.133 5.163 3.2 10.752 3.2 16.341s-1.109 11.136-3.243 16.299c-2.048 4.949-5.077 9.643-9.088 13.653zM341.333 298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tag" + ], + "grid": 0 + }, + { + "id": 228, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM810.667 512c0-40.363-8.021-78.976-22.613-114.219-15.147-36.565-37.333-69.461-64.853-96.981s-60.373-49.707-96.981-64.853c-35.243-14.592-73.856-22.613-114.219-22.613s-78.976 8.021-114.219 22.613c-36.608 15.189-69.461 37.376-96.981 64.853s-49.664 60.373-64.853 96.981c-14.592 35.243-22.613 73.856-22.613 114.219s8.021 78.976 22.613 114.219c15.147 36.565 37.333 69.461 64.853 96.981s60.373 49.707 96.981 64.853c35.243 14.592 73.856 22.613 114.219 22.613s78.976-8.021 114.219-22.613c36.565-15.147 69.461-37.333 96.981-64.853s49.707-60.373 64.853-96.981c14.592-35.243 22.613-73.856 22.613-114.219zM725.333 512c0 28.971-5.76 56.491-16.128 81.579-10.795 26.069-26.624 49.579-46.336 69.291s-43.221 35.541-69.291 46.336c-25.088 10.368-52.608 16.128-81.579 16.128s-56.491-5.76-81.579-16.128c-26.069-10.795-49.579-26.624-69.291-46.336s-35.541-43.221-46.336-69.291c-10.368-25.088-16.128-52.608-16.128-81.579s5.76-56.491 16.128-81.579c10.795-26.069 26.624-49.579 46.336-69.291s43.221-35.541 69.291-46.336c25.088-10.368 52.608-16.128 81.579-16.128s56.491 5.76 81.579 16.128c26.069 10.795 49.579 26.624 69.291 46.336s35.541 43.221 46.336 69.291c10.368 25.088 16.128 52.608 16.128 81.579zM640 512c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685s-33.835 3.456-48.981 9.728c-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939s3.456 33.835 9.728 48.981c6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685s33.835-3.456 48.981-9.728c15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939zM554.667 512c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2s-11.349-1.152-16.299-3.2c-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299s1.152-11.349 3.2-16.299c2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2s11.349 1.152 16.299 3.2c5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "target" + ], + "grid": 0 + }, + { + "id": 229, + "paths": [ + "M200.832 755.499l256-256c16.683-16.683 16.683-43.691 0-60.331l-256-256c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l225.835 225.835-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM512 853.333h341.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-341.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "terminal" + ], + "grid": 0 + }, + { + "id": 230, + "paths": [ + "M554.667 629.76c0.085 14.037 6.869 27.563 19.328 35.712 14.891 9.728 29.269 23.637 40.875 40.96 11.264 16.896 18.645 35.115 22.315 53.76 3.84 19.371 3.755 39.211-0.043 58.368s-11.349 37.504-22.315 53.931c-10.539 15.787-24.32 29.824-41.216 41.131s-35.115 18.645-53.76 22.315c-19.371 3.84-39.211 3.755-58.368-0.043s-37.504-11.349-53.931-22.315c-15.787-10.539-29.824-24.32-41.131-41.216-11.264-16.896-18.645-35.115-22.315-53.76-3.84-19.371-3.755-39.211 0.043-58.368s11.349-37.504 22.315-53.931c10.539-15.787 24.32-29.824 41.216-41.131 11.52-7.68 18.987-20.693 18.987-35.413v-480.427c0-8.747 1.749-16.981 4.821-24.448 3.243-7.808 7.979-14.848 13.909-20.779s13.013-10.667 20.779-13.909c7.509-3.115 15.744-4.864 24.491-4.864s16.981 1.749 24.448 4.821c7.808 3.243 14.848 7.979 20.779 13.909s10.667 13.013 13.909 20.779c3.115 7.509 4.864 15.744 4.864 24.491zM640 608.256v-458.923c0-20.139-4.011-39.467-11.307-57.131-7.595-18.304-18.688-34.731-32.427-48.469s-30.165-24.832-48.469-32.427c-17.664-7.296-36.992-11.307-57.131-11.307s-39.467 4.011-57.131 11.307c-18.304 7.595-34.731 18.688-48.469 32.427s-24.832 30.165-32.427 48.469c-7.296 17.664-11.307 36.992-11.307 57.131v459.051c-17.963 14.805-33.323 31.872-45.781 50.56-17.28 25.856-29.099 54.699-35.029 84.693s-6.101 61.141-0.043 91.648c5.845 29.397 17.451 58.155 35.072 84.523s39.723 48.085 64.64 64.725c25.856 17.28 54.699 29.099 84.693 35.029s61.141 6.101 91.648 0.043c29.397-5.845 58.155-17.451 84.523-35.072s48.085-39.723 64.725-64.64c17.28-25.856 29.099-54.699 35.029-84.693 5.973-29.952 6.101-61.141 0.043-91.648-5.845-29.397-17.451-58.155-35.072-84.523-13.099-19.584-28.715-36.693-45.781-50.773z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "thermometer" + ], + "grid": 0 + }, + { + "id": 231, + "paths": [ + "M768 512v-384h71.253c8.021-0.128 14.891 1.109 21.163 3.456 6.528 2.432 12.544 6.101 17.707 10.709 4.907 4.395 9.045 9.643 12.16 15.531 2.645 4.992 4.608 10.496 5.675 16.299v292.053c-1.109 6.272-3.243 12.032-6.144 17.195-3.413 6.101-7.893 11.477-13.269 15.872-5.077 4.181-10.923 7.467-17.195 9.685-6.101 2.133-12.672 3.243-19.541 3.157zM384 682.667v128c0 23.040 4.608 45.099 12.928 65.28 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971 17.28 0 32.171-10.283 38.997-25.344l159.403-358.656h85.589c17.024 0.256 33.536-2.56 48.981-7.936 15.829-5.547 30.421-13.824 43.179-24.32 13.483-11.093 24.96-24.704 33.621-40.235 8.363-15.061 14.123-31.829 16.555-49.749 0.213-1.749 0.341-3.755 0.341-5.76v-298.667c0-1.792-0.128-3.797-0.384-5.845-2.347-16.853-7.595-32.768-15.275-47.189-7.893-14.805-18.261-27.989-30.592-38.997-13.013-11.648-28.203-20.907-44.885-27.136-16.128-6.016-33.621-9.131-50.944-8.832h-595.2c-15.744-0.171-31.445 2.688-45.952 8.149-14.976 5.632-28.587 13.995-40.277 24.448-10.88 9.728-20.139 21.376-27.221 34.389-6.912 12.715-11.733 26.795-14.037 41.643l-58.88 384.085c-2.603 17.109-1.664 34.005 2.261 49.92 4.053 16.512 11.349 31.829 21.248 45.227s22.357 24.917 36.907 33.707c14.037 8.491 29.867 14.379 46.933 16.939 6.997 1.109 14.165 1.621 20.949 1.493zM469.333 640c0-23.552-19.115-42.667-42.667-42.667h-242.304c-1.621 0.043-3.968-0.085-6.528-0.469-5.803-0.896-11.051-2.859-15.659-5.632-4.821-2.901-9.003-6.741-12.331-11.264s-5.76-9.643-7.083-15.104c-1.28-5.205-1.621-10.795-0.725-16.555l58.88-383.915c0.768-5.077 2.432-9.771 4.736-14.037 2.389-4.395 5.504-8.277 9.131-11.563 3.925-3.499 8.448-6.272 13.397-8.107 4.693-1.792 9.856-2.773 15.872-2.688h438.613v417.621l-153.941 346.368c-2.261-0.725-4.48-1.536-6.656-2.432-10.411-4.309-19.797-10.667-27.733-18.56-7.893-7.893-14.251-17.323-18.56-27.733-4.139-9.984-6.443-20.949-6.443-32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "thumbs-down" + ], + "grid": 0 + }, + { + "id": 232, + "paths": [ + "M256 512v384h-85.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-298.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM640 341.333v-128c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928-17.28 0-32.171 10.283-38.997 25.344l-159.403 358.656h-100.267c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v298.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h609.28c15.744 0.171 31.445-2.688 45.952-8.149 14.976-5.632 28.587-13.995 40.277-24.448 10.923-9.771 20.139-21.419 27.221-34.432 6.912-12.715 11.733-26.752 14.080-41.643l58.88-384.085c2.603-17.109 1.664-34.005-2.261-49.92-4.053-16.512-11.349-31.829-21.248-45.227-9.856-13.397-22.357-24.917-36.907-33.707-14.037-8.491-29.867-14.336-46.933-16.939-7.040-1.067-14.208-1.579-20.992-1.451zM554.667 384c0 23.552 19.115 42.667 42.667 42.667h242.304c1.621-0.043 3.968 0.085 6.528 0.469 5.803 0.896 11.051 2.859 15.659 5.632 4.821 2.901 9.003 6.741 12.331 11.264s5.76 9.643 7.083 15.104c1.28 5.205 1.621 10.795 0.725 16.555l-58.88 383.915c-0.768 5.077-2.432 9.771-4.736 14.037-2.389 4.395-5.504 8.277-9.131 11.563-3.925 3.499-8.448 6.272-13.397 8.107-4.736 1.792-9.899 2.731-15.915 2.688h-438.571v-417.621l153.941-346.368c2.261 0.725 4.48 1.536 6.656 2.432 10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.139 9.984 6.443 20.949 6.443 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "thumbs-up" + ], + "grid": 0 + }, + { + "id": 233, + "paths": [ + "M341.333 170.667c-46.165 0-90.283 9.173-130.56 25.856-41.813 17.323-79.36 42.667-110.805 74.112s-56.789 69.035-74.112 110.805c-16.683 40.277-25.856 84.395-25.856 130.56s9.173 90.283 25.856 130.56c17.323 41.813 42.667 79.36 74.112 110.805s69.035 56.789 110.805 74.112c40.277 16.683 84.395 25.856 130.56 25.856h341.333c46.165 0 90.283-9.173 130.56-25.856 41.813-17.323 79.36-42.667 110.805-74.112s56.789-69.035 74.112-110.805c16.683-40.277 25.856-84.395 25.856-130.56s-9.173-90.283-25.856-130.56c-17.323-41.813-42.667-79.36-74.112-110.805s-69.035-56.789-110.805-74.112c-40.277-16.683-84.395-25.856-130.56-25.856zM341.333 256h341.333c34.773 0 67.797 6.912 97.877 19.371 31.275 12.971 59.477 31.957 83.115 55.595s42.667 51.84 55.595 83.115c12.501 30.123 19.413 63.147 19.413 97.92s-6.912 67.797-19.371 97.877c-12.971 31.275-31.957 59.477-55.595 83.115s-51.84 42.667-83.115 55.595c-30.123 12.501-63.147 19.413-97.92 19.413h-341.333c-34.773 0-67.797-6.912-97.877-19.371-31.275-12.971-59.477-31.957-83.115-55.595s-42.667-51.84-55.595-83.115c-12.501-30.123-19.413-63.147-19.413-97.92s6.912-67.797 19.371-97.877c12.971-31.275 31.957-59.477 55.595-83.115s51.84-42.667 83.115-55.595c30.123-12.501 63.147-19.413 97.92-19.413zM512 512c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.517-28.416-55.424-37.077c-20.181-8.32-42.24-12.928-65.28-12.928s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.416 34.517-37.077 55.424c-8.32 20.181-12.928 42.24-12.928 65.28s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM426.667 512c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56s14.251 17.323 18.56 27.733c4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "toggle-left" + ], + "grid": 0 + }, + { + "id": 234, + "paths": [ + "M341.333 170.667c-46.165 0-90.283 9.173-130.56 25.856-41.813 17.323-79.36 42.667-110.805 74.112s-56.789 69.035-74.112 110.805c-16.683 40.277-25.856 84.395-25.856 130.56s9.173 90.283 25.856 130.56c17.323 41.813 42.667 79.36 74.112 110.805s69.035 56.789 110.805 74.112c40.277 16.683 84.395 25.856 130.56 25.856h341.333c46.165 0 90.283-9.173 130.56-25.856 41.813-17.323 79.36-42.667 110.805-74.112s56.789-69.035 74.112-110.805c16.683-40.277 25.856-84.395 25.856-130.56s-9.173-90.283-25.856-130.56c-17.323-41.813-42.667-79.36-74.112-110.805s-69.035-56.789-110.805-74.112c-40.277-16.683-84.395-25.856-130.56-25.856zM341.333 256h341.333c34.773 0 67.797 6.912 97.877 19.371 31.275 12.971 59.477 31.957 83.115 55.595s42.667 51.84 55.595 83.115c12.501 30.123 19.413 63.147 19.413 97.92s-6.912 67.797-19.371 97.877c-12.971 31.275-31.957 59.477-55.595 83.115s-51.84 42.667-83.115 55.595c-30.123 12.501-63.147 19.413-97.92 19.413h-341.333c-34.773 0-67.797-6.912-97.877-19.371-31.275-12.971-59.477-31.957-83.115-55.595s-42.667-51.84-55.595-83.115c-12.501-30.123-19.413-63.147-19.413-97.92s6.912-67.797 19.371-97.877c12.971-31.275 31.957-59.477 55.595-83.115s51.84-42.667 83.115-55.595c30.123-12.501 63.147-19.413 97.92-19.413zM853.333 512c0-23.040-4.608-45.099-12.928-65.28-8.661-20.907-21.333-39.68-37.035-55.381s-34.475-28.373-55.381-37.035c-20.224-8.363-42.283-12.971-65.323-12.971s-45.099 4.608-65.28 12.928c-20.907 8.661-39.68 21.333-55.381 37.035s-28.373 34.475-37.035 55.381c-8.363 20.224-12.971 42.283-12.971 65.323s4.608 45.099 12.928 65.28c8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323zM768 512c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733-7.893 7.893-17.323 14.251-27.733 18.56-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56s-14.251-17.323-18.56-27.733c-4.096-9.984-6.4-20.949-6.4-32.597s2.304-22.613 6.443-32.64c4.309-10.411 10.667-19.797 18.56-27.733s17.323-14.251 27.733-18.56c9.984-4.096 20.949-6.4 32.597-6.4s22.613 2.304 32.64 6.443c10.411 4.309 19.797 10.667 27.733 18.56 7.893 7.893 14.251 17.323 18.56 27.733 4.096 9.984 6.4 20.949 6.4 32.597z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "toggle-right" + ], + "grid": 0 + }, + { + "id": 235, + "paths": [ + "M768 298.667v554.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-426.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-554.667zM725.333 213.333v-42.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-170.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h42.667v554.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h426.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-554.667h42.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM384 213.333v-42.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v42.667zM384 469.333v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM554.667 469.333v256c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trash-2" + ], + "grid": 0 + }, + { + "id": 236, + "paths": [ + "M768 298.667v554.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-426.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-554.667zM725.333 213.333v-42.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-170.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h42.667v554.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h426.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-554.667h42.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667zM384 213.333v-42.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2h170.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trash" + ], + "grid": 0 + }, + { + "id": 237, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM213.333 170.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM298.667 256c-23.552 0-42.667 19.115-42.667 42.667v384c0 23.552 19.115 42.667 42.667 42.667h128c23.552 0 42.667-19.115 42.667-42.667v-384c0-23.552-19.115-42.667-42.667-42.667zM341.333 341.333h42.667v298.667h-42.667zM597.333 256c-23.552 0-42.667 19.115-42.667 42.667v213.333c0 23.552 19.115 42.667 42.667 42.667h128c23.552 0 42.667-19.115 42.667-42.667v-213.333c0-23.552-19.115-42.667-42.667-42.667zM640 341.333h42.667v128h-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trello" + ], + "grid": 0 + }, + { + "id": 238, + "paths": [ + "M725.333 810.667h256c0.128 0 0.213 0 0.341 0 5.632-0.043 11.008-1.195 15.957-3.2 10.453-4.309 18.816-12.715 23.168-23.168 2.048-4.907 3.157-10.283 3.2-15.957 0-0.128 0-0.213 0-0.341v-256c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v153.003l-332.501-332.501c-16.683-16.683-43.691-16.683-60.331 0l-183.168 183.168-289.835-289.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l320 320c16.683 16.683 43.691 16.683 60.331 0l183.168-183.168 302.336 302.336h-153.003c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trending-down" + ], + "grid": 0 + }, + { + "id": 239, + "paths": [ + "M725.333 298.667h153.003l-302.336 302.336-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0l-320 320c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l289.835-289.835 183.168 183.168c16.683 16.683 43.691 16.683 60.331 0l332.501-332.501v153.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-256c0-5.76-1.152-11.264-3.2-16.299-4.309-10.453-12.672-18.816-23.168-23.168-4.907-2.005-10.283-3.157-15.915-3.2-0.128 0-0.256 0-0.384 0h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trending-up" + ], + "grid": 0 + }, + { + "id": 240, + "paths": [ + "M475.648 186.624c3.115-5.248 7.893-10.325 14.251-14.165 4.992-3.029 10.283-4.907 15.616-5.717 5.547-0.853 11.221-0.597 16.683 0.725s10.581 3.712 15.147 7.040c4.352 3.2 8.149 7.253 11.093 12.075l361.216 603.008c3.285 5.589 5.461 12.715 5.547 20.565 0.085 5.845-1.024 11.349-3.029 16.341-2.091 5.205-5.205 9.941-9.131 13.952s-8.619 7.211-13.781 9.429c-4.949 2.091-10.411 3.328-15.787 3.371l-722.731 0.085c-6.485-0.043-13.696-1.749-20.523-5.717-5.077-2.944-9.259-6.656-12.501-10.923-3.413-4.437-5.931-9.557-7.381-14.976s-1.835-11.093-1.109-16.64c0.683-5.333 2.432-10.667 5.035-15.147zM402.432 142.763l-361.387 603.307c-8.96 15.531-14.293 31.616-16.427 47.829-2.219 16.853-1.024 33.792 3.285 49.877s11.733 31.36 22.101 44.843c9.984 13.013 22.613 24.277 37.547 32.896 19.797 11.435 41.643 17.067 62.933 17.152h722.901c17.707-0.213 34.261-3.797 49.323-10.24 15.616-6.656 29.611-16.341 41.259-28.245s20.992-26.069 27.307-41.856c6.101-15.189 9.344-31.787 9.173-49.067-0.256-22.869-6.528-44.544-17.323-62.891l-361.557-603.605c-9.088-14.976-20.608-27.349-33.835-37.035-13.696-10.069-29.141-17.152-45.312-21.12s-33.152-4.779-49.92-2.219c-16.213 2.475-32.128 8.149-46.891 17.109-18.304 11.093-33.067 26.24-43.179 43.264z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "triangle" + ], + "grid": 0 + }, + { + "id": 241, + "paths": [ + "M640 640h-554.667v-469.333h554.667v170.667zM725.333 384h110.336l102.997 102.997v153.003h-213.333zM298.667 789.333c0 8.747-1.749 16.981-4.821 24.448-3.243 7.808-7.979 14.848-13.909 20.779s-13.013 10.667-20.779 13.909c-7.509 3.115-15.744 4.864-24.491 4.864s-16.981-1.749-24.448-4.821c-7.808-3.243-14.848-7.979-20.779-13.909s-10.667-13.013-13.909-20.779c-3.115-7.509-4.864-15.744-4.864-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.848 13.909-20.779s13.013-10.667 20.779-13.909c7.509-3.115 15.744-4.864 24.491-4.864s16.981 1.749 24.448 4.821c7.808 3.243 14.848 7.979 20.779 13.909s10.667 13.013 13.909 20.779c3.115 7.509 4.864 15.744 4.864 24.491zM938.667 789.333c0-20.139-4.011-39.467-11.307-57.131-0.981-2.304-1.963-4.608-3.072-6.869h57.045c23.552 0 42.667-19.115 42.667-42.667v-213.333c0-10.923-4.181-21.845-12.501-30.165l-128-128c-7.723-7.723-18.389-12.501-30.165-12.501h-128v-170.667c0-23.552-19.115-42.667-42.667-42.667h-640c-23.552 0-42.667 19.115-42.667 42.667v554.667c0 23.552 19.115 42.667 42.667 42.667h57.045c-1.067 2.261-2.091 4.565-3.072 6.869-7.296 17.664-11.307 36.992-11.307 57.131s4.011 39.467 11.307 57.131c7.595 18.304 18.688 34.731 32.427 48.469s30.165 24.832 48.469 32.427c17.664 7.296 36.992 11.307 57.131 11.307s39.467-4.011 57.131-11.307c18.304-7.595 34.731-18.688 48.469-32.427s24.832-30.165 32.427-48.469c7.296-17.664 11.307-36.992 11.307-57.131s-4.011-39.467-11.307-57.131c-0.981-2.304-2.005-4.608-3.072-6.869h284.757c-1.067 2.261-2.091 4.565-3.072 6.869-7.296 17.664-11.307 36.992-11.307 57.131s4.011 39.467 11.307 57.131c7.595 18.304 18.688 34.731 32.427 48.469s30.165 24.832 48.469 32.427c17.664 7.296 36.992 11.307 57.131 11.307s39.467-4.011 57.131-11.307c18.304-7.595 34.731-18.688 48.469-32.427s24.832-30.165 32.427-48.469c7.296-17.664 11.307-36.992 11.307-57.131zM853.333 789.333c0 8.747-1.749 16.981-4.821 24.448-3.243 7.808-7.979 14.848-13.909 20.779s-13.013 10.667-20.779 13.909c-7.509 3.115-15.744 4.864-24.491 4.864s-16.981-1.749-24.448-4.821c-7.808-3.243-14.848-7.979-20.779-13.909s-10.667-13.013-13.909-20.779c-3.115-7.509-4.864-15.744-4.864-24.491s1.749-16.981 4.821-24.448c3.243-7.808 7.979-14.848 13.909-20.779s13.013-10.667 20.779-13.909c7.509-3.115 15.744-4.864 24.491-4.864s16.981 1.749 24.448 4.821c7.808 3.243 14.848 7.979 20.779 13.909s10.667 13.013 13.909 20.779c3.115 7.509 4.864 15.744 4.864 24.491z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "truck" + ], + "grid": 0 + }, + { + "id": 242, + "paths": [ + "M170.667 341.333h682.667c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v469.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-682.667c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-469.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM695.168 55.168l-183.168 183.168-183.168-183.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l140.501 140.501h-238.336c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v469.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h682.667c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-469.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-238.336l140.501-140.501c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tv" + ], + "grid": 0 + }, + { + "id": 243, + "paths": [ + "M888.875 224.512c-7.936 10.325-16.683 20.267-26.283 29.696-9.941 9.899-14.805 24.192-11.861 38.741 1.579 7.765 2.56 17.237 2.603 27.051 0 59.947-7.424 115.072-20.907 165.163-12.459 46.293-30.165 88.405-52.139 126.208-43.477 74.837-103.979 133.248-174.848 173.611-36.395 20.736-75.605 36.779-116.693 47.787-42.837 11.477-87.723 17.493-133.675 17.749-46.976 0.256-95.019-5.547-143.061-17.749 53.163-15.403 104.96-39.339 153.301-72.107 6.272-4.224 11.648-10.368 15.019-17.963 9.557-21.547-0.128-46.763-21.675-56.32-61.355-27.264-106.112-59.733-138.709-94.080-30.72-32.341-51.328-66.987-64.768-101.931-7.893-20.523-13.355-41.259-16.896-61.824-4.48-25.856-5.888-51.456-5.248-75.861 0.896-34.603 5.888-66.56 11.861-93.099 16.768 14.635 34.219 30.123 52.651 44.288 44.459 34.176 94.72 60.971 148.949 78.635 52.437 17.067 108.544 25.6 166.613 24.107 23.083-0.64 41.557-19.499 41.557-42.667v-43.136c-0.085-7.637 0.384-15.232 1.451-22.784 2.475-17.536 8.021-34.56 16.512-50.261 8.192-15.104 19.2-29.056 33.024-41.088 15.232-13.269 32.299-22.784 50.219-28.8 18.603-6.229 38.187-8.661 57.557-7.296s38.4 6.443 55.979 15.232c16.896 8.405 32.469 20.224 45.739 35.456 10.752 12.203 27.691 17.749 44.075 12.971 9.856-2.859 19.755-6.101 29.653-9.728zM956.757 93.141c-31.573 22.272-64.981 39.509-97.579 51.413-15.829-14.208-33.237-25.941-51.669-35.115-27.733-13.824-57.728-21.845-88.107-23.979s-61.184 1.664-90.581 11.52c-28.331 9.472-55.253 24.576-79.104 45.312-21.632 18.816-39.040 40.832-52.053 64.768-13.483 24.832-22.187 51.669-26.027 79.104-1.579 11.307-2.347 22.741-2.304 34.133-33.408-2.475-65.664-8.917-96.427-18.944-44.8-14.592-86.443-36.736-123.349-65.109-33.067-25.429-62.336-55.851-86.741-90.283-13.653-19.2-40.277-23.723-59.52-10.112-6.528 4.651-11.392 10.837-14.293 17.493-0.725 1.664-15.659 35.499-27.392 87.467-6.912 30.805-12.843 68.395-13.909 109.696-0.725 29.184 0.939 60.416 6.485 92.544 4.395 25.6 11.307 51.797 21.376 77.952 17.237 44.843 43.691 89.173 82.517 130.048 25.984 27.349 57.173 52.821 94.379 75.733-21.803 10.112-44.117 18.304-66.645 24.704-46.677 13.227-94.464 18.603-141.269 16.555-23.552-1.024-43.477 17.195-44.501 40.747-0.725 16.597 8.107 31.403 21.888 39.168 107.904 59.947 222.763 88.448 333.611 87.851 53.12-0.299 105.301-7.253 155.307-20.651 48-12.843 93.995-31.616 136.875-56.064 83.627-47.659 155.136-116.736 206.379-204.885 25.813-44.416 46.379-93.483 60.757-146.859 15.488-57.728 23.808-120.32 23.808-187.349-0.043-7.125-0.341-14.080-0.981-20.864 42.923-47.573 71.509-103.637 85.163-161.323 5.419-22.912-8.789-45.909-31.701-51.328-12.373-2.944-24.747-0.128-34.432 6.656z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "twitter" + ], + "grid": 0 + }, + { + "id": 244, + "paths": [ + "M469.333 213.333v597.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-597.333h256v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-128c0-23.552-19.115-42.667-42.667-42.667h-682.667c-23.552 0-42.667 19.115-42.667 42.667v128c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "type" + ], + "grid": 0 + }, + { + "id": 245, + "paths": [ + "M1023.787 507.947c-6.613-69.248-26.709-134.059-57.472-192.043-31.957-60.203-75.349-112.896-126.976-155.563s-111.616-75.307-176.768-95.275c-62.72-19.243-130.176-26.709-199.424-20.096-63.957 6.144-124.117 23.723-178.645 50.688-56.363 27.861-106.581 65.664-148.608 111.019-37.888 40.832-69.163 87.808-92.288 139.136-22.741 50.347-37.675 104.875-43.392 161.92-2.347 23.467 14.763 44.373 38.187 46.72 1.493 0.171 2.987 0.213 4.267 0.213h426.667v256c0 23.040 4.608 45.099 12.928 65.28 8.661 20.907 21.333 39.68 37.035 55.381s34.475 28.373 55.381 37.035c20.224 8.363 42.283 12.971 65.323 12.971s45.099-4.608 65.28-12.928c20.907-8.661 39.68-21.333 55.381-37.035s28.373-34.475 37.035-55.381c8.363-20.224 12.971-42.283 12.971-65.323 0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667c0 11.648-2.304 22.613-6.443 32.64-4.309 10.411-10.667 19.797-18.56 27.733s-17.323 14.251-27.733 18.56c-9.984 4.096-20.949 6.4-32.597 6.4s-22.613-2.304-32.64-6.443c-10.411-4.309-19.797-10.667-27.733-18.56-7.893-7.893-14.251-17.323-18.56-27.733-4.096-9.984-6.4-20.949-6.4-32.597v-256h426.667c23.552 0 42.667-19.115 42.667-42.667 0-1.408-0.085-2.859-0.213-4.053zM931.84 469.333h-839.424c6.528-30.763 16.299-60.373 28.928-88.405 19.371-42.88 45.483-82.133 77.099-116.224 35.115-37.845 76.971-69.376 123.861-92.544 45.312-22.4 95.445-37.077 148.907-42.197 57.899-5.547 114.091 0.725 166.315 16.725 54.272 16.64 104.32 43.861 147.456 79.488s79.36 79.616 105.941 129.749c18.56 35.029 32.512 73.045 40.875 113.408z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "umbrella" + ], + "grid": 0 + }, + { + "id": 246, + "paths": [ + "M213.333 128v298.667c0 40.363 8.021 78.976 22.613 114.219 15.147 36.565 37.333 69.461 64.853 96.981s60.373 49.707 96.981 64.853c35.243 14.592 73.856 22.613 114.219 22.613s78.976-8.021 114.219-22.613c36.565-15.147 69.461-37.333 96.981-64.853s49.707-60.373 64.853-96.981c14.592-35.243 22.613-73.856 22.613-114.219v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v298.667c0 28.971-5.76 56.491-16.128 81.579-10.795 26.069-26.624 49.579-46.336 69.291s-43.221 35.541-69.291 46.336c-25.088 10.368-52.608 16.128-81.579 16.128s-56.491-5.76-81.579-16.128c-26.069-10.795-49.579-26.624-69.291-46.336s-35.541-43.221-46.336-69.291c-10.368-25.088-16.128-52.608-16.128-81.579v-298.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM170.667 938.667h682.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-682.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "underline" + ], + "grid": 0 + }, + { + "id": 247, + "paths": [ + "M213.333 512h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v298.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-298.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM341.333 426.667v-128c-0.043-23.253 4.565-45.269 12.843-65.323 8.619-20.864 21.291-39.68 37.035-55.467s34.56-28.459 55.381-37.12c20.053-8.32 42.027-12.971 65.237-12.971 21.333-0.043 41.685 3.84 60.416 10.965 19.371 7.339 37.12 18.133 52.48 31.744 13.739 12.16 25.515 26.539 34.816 42.581 9.131 15.744 15.915 33.109 19.755 51.669 4.821 23.083 27.392 37.888 50.475 33.067s37.888-27.392 33.067-50.475c-5.76-27.605-15.829-53.547-29.483-77.099-13.909-23.979-31.531-45.44-52.096-63.616-22.955-20.352-49.621-36.565-78.805-47.659-28.288-10.709-58.88-16.512-90.709-16.469-34.603 0.043-67.669 6.955-97.877 19.499-31.36 13.013-59.52 32.085-83.072 55.68s-42.539 51.712-55.509 83.115c-12.459 30.208-19.328 63.317-19.285 97.877v128h-42.667c-17.28 0-33.835 3.456-48.981 9.728-15.701 6.485-29.781 16-41.557 27.776s-21.291 25.856-27.776 41.557c-6.229 15.104-9.685 31.659-9.685 48.939v298.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-298.667c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "unlock" + ], + "grid": 0 + }, + { + "id": 248, + "paths": [ + "M469.333 614.997v281.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-281.003l97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-170.667-170.667c-0.085-0.085-0.171-0.171-0.256-0.256-4.053-3.968-8.661-6.955-13.568-9.003-5.12-2.133-10.624-3.2-16.085-3.243-0.171 0-0.341 0-0.469 0-5.461 0.043-10.965 1.109-16.085 3.243-4.949 2.048-9.557 5.035-13.568 9.003-0.085 0.085-0.171 0.171-0.256 0.256l-170.667 170.667c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM890.411 822.101c30.379-16.555 56.149-38.443 76.672-63.915 21.333-26.411 36.949-56.619 46.379-88.576s12.629-65.835 9.003-99.584c-3.456-32.512-13.269-64.896-29.824-95.232-14.208-26.069-32.384-48.768-53.376-67.669-21.717-19.541-46.421-34.944-72.875-45.952-30.891-12.8-64.171-19.584-98.048-19.84h-22.528c-13.312-37.717-32.085-72.235-55.168-102.912-30.635-40.661-68.821-74.453-111.915-99.84s-91.179-42.411-141.568-49.536c-48.597-6.784-99.243-4.395-149.504 8.619s-95.744 35.413-134.912 64.939c-40.661 30.635-74.453 68.821-99.84 111.915s-42.411 91.179-49.493 141.568c-6.827 48.555-4.395 99.2 8.576 149.461 15.872 61.312 45.781 115.627 84.267 158.421 15.744 17.536 42.752 18.944 60.245 3.2s18.944-42.752 3.2-60.245c-29.355-32.64-52.693-74.667-65.109-122.752-10.155-39.253-11.989-78.592-6.699-116.224 5.504-39.125 18.773-76.501 38.571-110.123s46.080-63.317 77.653-87.083c30.379-22.869 65.664-40.32 104.917-50.475s78.592-11.989 116.224-6.699c39.125 5.504 76.544 18.731 110.123 38.528s63.317 46.080 87.083 77.653c22.869 30.379 40.32 65.664 50.475 104.917 4.907 18.56 21.547 32 41.301 32h53.461c22.869 0.171 45.269 4.736 65.92 13.312 17.707 7.339 34.133 17.621 48.512 30.592 13.909 12.501 25.984 27.605 35.541 45.099 11.093 20.352 17.579 41.899 19.883 63.488 2.389 22.443 0.256 45.013-6.016 66.432s-16.725 41.515-30.933 59.093c-13.611 16.896-30.763 31.445-51.115 42.581-20.693 11.264-28.331 37.205-17.024 57.899s37.205 28.331 57.899 17.024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "upload-cloud" + ], + "grid": 0 + }, + { + "id": 249, + "paths": [ + "M853.333 640v170.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v170.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 230.997v409.003c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-409.003l140.501 140.501c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-213.333-213.333c-0.043-0.043-0.128-0.085-0.171-0.171-4.053-4.011-8.704-7.040-13.653-9.088-10.453-4.309-22.229-4.309-32.683 0-4.949 2.048-9.6 5.077-13.653 9.088-0.043 0.043-0.128 0.085-0.171 0.171l-213.333 213.333c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "upload" + ], + "grid": 0 + }, + { + "id": 250, + "paths": [ + "M725.333 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-298.667c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM576 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM490.667 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-15.957-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM695.168 499.499l85.333 85.333c16.683 16.683 43.691 16.683 60.331 0l170.667-170.667c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-140.501 140.501-55.168-55.168c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-check" + ], + "grid": 0 + }, + { + "id": 251, + "paths": [ + "M725.333 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-298.667c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM576 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM490.667 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-15.957-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM981.333 426.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-minus" + ], + "grid": 0 + }, + { + "id": 252, + "paths": [ + "M725.333 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-298.667c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM576 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM490.667 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-15.957-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM981.333 426.667h-85.333v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h85.333v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-plus" + ], + "grid": 0 + }, + { + "id": 253, + "paths": [ + "M725.333 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-298.667c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h298.667c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM576 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM490.667 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-15.957-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM951.168 311.168l-76.501 76.501-76.501-76.501c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l76.501 76.501-76.501 76.501c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l76.501-76.501 76.501 76.501c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-76.501-76.501 76.501-76.501c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-x" + ], + "grid": 0 + }, + { + "id": 254, + "paths": [ + "M896 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-341.333c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h341.333c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM725.333 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM640 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-16-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user" + ], + "grid": 0 + }, + { + "id": 255, + "paths": [ + "M768 896v-85.333c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213h-341.333c-28.8 0-56.405 5.717-81.579 16.171-26.155 10.837-49.621 26.667-69.248 46.293s-35.499 43.136-46.293 69.248c-10.496 25.216-16.213 52.821-16.213 81.621v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333c0-17.408 3.456-33.92 9.685-48.939 6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685h341.333c17.408 0 33.92 3.456 48.939 9.685 15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM597.333 298.667c0-28.8-5.717-56.405-16.171-81.579-10.837-26.155-26.667-49.621-46.293-69.248s-43.136-35.499-69.248-46.293c-25.216-10.496-52.821-16.213-81.621-16.213s-56.405 5.717-81.579 16.171c-26.155 10.837-49.621 26.667-69.291 46.293s-35.456 43.136-46.293 69.291c-10.453 25.173-16.171 52.779-16.171 81.579s5.717 56.405 16.171 81.579c10.837 26.155 26.667 49.621 46.293 69.248s43.136 35.499 69.248 46.293c25.216 10.496 52.821 16.213 81.621 16.213s56.405-5.717 81.579-16.171c26.155-10.837 49.621-26.667 69.248-46.293s35.499-43.136 46.293-69.248c10.496-25.216 16.213-52.821 16.213-81.621zM512 298.667c0 17.408-3.456 33.92-9.685 48.939-6.485 15.616-16 29.739-27.819 41.557s-25.941 21.333-41.557 27.819c-15.019 6.229-31.531 9.685-48.939 9.685s-33.92-3.456-48.939-9.685c-15.616-6.485-29.739-15.957-41.557-27.819s-21.333-25.941-27.819-41.557c-6.229-15.019-9.685-31.531-9.685-48.939s3.456-33.92 9.685-48.939c6.485-15.616 16-29.739 27.819-41.557s25.941-21.333 41.557-27.819c15.019-6.229 31.531-9.685 48.939-9.685s33.92 3.456 48.939 9.685c15.616 6.485 29.739 16 41.557 27.819s21.333 25.941 27.819 41.557c6.229 15.019 9.685 31.531 9.685 48.939zM1024 896v-85.333c0-26.069-4.736-51.115-13.397-74.325-8.96-23.979-22.059-45.867-38.485-64.811-27.819-32.128-65.195-55.936-107.904-67.243-22.784-6.016-46.123 7.552-52.139 30.336s7.552 46.123 30.336 52.139c25.899 6.869 48.469 21.248 65.195 40.619 9.899 11.392 17.707 24.533 23.040 38.784 5.163 13.824 8.021 28.8 8.021 44.501v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667zM672.085 174.891c16.853 4.309 32 11.776 45.013 21.504 13.525 10.155 24.832 22.869 33.365 37.248s14.251 30.421 16.64 47.189c2.304 16.085 1.536 32.939-2.773 49.792-3.712 14.507-9.728 27.691-17.493 39.339-8.064 12.032-18.091 22.571-29.653 31.189-13.397 10.027-28.8 17.493-45.355 21.803-22.784 5.973-36.437 29.312-30.421 52.096s29.312 36.437 52.096 30.421c27.179-7.125 52.565-19.413 74.795-36.011 19.115-14.293 35.925-31.829 49.451-52.053 13.099-19.584 23.125-41.643 29.269-65.621 7.168-27.904 8.448-56.064 4.565-83.072-4.011-27.989-13.525-54.699-27.691-78.592s-32.981-45.056-55.595-62.037c-21.803-16.341-47.104-28.757-75.051-35.925-22.827-5.845-46.080 7.936-51.925 30.763s7.936 46.080 30.763 51.925z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "users" + ], + "grid": 0 + }, + { + "id": 256, + "paths": [ + "M454.827 256h142.507c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v142.507c0 11.776 4.779 22.443 12.501 30.165l42.667 42.667c14.976 14.976 38.315 16.512 55.168 4.395l188.331-136.192v343.125c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-426.667c0.043-8.576-2.603-17.408-8.107-25.003-13.824-19.072-40.491-23.381-59.563-9.557l-226.517 163.883-4.48-4.48v-124.843c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685h-142.507c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM195.669 256l444.331 444.331v25.003c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-469.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-426.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM12.501 72.832l98.901 98.901c-11.307 1.451-22.144 4.395-32.384 8.661-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v426.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h469.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776 11.477-11.477 20.821-25.131 27.307-40.363l236.032 236.032c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.709-938.667c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "video-off" + ], + "grid": 0 + }, + { + "id": 257, + "paths": [ + "M938.667 381.568v260.864l-182.613-130.432zM128 170.667c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v426.667c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h469.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-130.432l231.211 165.163c19.157 13.696 45.824 9.259 59.52-9.899 5.376-7.595 7.979-16.341 7.936-24.832v-426.667c0-23.552-19.115-42.667-42.667-42.667-9.301 0-17.92 2.987-24.789 7.936l-231.211 165.163v-130.432c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM128 256h469.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v426.667c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-469.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-426.667c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "video" + ], + "grid": 0 + }, + { + "id": 258, + "paths": [ + "M384 490.667c0 20.309-4.011 39.552-11.307 57.088-7.552 18.261-18.645 34.688-32.427 48.512s-30.251 24.875-48.512 32.427c-17.536 7.296-36.779 11.307-57.088 11.307s-39.552-4.011-57.088-11.307c-18.261-7.552-34.688-18.645-48.512-32.427s-24.875-30.251-32.427-48.512c-7.296-17.536-11.307-36.779-11.307-57.088s4.011-39.552 11.307-57.088c7.552-18.261 18.645-34.688 32.427-48.512s30.251-24.875 48.512-32.427c17.536-7.296 36.779-11.307 57.088-11.307s39.552 4.011 57.088 11.307c18.261 7.552 34.688 18.645 48.512 32.427s24.875 30.251 32.427 48.512c7.296 17.536 11.307 36.779 11.307 57.088zM938.667 490.667c0 20.309-4.011 39.552-11.307 57.088-7.552 18.261-18.645 34.688-32.427 48.512s-30.251 24.875-48.512 32.427c-17.536 7.296-36.779 11.307-57.088 11.307s-39.552-4.011-57.088-11.307c-18.261-7.552-34.688-18.645-48.512-32.427s-24.875-30.251-32.427-48.512c-7.296-17.536-11.307-36.779-11.307-57.088s4.011-39.552 11.307-57.088c7.552-18.261 18.645-34.688 32.427-48.512s30.251-24.875 48.512-32.427c17.536-7.296 36.779-11.307 57.088-11.307s39.552 4.011 57.088 11.307c18.261 7.552 34.688 18.645 48.512 32.427s24.875 30.251 32.427 48.512c7.296 17.536 11.307 36.779 11.307 57.088zM234.667 725.333h554.667c31.701 0 62.037-6.315 89.771-17.792 28.757-11.904 54.571-29.355 76.203-50.944s39.040-47.445 50.944-76.203c11.435-27.691 17.749-58.027 17.749-89.728s-6.315-62.037-17.792-89.771c-11.904-28.757-29.355-54.571-50.944-76.203s-47.445-39.040-76.203-50.944c-27.691-11.435-58.027-17.749-89.728-17.749s-62.037 6.315-89.771 17.792c-28.757 11.904-54.571 29.355-76.203 50.944s-39.040 47.445-50.944 76.203c-11.435 27.691-17.749 58.027-17.749 89.728s6.315 62.037 17.792 89.771c9.003 21.717 21.12 41.771 35.84 59.563h-192.597c14.72-17.835 26.88-37.888 35.84-59.563 11.477-27.733 17.792-58.069 17.792-89.771s-6.315-62.037-17.792-89.771c-11.904-28.757-29.355-54.571-50.944-76.203s-47.445-38.997-76.16-50.901c-27.733-11.477-58.069-17.792-89.771-17.792s-62.037 6.315-89.771 17.792c-28.715 11.904-54.571 29.355-76.16 50.944s-39.040 47.445-50.944 76.16c-11.477 27.733-17.792 58.069-17.792 89.771s6.315 62.037 17.792 89.771c11.904 28.757 29.355 54.571 50.944 76.203s47.445 39.040 76.203 50.944c27.691 11.435 58.027 17.749 89.728 17.749z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "voicemail" + ], + "grid": 0 + }, + { + "id": 259, + "paths": [ + "M426.667 302.123v419.797l-144-115.2c-7.253-5.845-16.555-9.387-26.667-9.387h-128v-170.667h128c9.301 0.043 18.731-3.029 26.667-9.344zM442.667 180.011l-201.643 161.323h-155.691c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h155.691l201.643 161.323c18.389 14.72 45.269 11.733 59.989-6.656 6.315-7.893 9.387-17.365 9.344-26.667v-597.333c0-23.552-19.115-42.667-42.667-42.667-10.112 0-19.413 3.541-26.667 9.344zM632.875 391.125c16.384 16.384 28.715 35.243 36.992 55.253 8.619 20.864 12.971 43.093 12.971 65.408 0 22.272-4.352 44.544-12.971 65.408-8.32 20.053-20.608 38.869-36.992 55.253-16.64 16.683-16.64 43.691 0 60.331s43.691 16.64 60.331 0c24.448-24.448 42.965-52.736 55.509-82.944 12.971-31.36 19.456-64.725 19.456-98.048s-6.485-66.688-19.456-98.048c-12.501-30.208-31.019-58.496-55.509-82.944-16.64-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-1" + ], + "grid": 0 + }, + { + "id": 260, + "paths": [ + "M426.667 302.123v419.797l-144-115.2c-7.253-5.845-16.555-9.387-26.667-9.387h-128v-170.667h128c9.301 0.043 18.731-3.029 26.667-9.344zM442.667 180.011l-201.643 161.323h-155.691c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h155.691l201.643 161.323c18.389 14.72 45.269 11.733 59.989-6.656 6.315-7.893 9.387-17.365 9.344-26.667v-597.333c0-23.552-19.115-42.667-42.667-42.667-10.112 0-19.413 3.541-26.667 9.344zM783.488 240.512c36.821 36.821 64.555 79.189 83.243 124.373 19.456 46.933 29.184 97.024 29.184 147.115s-9.728 100.181-29.184 147.115c-18.688 45.184-46.421 87.509-83.243 124.373-16.64 16.683-16.64 43.691 0 60.331s43.691 16.64 60.331 0c44.885-44.885 78.805-96.683 101.717-152.021 23.808-57.472 35.669-118.613 35.669-179.755s-11.861-122.325-35.669-179.797c-22.912-55.339-56.875-107.179-101.76-152.064-16.64-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM632.875 391.125c16.384 16.384 28.715 35.243 36.992 55.253 8.619 20.864 12.971 43.093 12.971 65.408 0 22.272-4.352 44.544-12.971 65.408-8.32 20.053-20.608 38.869-36.992 55.253-16.64 16.683-16.64 43.691 0 60.331s43.691 16.64 60.331 0c24.448-24.448 42.965-52.736 55.509-82.944 12.971-31.36 19.456-64.725 19.456-98.048s-6.485-66.688-19.456-98.048c-12.501-30.208-31.019-58.496-55.509-82.944-16.64-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-2" + ], + "grid": 0 + }, + { + "id": 261, + "paths": [ + "M426.667 302.123v419.797l-144-115.2c-7.253-5.845-16.555-9.387-26.667-9.387h-128v-170.667h128c9.301 0.043 18.731-3.029 26.667-9.344zM442.667 180.011l-201.643 161.323h-155.691c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h155.691l201.643 161.323c18.389 14.72 45.269 11.733 59.989-6.656 6.315-7.893 9.387-17.365 9.344-26.667v-597.333c0-23.552-19.115-42.667-42.667-42.667-10.112 0-19.413 3.541-26.667 9.344zM695.168 414.165l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-x" + ], + "grid": 0 + }, + { + "id": 262, + "paths": [ + "M426.667 302.123v419.797l-144-115.2c-7.253-5.845-16.555-9.387-26.667-9.387h-128v-170.667h128c9.301 0.043 18.731-3.029 26.667-9.344zM442.667 180.011l-201.643 161.323h-155.691c-23.552 0-42.667 19.115-42.667 42.667v256c0 23.552 19.115 42.667 42.667 42.667h155.691l201.643 161.323c18.389 14.72 45.269 11.733 59.989-6.656 6.315-7.893 9.387-17.365 9.344-26.667v-597.333c0-23.552-19.115-42.667-42.667-42.667-10.112 0-19.413 3.541-26.667 9.344z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume" + ], + "grid": 0 + }, + { + "id": 263, + "paths": [ + "M469.333 384v128c0 11.776 4.779 22.443 12.501 30.165l64 64c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-51.499-51.499v-110.336c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM654.080 822.443l-7.083 77.355c-0.512 5.675-2.048 10.837-4.395 15.445-2.432 4.821-5.76 9.088-9.813 12.672-3.883 3.456-8.405 6.187-13.312 8.021-4.693 1.749-9.856 2.731-15.232 2.731h-184.832c-5.803 0.043-11.093-1.024-15.915-2.901-4.992-1.963-9.6-4.907-13.525-8.576-3.797-3.541-6.912-7.765-9.216-12.501-2.219-4.565-3.669-9.557-4.181-14.976l-7.083-77.483c3.968 1.835 7.936 3.541 11.989 5.248 40.235 16.683 84.352 25.856 130.517 25.856s90.283-9.173 130.56-25.856c3.883-1.621 7.723-3.285 11.52-5.035zM349.312 314.325c19.413-16 41.259-29.227 64.811-38.955 30.080-12.459 63.104-19.371 97.877-19.371s67.797 6.912 97.877 19.371c31.275 12.971 59.477 31.957 83.115 55.595s42.667 51.84 55.595 83.115c12.501 30.123 19.413 63.147 19.413 97.92s-6.912 67.797-19.371 97.877c-12.971 31.275-31.957 59.477-55.595 83.115-4.437 4.437-9.045 8.704-13.781 12.8-1.493 1.28-3.029 2.56-4.565 3.84-19.413 16-41.259 29.227-64.811 38.955-30.080 12.501-63.104 19.413-97.877 19.413s-67.797-6.912-97.877-19.371c-31.275-12.971-59.477-31.957-83.115-55.595s-42.667-51.84-55.595-83.115c-12.501-30.123-19.413-63.147-19.413-97.92s6.912-67.797 19.371-97.877c12.971-31.275 31.957-59.477 55.595-83.115 4.437-4.437 9.045-8.704 13.781-12.8 1.493-1.28 3.029-2.56 4.565-3.84zM746.283 263.765l-13.44-147.413c-1.493-15.829-5.803-30.891-12.501-44.587-6.869-14.080-16.256-26.667-27.52-37.205-11.691-10.965-25.429-19.755-40.661-25.728-14.72-5.76-30.72-8.917-47.147-8.832h-185.771c-15.829 0.043-31.147 3.029-45.312 8.405-14.549 5.547-27.904 13.696-39.381 23.851-11.947 10.581-21.973 23.467-29.355 38.016-7.125 14.080-11.691 29.653-13.184 46.123l-13.397 146.517c-2.688 2.517-5.376 5.12-7.979 7.723-31.445 31.445-56.789 69.035-74.112 110.805-16.683 40.277-25.856 84.395-25.856 130.56s9.173 90.283 25.856 130.56c17.323 41.813 42.667 79.36 74.112 110.805 2.475 2.475 4.992 4.907 7.509 7.296l13.44 146.987c1.493 15.829 5.803 30.891 12.501 44.587 6.869 14.080 16.256 26.667 27.52 37.205 11.691 10.965 25.429 19.755 40.661 25.728 14.72 5.76 30.72 8.917 47.147 8.832h184.661c15.915 0.043 31.317-2.901 45.568-8.277 14.677-5.547 28.075-13.653 39.637-23.893 11.989-10.624 22.059-23.467 29.44-38.059 7.125-14.080 11.733-29.739 13.227-46.208l13.397-146.475c2.688-2.517 5.376-5.12 7.979-7.723 31.445-31.445 56.789-69.035 74.112-110.805 16.725-40.277 25.899-84.395 25.899-130.56s-9.173-90.283-25.856-130.56c-17.323-41.813-42.667-79.36-74.112-110.805-2.304-2.304-4.693-4.608-7.040-6.869zM369.92 201.557l7.083-77.355c0.512-5.632 2.048-10.795 4.352-15.403 2.432-4.779 5.76-9.088 9.771-12.629 3.883-3.456 8.363-6.144 13.227-8.021 4.693-1.792 9.813-2.816 15.232-2.816h185.429c5.803-0.043 11.093 1.024 15.915 2.901 4.992 1.963 9.6 4.907 13.525 8.576 3.797 3.541 6.912 7.765 9.216 12.501 2.219 4.565 3.669 9.557 4.181 14.976l7.083 77.696c-4.096-1.877-8.235-3.712-12.416-5.419-40.235-16.725-84.352-25.899-130.517-25.899s-90.283 9.173-130.56 25.856c-3.883 1.621-7.723 3.285-11.52 5.035z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "watch" + ], + "grid": 0 + }, + { + "id": 264, + "paths": [ + "M694.656 510.251c33.664 16.427 63.915 36.565 88.064 57.472 17.835 15.403 44.757 13.483 60.203-4.352s13.483-44.757-4.352-60.203c-29.995-25.984-66.517-50.133-106.496-69.675-21.163-10.325-46.72-1.536-57.045 19.627s-1.536 46.72 19.627 57.045zM460.373 258.005c84.181-6.784 165.76 3.029 241.579 26.667 78.805 24.576 151.552 64.085 214.613 115.541 6.315 5.163 12.501 10.411 18.603 15.787 17.664 15.573 44.629 13.867 60.203-3.797s13.867-44.629-3.797-60.203c-6.912-6.101-13.952-12.032-21.077-17.877-71.381-58.24-153.771-103.040-243.157-130.901-86.101-26.837-178.603-37.931-273.835-30.251-23.467 1.877-40.96 22.443-39.083 45.909s22.485 41.003 45.952 39.083zM388.651 722.133c20.821-14.805 43.349-25.301 66.475-31.701 23.936-6.613 48.683-8.875 73.173-6.955 38.016 2.987 75.307 16.128 107.648 38.784 9.941 6.955 21.845 9.088 32.853 6.912l282.325 282.325c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-544-544c-2.133-2.688-4.523-5.12-7.211-7.211l-387.413-387.456c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331l174.421 174.421c-53.632 26.837-106.24 61.995-154.581 104.747-17.664 15.616-19.328 42.581-3.712 60.245s42.581 19.328 60.203 3.712c50.816-44.928 106.624-80.085 162.219-104.576l99.115 99.115c-57.941 19.499-113.963 50.219-164.224 92.203-18.091 15.104-20.48 42.027-5.376 60.117s42.027 20.48 60.117 5.376c54.187-45.312 116.224-74.667 178.688-88.491l118.997 118.997c-1.109-0.085-2.219-0.213-3.328-0.299-34.219-2.688-68.949 0.469-102.613 9.771-32.64 9.003-64.171 23.765-93.184 44.373-19.2 13.653-23.723 40.277-10.069 59.477s40.277 23.723 59.477 10.069zM554.667 853.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wifi-off" + ], + "grid": 0 + }, + { + "id": 265, + "paths": [ + "M240.64 568.235c41.173-34.304 86.827-59.477 134.613-75.819 49.408-16.896 101.205-24.32 152.704-22.485 46.72 1.664 93.141 10.965 137.344 27.733 43.307 16.427 84.48 40.064 121.685 70.741 18.176 14.976 45.056 12.416 60.075-5.76s12.416-45.056-5.76-60.075c-44.459-36.693-93.781-64.981-145.707-84.693-52.949-20.096-108.587-31.232-164.565-33.237-61.739-2.219-123.904 6.699-183.339 27.051-57.472 19.669-112.299 49.92-161.621 91.008-18.091 15.061-20.565 41.984-5.461 60.075s41.984 20.565 60.075 5.461zM88.789 416c61.611-54.315 130.645-94.592 203.307-121.088 75.264-27.435 154.496-40.192 233.429-38.357 74.368 1.707 148.48 16.299 218.709 43.605 68.437 26.624 133.205 65.323 191.061 115.925 17.749 15.531 44.672 13.739 60.203-4.011s13.739-44.672-4.011-60.203c-65.408-57.259-138.795-101.077-216.32-131.243-79.531-30.933-163.456-47.488-247.68-49.408-89.387-2.048-179.243 12.373-264.661 43.52-82.475 30.123-160.768 75.819-230.443 137.259-17.707 15.573-19.371 42.539-3.797 60.203s42.539 19.371 60.203 3.797zM388.651 722.133c20.821-14.805 43.349-25.301 66.475-31.701 23.936-6.613 48.683-8.875 73.173-6.955 38.016 2.987 75.307 16.128 107.648 38.784 19.285 13.525 45.909 8.832 59.435-10.453s8.832-45.909-10.453-59.435c-44.928-31.488-96.811-49.792-149.931-53.973-34.219-2.688-68.949 0.469-102.613 9.771-32.64 9.003-64.171 23.765-93.184 44.373-19.2 13.653-23.723 40.277-10.069 59.477s40.277 23.723 59.477 10.069zM554.667 853.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667 19.115 42.667 42.667 42.667 42.667-19.115 42.667-42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wifi" + ], + "grid": 0 + }, + { + "id": 266, + "paths": [ + "M439.424 225.92c4.139-4.139 8.832-7.211 13.781-9.301 5.163-2.176 10.752-3.285 16.341-3.285s11.179 1.067 16.384 3.2c4.992 2.048 9.685 5.077 13.824 9.216s7.211 8.832 9.301 13.781c2.176 5.163 3.285 10.752 3.285 16.341s-1.067 11.179-3.2 16.384c-2.048 4.992-5.077 9.685-9.216 13.824-4.096 4.096-8.704 7.168-13.611 9.216-5.077 2.176-10.581 3.285-16.128 3.371h-384.853c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h385.835c16.469-0.171 32.939-3.541 48.427-10.069 14.933-6.315 28.885-15.573 40.917-27.648 12.16-12.245 21.419-26.411 27.648-41.557 6.443-15.701 9.643-32.427 9.6-49.067s-3.413-33.365-9.941-49.024c-6.315-15.104-15.659-29.227-27.861-41.387s-26.411-21.419-41.557-27.648c-15.744-6.443-32.427-9.643-49.067-9.6s-33.323 3.328-48.981 9.899c-15.147 6.315-29.269 15.616-41.429 27.861-16.64 16.725-16.555 43.733 0.171 60.331s43.733 16.512 60.331-0.171zM506.923 858.24c12.16 12.245 26.283 21.547 41.387 27.861 15.659 6.528 32.341 9.813 48.981 9.899s33.323-3.115 49.067-9.6c15.147-6.229 29.312-15.445 41.557-27.648s21.547-26.283 27.861-41.387c6.528-15.659 9.813-32.341 9.899-48.981s-3.115-33.323-9.6-49.067c-6.229-15.147-15.445-29.312-27.648-41.557-12.032-12.117-25.984-21.333-40.917-27.648-15.488-6.528-31.957-9.899-48.427-10.069l-513.749-0.043c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h512.853c5.547 0.085 11.051 1.195 16.171 3.371 4.907 2.091 9.515 5.12 13.611 9.216 4.139 4.139 7.168 8.875 9.216 13.824 2.133 5.205 3.2 10.752 3.2 16.384s-1.109 11.179-3.285 16.341c-2.091 4.949-5.163 9.643-9.301 13.781s-8.875 7.168-13.824 9.216c-5.205 2.133-10.795 3.2-16.427 3.2s-11.179-1.109-16.341-3.285c-4.949-2.091-9.643-5.163-13.781-9.301-16.597-16.725-43.648-16.811-60.331-0.171s-16.811 43.648-0.171 60.331zM786.603 360.021c6.187-6.187 13.227-10.752 20.736-13.867 7.808-3.2 16.171-4.821 24.533-4.821s16.725 1.664 24.533 4.907c7.467 3.115 14.507 7.723 20.693 13.909s10.752 13.227 13.867 20.736c3.2 7.808 4.821 16.171 4.821 24.533s-1.664 16.725-4.907 24.533c-3.115 7.467-7.723 14.507-13.909 20.693-6.187 6.144-13.184 10.752-20.651 13.824-7.723 3.2-16.043 4.864-24.405 4.864h-746.581c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h746.752c19.328-0.043 38.741-3.797 56.96-11.349 17.579-7.296 34.005-18.048 48.256-32.256s25.088-30.72 32.427-48.341c7.595-18.304 11.392-37.76 11.435-57.173s-3.712-38.912-11.264-57.216c-7.296-17.664-18.091-34.176-32.299-48.427s-30.72-25.088-48.341-32.427c-18.389-7.637-37.845-11.435-57.259-11.477s-38.912 3.712-57.216 11.264c-17.664 7.296-34.176 18.091-48.427 32.299-16.683 16.64-16.725 43.648-0.085 60.331s43.648 16.725 60.331 0.085z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wind" + ], + "grid": 0 + }, + { + "id": 267, + "paths": [ + "M981.333 512c0-63.488-12.629-124.16-35.541-179.499-23.808-57.472-58.667-109.141-101.888-152.363s-94.891-78.123-152.363-101.888c-55.381-22.955-116.053-35.584-179.541-35.584s-124.16 12.629-179.499 35.541c-57.472 23.808-109.141 58.667-152.363 101.931s-78.123 94.891-101.931 152.363c-22.912 55.339-35.541 116.011-35.541 179.499s12.629 124.16 35.541 179.499c23.808 57.472 58.667 109.141 101.888 152.363s94.891 78.123 152.363 101.888c55.381 22.955 116.053 35.584 179.541 35.584s124.16-12.629 179.499-35.541c57.472-23.808 109.141-58.667 152.363-101.888s78.123-94.891 101.888-152.363c22.955-55.381 35.584-116.053 35.584-179.541zM896 512c0 52.096-10.368 101.675-29.056 146.859-19.456 46.976-47.957 89.259-83.413 124.672s-77.739 63.957-124.672 83.413c-45.184 18.688-94.763 29.056-146.859 29.056s-101.675-10.368-146.859-29.056c-46.976-19.456-89.259-47.957-124.672-83.413-35.456-35.456-63.957-77.739-83.413-124.672-18.688-45.184-29.056-94.763-29.056-146.859s10.368-101.675 29.056-146.859c19.456-46.976 47.957-89.259 83.413-124.672s77.739-63.957 124.672-83.413c45.184-18.688 94.763-29.056 146.859-29.056s101.675 10.368 146.859 29.056c46.976 19.456 89.259 47.957 124.672 83.413 35.456 35.456 63.957 77.739 83.413 124.672 18.688 45.184 29.056 94.763 29.056 146.859zM353.835 414.165l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "x-circle" + ], + "grid": 0 + }, + { + "id": 268, + "paths": [ + "M335.36 42.667c-10.923 0-21.845 4.181-30.165 12.501l-250.027 250.027c-7.723 7.723-12.501 18.389-12.501 30.165v353.28c0 10.923 4.181 21.845 12.501 30.165l250.027 250.027c7.723 7.723 18.389 12.501 30.165 12.501h353.28c10.923 0 21.845-4.181 30.165-12.501l250.027-250.027c7.723-7.723 12.501-18.389 12.501-30.165v-353.28c0-10.923-4.181-21.845-12.501-30.165l-250.027-250.027c-7.723-7.723-18.389-12.501-30.165-12.501zM353.024 128h317.952l225.024 225.024v317.952l-225.024 225.024h-317.952l-225.024-225.024v-317.952zM353.835 414.165l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "x-octagon" + ], + "grid": 0 + }, + { + "id": 269, + "paths": [ + "M213.333 85.333c-17.28 0-33.835 3.456-48.981 9.728-15.659 6.485-29.739 16-41.515 27.776s-21.291 25.856-27.776 41.515c-6.272 15.147-9.728 31.701-9.728 48.981v597.333c0 17.28 3.456 33.835 9.728 48.981 6.485 15.701 16 29.781 27.776 41.557s25.856 21.291 41.557 27.776c15.104 6.229 31.659 9.685 48.939 9.685h597.333c17.28 0 33.835-3.456 48.981-9.728 15.701-6.485 29.781-16 41.557-27.776s21.291-25.856 27.776-41.557c6.229-15.104 9.685-31.659 9.685-48.939v-597.333c0-17.28-3.456-33.835-9.728-48.981-6.485-15.701-16-29.781-27.776-41.557s-25.856-21.291-41.557-27.776c-15.104-6.229-31.659-9.685-48.939-9.685zM213.333 170.667h597.333c5.845 0 11.349 1.152 16.299 3.2 5.205 2.133 9.899 5.333 13.867 9.301s7.125 8.661 9.301 13.867c2.048 4.949 3.2 10.453 3.2 16.299v597.333c0 5.845-1.152 11.349-3.2 16.299-2.133 5.205-5.333 9.899-9.301 13.867s-8.661 7.125-13.867 9.301c-4.949 2.048-10.453 3.2-16.299 3.2h-597.333c-5.845 0-11.349-1.152-16.299-3.2-5.205-2.133-9.899-5.333-13.867-9.301s-7.125-8.661-9.301-13.867c-2.048-4.949-3.2-10.453-3.2-16.299v-597.333c0-5.845 1.152-11.349 3.2-16.299 2.133-5.205 5.333-9.899 9.301-13.867s8.661-7.125 13.867-9.301c4.949-2.048 10.453-3.2 16.299-3.2zM609.835 353.835l-97.835 97.835-97.835-97.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331l97.835 97.835-97.835 97.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l97.835-97.835 97.835 97.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-97.835-97.835 97.835-97.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "x-square" + ], + "grid": 0 + }, + { + "id": 270, + "paths": [ + "M225.835 286.165l225.835 225.835-225.835 225.835c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0l225.835-225.835 225.835 225.835c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-225.835-225.835 225.835-225.835c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-225.835 225.835-225.835-225.835c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "x" + ], + "grid": 0 + }, + { + "id": 271, + "paths": [ + "M920.021 283.179c12.245 65.237 19.115 140.587 18.645 218.667 0.811 65.195-5.248 139.392-18.645 214.229-2.432 8.875-6.4 17.195-11.605 24.533-4.949 6.997-11.051 13.099-18.048 18.048-6.784 4.779-14.464 8.533-22.443 10.795-4.309 1.152-10.752 2.432-19.499 3.755-9.643 1.451-21.163 2.816-34.219 4.053-32.043 3.029-71.552 5.205-111.701 6.741-31.659 1.195-63.36 2.005-91.605 2.56-22.613 0.427-42.965 0.683-59.264 0.853-24.491 0.213-39.637 0.213-39.637 0.213s-15.147 0-39.595-0.213c-16.299-0.171-36.651-0.427-59.264-0.853-28.245-0.555-59.989-1.365-91.605-2.56-40.149-1.536-79.659-3.712-111.701-6.741-13.099-1.237-24.619-2.603-34.219-4.053-8.747-1.323-15.147-2.603-19.029-3.627-8.747-2.432-16.896-6.315-24.149-11.435-6.997-4.949-13.141-11.008-18.091-18.005-4.48-6.315-8.064-13.483-10.496-21.376-12.203-65.109-18.987-140.203-18.517-218.027-0.896-65.707 5.163-140.459 18.645-215.893 2.432-8.875 6.4-17.195 11.605-24.533 4.949-6.997 11.051-13.099 18.048-18.048 6.784-4.779 14.464-8.533 22.443-10.795 4.309-1.152 10.752-2.432 19.499-3.755 9.643-1.451 21.163-2.816 34.219-4.053 32.043-3.029 71.552-5.205 111.701-6.741 31.659-1.195 63.36-2.005 91.605-2.56 56.619-1.024 98.901-1.024 98.901-1.024s42.325 0 98.901 0.981c28.288 0.512 59.989 1.237 91.648 2.347 40.277 1.408 79.957 3.413 112.171 6.187 13.099 1.109 24.661 2.347 34.347 3.712 8.747 1.195 15.275 2.389 18.005 3.029 9.344 2.688 17.963 7.040 25.515 12.757 6.827 5.163 12.757 11.435 17.451 18.56 4.395 6.613 7.808 14.080 9.984 22.272zM1003.093 263.552c-4.523-18.133-11.989-34.816-21.845-49.707-10.155-15.36-22.784-28.672-37.163-39.552-15.915-12.032-33.963-21.077-53.291-26.667-10.155-2.517-19.84-4.181-30.037-5.589-11.605-1.621-24.704-2.987-38.699-4.181-34.304-2.944-75.691-4.992-116.523-6.443-32.299-1.109-64.555-1.877-93.141-2.389-57.259-1.024-100.395-1.024-100.395-1.024s-15.659 0-40.448 0.256c-16.512 0.171-37.163 0.427-60.075 0.853-28.629 0.555-60.928 1.365-93.269 2.603-40.747 1.579-82.048 3.797-116.395 7.040-14.080 1.323-27.264 2.859-38.997 4.608-10.283 1.579-20.181 3.413-28.757 5.717-18.56 5.248-35.072 13.227-49.621 23.509-15.061 10.624-27.989 23.637-38.443 38.357-11.52 16.256-20.011 34.603-25.045 54.101-0.256 1.024-0.469 2.091-0.64 3.029-14.635 80.981-21.291 161.835-20.309 233.856-0.512 82.091 6.869 163.456 20.352 234.752 0.256 1.323 0.555 2.645 0.896 3.755 5.12 17.963 13.141 34.432 23.467 49.024 10.667 15.061 23.723 27.947 38.485 38.357 15.232 10.752 32.299 18.859 50.389 23.936 9.045 2.432 18.944 4.267 29.269 5.845 11.691 1.749 24.917 3.285 38.955 4.608 34.347 3.243 75.691 5.461 116.437 7.040 32.341 1.237 64.597 2.048 93.269 2.603 57.301 1.109 100.48 1.109 100.48 1.109s43.179 0 100.523-1.109c28.629-0.555 60.928-1.365 93.269-2.603 40.747-1.579 82.091-3.797 116.437-7.040 14.080-1.323 27.264-2.859 38.955-4.608 10.283-1.536 20.181-3.413 28.8-5.717 18.56-5.248 35.029-13.227 49.621-23.509 15.061-10.624 27.989-23.637 38.4-38.357 11.52-16.256 20.053-34.603 25.045-54.101 0.256-1.067 0.512-2.133 0.64-3.029 14.507-80.384 21.163-160.64 20.309-232.107 0.512-82.133-6.869-163.541-20.352-234.837-0.171-0.853-0.341-1.707-0.512-2.432zM458.667 567.509v-132.352l116.352 66.176zM437.077 677.931l245.333-139.52c20.48-11.648 27.648-37.717 16-58.197-3.968-6.997-9.643-12.459-16-16l-245.333-139.52c-20.48-11.648-46.549-4.48-58.197 16-3.84 6.741-5.632 14.080-5.589 21.077v279.040c0 23.552 19.115 42.667 42.667 42.667 7.765 0 15.019-2.091 21.077-5.589z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "youtube" + ], + "grid": 0 + }, + { + "id": 272, + "paths": [ + "M571.819 293.248l25.173-202.667c2.901-23.381-13.696-44.715-37.077-47.616-14.891-1.835-28.928 4.224-38.059 15.061l-103.68 124.587c-15.061 18.133-12.587 45.013 5.504 60.117s45.013 12.629 60.075-5.504l10.624-12.757-7.253 58.283c-2.901 23.381 13.696 44.715 37.077 47.616s44.715-13.696 47.616-37.077zM825.088 578.176l103.68-124.16c15.104-18.091 12.672-45.013-5.419-60.117-8.021-6.656-17.749-9.941-27.349-9.899h-227.84c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667h136.619l-45.227 54.144c-15.104 18.091-12.672 45.013 5.419 60.117s45.013 12.672 60.117-5.419zM553.003 613.333l71.936 71.936-95.232 114.261zM344.192 404.523l150.144 150.144h-275.243zM12.501 72.832l271.104 271.104-188.373 226.091c-15.104 18.091-12.629 45.013 5.461 60.075 7.979 6.699 17.707 9.941 27.307 9.899h335.659l-36.651 293.376c-2.944 23.381 13.653 44.715 37.035 47.616 14.891 1.877 28.928-4.181 38.059-15.019l183.424-220.117 265.643 265.643c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-938.667-938.667c-16.683-16.683-43.691-16.683-60.331 0-16.64 16.64-16.683 43.648 0 60.331z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zap-off" + ], + "grid": 0 + }, + { + "id": 273, + "paths": [ + "M494.293 224.427l-24.619 196.949c-0.213 1.579-0.341 3.413-0.341 5.291 0 23.552 19.115 42.667 42.667 42.667h292.907l-275.2 330.24 24.619-196.949c0.213-1.579 0.341-3.413 0.341-5.291 0-23.552-19.115-42.667-42.667-42.667h-292.907zM521.899 58.027l-426.667 512c-15.104 18.091-12.629 45.013 5.461 60.075 7.979 6.699 17.707 9.941 27.307 9.899h335.659l-36.651 293.376c-2.944 23.381 13.653 44.715 37.035 47.616 14.891 1.877 28.928-4.181 38.059-15.019l426.667-512c15.104-18.091 12.629-45.013-5.461-60.075-7.979-6.699-17.707-9.941-27.307-9.899h-335.659l36.651-293.376c2.944-23.381-13.653-44.715-37.035-47.616-14.891-1.877-28.928 4.181-38.059 15.019z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zap" + ], + "grid": 0 + }, + { + "id": 274, + "paths": [ + "M684.416 676.523c-1.451 1.109-2.859 2.347-4.224 3.712s-2.56 2.731-3.712 4.224c-26.752 25.771-58.24 46.549-93.013 60.971-35.072 14.507-73.6 22.571-114.133 22.571s-79.061-8.064-114.219-22.613c-36.523-15.104-69.419-37.291-96.981-64.896s-49.749-60.459-64.896-96.981c-14.507-35.115-22.571-73.643-22.571-114.176s8.064-79.061 22.613-114.219c15.104-36.48 37.291-69.419 64.853-96.981s60.501-49.749 96.981-64.853c35.157-14.549 73.685-22.613 114.219-22.613s79.061 8.064 114.219 22.613c36.523 15.104 69.419 37.291 96.981 64.896s49.749 60.459 64.896 96.981c14.507 35.115 22.571 73.643 22.571 114.176s-8.064 79.061-22.613 114.219c-14.421 34.773-35.2 66.261-60.971 93.013zM926.165 865.835l-156.8-156.8c22.4-27.989 40.96-59.179 54.869-92.843 18.773-45.312 29.099-94.933 29.099-146.859s-10.325-101.547-29.099-146.859c-19.456-47.019-48-89.301-83.371-124.672s-77.653-63.915-124.672-83.371c-45.312-18.773-94.933-29.099-146.859-29.099s-101.547 10.325-146.859 29.099c-47.019 19.456-89.301 48-124.672 83.371s-63.915 77.653-83.371 124.672c-18.773 45.312-29.099 94.933-29.099 146.859s10.325 101.547 29.099 146.859c19.456 47.019 48 89.301 83.371 124.672s77.653 63.915 124.672 83.371c45.312 18.773 94.933 29.099 146.859 29.099s101.547-10.325 146.859-29.099c33.621-13.952 64.853-32.512 92.843-54.869l156.8 156.8c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331zM341.333 512h85.333v85.333c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-85.333h85.333c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-85.333v-85.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v85.333h-85.333c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zoom-in" + ], + "grid": 0 + }, + { + "id": 275, + "paths": [ + "M684.416 676.523c-1.451 1.109-2.859 2.347-4.224 3.712s-2.56 2.731-3.712 4.224c-26.752 25.771-58.24 46.549-93.013 60.971-35.072 14.507-73.6 22.571-114.133 22.571s-79.061-8.064-114.219-22.613c-36.523-15.104-69.419-37.291-96.981-64.896s-49.749-60.459-64.896-96.981c-14.507-35.115-22.571-73.643-22.571-114.176s8.064-79.061 22.613-114.219c15.104-36.48 37.291-69.419 64.853-96.981s60.501-49.749 96.981-64.853c35.157-14.549 73.685-22.613 114.219-22.613s79.061 8.064 114.219 22.613c36.523 15.104 69.419 37.291 96.981 64.896s49.749 60.459 64.896 96.981c14.507 35.115 22.571 73.643 22.571 114.176s-8.064 79.061-22.613 114.219c-14.421 34.773-35.2 66.261-60.971 93.013zM926.165 865.835l-156.8-156.8c22.4-27.989 40.96-59.179 54.869-92.843 18.773-45.312 29.099-94.933 29.099-146.859s-10.325-101.547-29.099-146.859c-19.456-47.019-48-89.301-83.371-124.672s-77.653-63.915-124.672-83.371c-45.312-18.773-94.933-29.099-146.859-29.099s-101.547 10.325-146.859 29.099c-47.019 19.456-89.301 48-124.672 83.371s-63.915 77.653-83.371 124.672c-18.773 45.312-29.099 94.933-29.099 146.859s10.325 101.547 29.099 146.859c19.456 47.019 48 89.301 83.371 124.672s77.653 63.915 124.672 83.371c45.312 18.773 94.933 29.099 146.859 29.099s101.547-10.325 146.859-29.099c33.621-13.952 64.853-32.512 92.843-54.869l156.8 156.8c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331zM341.333 512h256c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-256c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zoom-out" + ], + "grid": 0 + } + ], + "colorThemes": [], + "colorThemeIdx": 0 + }, + { + "selection": [], + "id": 0, + "metadata": { + "name": "Untitled Set", + "importSize": { + "width": 24, + "height": 24 + } + }, + "height": 1024, + "prevSize": 32, + "icons": [], + "invisible": false, + "colorThemes": [] + } + ], + "uid": 163665, + "preferences": { + "showGlyphs": true, + "showCodes": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "fc-icon-", + "metadata": { + "fontFamily": "fcicons", + "majorVersion": 1, + "minorVersion": 0 + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "embed": true, + "noie8": true, + "ie7": false, + "showSelector": true, + "selector": "class", + "classSelector": ".fc-icon", + "showMetrics": false, + "showMetadata": false, + "showVersion": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 0, + "bgColor": 16777215, + "name": "icomoon", + "classSelector": ".icon" + }, + "historySize": 50 + }, + "premium": true, + "time": 1552517336364 +} \ No newline at end of file diff --git a/fullcalendar-main/packages/core/src/styles/icon-generation/README.txt b/fullcalendar-main/packages/core/src/styles/icon-generation/README.txt new file mode 100644 index 0000000..c45946e --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/icon-generation/README.txt @@ -0,0 +1,10 @@ +made with IcoMoon (https://icomoon.io/app/#/select) + +- Upload the IcoMoon project file (a JSON file) +- Must have "PRO" in order to do CSS data url embed +- In app, Generate Font > Download +- In resulting ZIP, grab style.css +- Rename to _icons.scss +- Reintroduce the "added for fc" statements + +http://stephenscaff.com/articles/2013/09/font-face-and-base64-data-uri/ diff --git a/fullcalendar-main/packages/core/src/styles/icons.css b/fullcalendar-main/packages/core/src/styles/icons.css new file mode 100755 index 0000000..0c9bbc7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/icons.css @@ -0,0 +1,51 @@ + +@font-face { + font-family: 'fcicons'; + src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format('truetype'); + font-weight: normal; + font-style: normal; +} + +.fc-icon { + /* added for fc */ + display: inline-block; + width: 1em; + height: 1em; + text-align: center; + user-select: none; + + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'fcicons' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.fc-icon-chevron-left:before { + content: "\e900"; +} +.fc-icon-chevron-right:before { + content: "\e901"; +} +.fc-icon-chevrons-left:before { + content: "\e902"; +} +.fc-icon-chevrons-right:before { + content: "\e903"; +} +.fc-icon-minus-square:before { + content: "\e904"; +} +.fc-icon-plus-square:before { + content: "\e905"; +} +.fc-icon-x:before { + content: "\e906"; +} diff --git a/fullcalendar-main/packages/core/src/styles/mixins.css b/fullcalendar-main/packages/core/src/styles/mixins.css new file mode 100644 index 0000000..5bd9517 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/mixins.css @@ -0,0 +1,21 @@ + +@mixin liquid-absolute-override { + height: auto; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +@mixin clearfix { + content: ""; + clear: both; + display: table; +} + +@mixin bg-z-indexes { // TODO: kill + & .fc-non-business { z-index: 1 } + & .fc-bg-event { z-index: 2 } + & .fc-highlight { z-index: 3 } +} diff --git a/fullcalendar-main/packages/core/src/styles/page-root.css b/fullcalendar-main/packages/core/src/styles/page-root.css new file mode 100644 index 0000000..c6cd476 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/page-root.css @@ -0,0 +1,7 @@ + +// classes attached to <body> +// TODO: make fc-event selector work when calender in shadow DOM +.fc-not-allowed, +.fc-not-allowed .fc-event { // override events' custom cursors + cursor: not-allowed; +} diff --git a/fullcalendar-main/packages/core/src/styles/popover.css b/fullcalendar-main/packages/core/src/styles/popover.css new file mode 100644 index 0000000..3357771 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/popover.css @@ -0,0 +1,41 @@ + +.fc { + + & .fc-popover { + position: absolute; + z-index: 9999; + box-shadow: 0 2px 6px rgba(0,0,0,.15); + } + + & .fc-popover-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 3px 4px; + } + + & .fc-popover-title { + margin: 0 2px; + } + + & .fc-popover-close { + cursor: pointer; + opacity: 0.65; + font-size: 1.1em; + } + +} + +.fc-theme-standard { + + & .fc-popover { + border: 1px solid var(--fc-border-color); + background: var(--fc-page-bg-color); + } + + & .fc-popover-header { + background: var(--fc-neutral-bg-color); + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/scroller-harness.css b/fullcalendar-main/packages/core/src/styles/scroller-harness.css new file mode 100644 index 0000000..c9992ed --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/scroller-harness.css @@ -0,0 +1,24 @@ + +.fc { + + & .fc-scroller-harness { + position: relative; + overflow: hidden; + direction: ltr; + // hack for chrome computing the scroller's right/left wrong for rtl. undone below... + // TODO: demonstrate in codepen + } + + & .fc-scroller-harness-liquid { + height: 100%; + } + +} + +.fc-direction-rtl { + + & .fc-scroller-harness > .fc-scroller { // undo above hack + direction: rtl; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/scroller.css b/fullcalendar-main/packages/core/src/styles/scroller.css new file mode 100644 index 0000000..8f27847 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/scroller.css @@ -0,0 +1,21 @@ + +.fc { + + & .fc-scroller { + -webkit-overflow-scrolling: touch; + position: relative; // for abs-positioned elements within + } + + & .fc-scroller-liquid { + height: 100%; + } + + & .fc-scroller-liquid-absolute { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/scrollgrid.css b/fullcalendar-main/packages/core/src/styles/scrollgrid.css new file mode 100644 index 0000000..c6d92fc --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/scrollgrid.css @@ -0,0 +1,95 @@ + +.fc-theme-standard { + + & .fc-scrollgrid { + border: 1px solid var(--fc-border-color); // bootstrap does this. match + } + +} + +.fc { + + & .fc-scrollgrid { + + &, + & table { // all tables (self included) + width: 100%; // because tables don't normally do this + table-layout: fixed; + } + + & table { // inner tables + border-top-style: hidden; + border-left-style: hidden; + border-right-style: hidden; + } + + border-collapse: separate; + border-right-width: 0; + border-bottom-width: 0; + + } + + & .fc-scrollgrid-liquid { + height: 100%; + } + + & .fc-scrollgrid-section { // a <tr> + height: 1px; // better than 0, for firefox + + & > td { + height: 1px; // needs a height so inner div within grow. better than 0, for firefox + } + + & table { + height: 1px; + // for most browsers, if a height isn't set on the table, can't do liquid-height within cells + // serves as a min-height. harmless + } + + } + + & .fc-scrollgrid-section-liquid { + & > td { + height: 100%; // better than `auto`, for firefox + } + } + + & .fc-scrollgrid-section > * { + border-top-width: 0; + border-left-width: 0; + } + + & .fc-scrollgrid-section-header > *, + & .fc-scrollgrid-section-footer > * { + border-bottom-width: 0; + } + + & .fc-scrollgrid-section-body table, + & .fc-scrollgrid-section-footer table { + border-bottom-style: hidden; // head keeps its bottom border tho + } + + // stickiness + + & .fc-scrollgrid-section-sticky > * { + background: var(--fc-page-bg-color); + position: sticky; + z-index: 3; // TODO: var + // TODO: box-shadow when sticking + } + + & .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * { + top: 0; // because border-sharing causes a gap at the top + // TODO: give safari -1. has bug + } + + & .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky > * { + bottom: 0; // known bug: bottom-stickiness doesn't work in safari + } + + & .fc-scrollgrid-sticky-shim { // for horizontal scrollbar + height: 1px; // needs height to create scrollbars + margin-bottom: -1px; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/sticky.css b/fullcalendar-main/packages/core/src/styles/sticky.css new file mode 100644 index 0000000..7fbafa9 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/sticky.css @@ -0,0 +1,4 @@ + +.fc-sticky { // no .fc wrap because used as child of body + position: sticky; +} diff --git a/fullcalendar-main/packages/core/src/styles/toolbar.css b/fullcalendar-main/packages/core/src/styles/toolbar.css new file mode 100644 index 0000000..cfcfdac --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/toolbar.css @@ -0,0 +1,43 @@ + +.fc { + + & .fc-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + } + + & .fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; + } + + & .fc-toolbar.fc-footer-toolbar { + margin-top: 1.5em; + } + + & .fc-toolbar-title { + font-size: 1.75em; + margin: 0; + } + +} + +.fc-direction-ltr { + + & .fc-toolbar > * > :not(:first-child) { + margin-left: .75em; // space between + } + +} + +.fc-direction-rtl { + + & .fc-toolbar > * > :not(:first-child) { + margin-right: .75em; // space between + } + + & .fc-toolbar-ltr { // when the toolbar-chunk positioning system is explicitly left-to-right + flex-direction: row-reverse; + } + +} diff --git a/fullcalendar-main/packages/core/src/styles/vars.css b/fullcalendar-main/packages/core/src/styles/vars.css new file mode 100644 index 0000000..2aa2ec2 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/vars.css @@ -0,0 +1,45 @@ + +/* +for css vars only. +these values are automatically known in all stylesheets. +the :root statement itself is only included in the common stylesheet. +this file is not processed by postcss when imported into the postcss-custom-properties plugin, +so only write standard css! + +NOTE: for old browsers, will need to restart watcher after changing a variable +*/ + +:root { + --fc-small-font-size: .85em; + --fc-page-bg-color: #fff; + --fc-neutral-bg-color: rgba(208, 208, 208, 0.3); + --fc-neutral-text-color: #808080; + --fc-border-color: #ddd; + + --fc-button-text-color: #fff; + --fc-button-bg-color: #2C3E50; + --fc-button-border-color: #2C3E50; + --fc-button-hover-bg-color: #1e2b37; + --fc-button-hover-border-color: #1a252f; + --fc-button-active-bg-color: #1a252f; + --fc-button-active-border-color: #151e27; + + --fc-event-bg-color: #3788d8; + --fc-event-border-color: #3788d8; + --fc-event-text-color: #fff; + --fc-event-selected-overlay-color: rgba(0, 0, 0, 0.25); + + --fc-more-link-bg-color: #d0d0d0; + --fc-more-link-text-color: inherit; + + --fc-event-resizer-thickness: 8px; + --fc-event-resizer-dot-total-width: 8px; + --fc-event-resizer-dot-border-width: 1px; + + --fc-non-business-color: rgba(215, 215, 215, 0.3); + --fc-bg-event-color: rgb(143, 223, 130); + --fc-bg-event-opacity: 0.3; + --fc-highlight-color: rgba(188, 232, 241, 0.3); + --fc-today-bg-color: rgba(255, 220, 40, 0.15); + --fc-now-indicator-color: red; +} diff --git a/fullcalendar-main/packages/core/src/styles/view-harness.css b/fullcalendar-main/packages/core/src/styles/view-harness.css new file mode 100644 index 0000000..35e7904 --- /dev/null +++ b/fullcalendar-main/packages/core/src/styles/view-harness.css @@ -0,0 +1,18 @@ + +.fc { + + & .fc-view-harness { + flex-grow: 1; // because this harness is WITHIN the .fc's flexbox + position: relative; + } + + // when the harness controls the height, make the view liquid + & .fc-view-harness-active > .fc-view { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + +} diff --git a/fullcalendar-main/packages/core/src/theme/StandardTheme.ts b/fullcalendar-main/packages/core/src/theme/StandardTheme.ts new file mode 100644 index 0000000..48c7e73 --- /dev/null +++ b/fullcalendar-main/packages/core/src/theme/StandardTheme.ts @@ -0,0 +1,31 @@ +import { Theme } from './Theme.js' + +export class StandardTheme extends Theme { +} + +StandardTheme.prototype.classes = { + root: 'fc-theme-standard', // TODO: compute this off of registered theme name + tableCellShaded: 'fc-cell-shaded', + buttonGroup: 'fc-button-group', + button: 'fc-button fc-button-primary', + buttonActive: 'fc-button-active', +} + +StandardTheme.prototype.baseIconClass = 'fc-icon' +StandardTheme.prototype.iconClasses = { + close: 'fc-icon-x', + prev: 'fc-icon-chevron-left', + next: 'fc-icon-chevron-right', + prevYear: 'fc-icon-chevrons-left', + nextYear: 'fc-icon-chevrons-right', +} +StandardTheme.prototype.rtlIconClasses = { + prev: 'fc-icon-chevron-right', + next: 'fc-icon-chevron-left', + prevYear: 'fc-icon-chevrons-right', + nextYear: 'fc-icon-chevrons-left', +} + +StandardTheme.prototype.iconOverrideOption = 'buttonIcons' // TODO: make TS-friendly +StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon' +StandardTheme.prototype.iconOverridePrefix = 'fc-icon-' diff --git a/fullcalendar-main/packages/core/src/theme/Theme.ts b/fullcalendar-main/packages/core/src/theme/Theme.ts new file mode 100644 index 0000000..5d116ae --- /dev/null +++ b/fullcalendar-main/packages/core/src/theme/Theme.ts @@ -0,0 +1,90 @@ +import { CalendarOptionsRefined } from '../options.js' + +export class Theme { + // settings. default values are set after the class + classes: any + iconClasses: any + rtlIconClasses: any + baseIconClass: string // className that ALL icon elements for this theme should have + iconOverrideOption: any // the name of the setting to use for icons. the subclass must set this. + iconOverrideCustomButtonOption: any // the name of the setting, *within* each customButtons object, to use for icons + iconOverridePrefix: string + + constructor(calendarOptions: CalendarOptionsRefined) { + if (this.iconOverrideOption) { + this.setIconOverride( + calendarOptions[this.iconOverrideOption], + ) + } + } + + setIconOverride(iconOverrideHash) { + let iconClassesCopy + let buttonName + + if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object + iconClassesCopy = { ...this.iconClasses } + + for (buttonName in iconOverrideHash) { + iconClassesCopy[buttonName] = this.applyIconOverridePrefix( + iconOverrideHash[buttonName], + ) + } + + this.iconClasses = iconClassesCopy + } else if (iconOverrideHash === false) { + this.iconClasses = {} + } + } + + applyIconOverridePrefix(className) { + let prefix = this.iconOverridePrefix + + if (prefix && className.indexOf(prefix) !== 0) { // if not already present + className = prefix + className + } + + return className + } + + getClass(key) { + return this.classes[key] || '' + } + + getIconClass(buttonName, isRtl?: boolean) { + let className + + if (isRtl && this.rtlIconClasses) { + className = this.rtlIconClasses[buttonName] || this.iconClasses[buttonName] + } else { + className = this.iconClasses[buttonName] + } + + if (className) { + return `${this.baseIconClass} ${className}` + } + + return '' + } + + getCustomButtonIconClass(customButtonProps) { + let className + + if (this.iconOverrideCustomButtonOption) { + className = customButtonProps[this.iconOverrideCustomButtonOption] + + if (className) { + return `${this.baseIconClass} ${this.applyIconOverridePrefix(className)}` + } + } + + return '' + } +} + +Theme.prototype.classes = {} +Theme.prototype.iconClasses = {} +Theme.prototype.baseIconClass = '' +Theme.prototype.iconOverridePrefix = '' + +export type ThemeClass = { new(calendarOptions: any): Theme } diff --git a/fullcalendar-main/packages/core/src/toolbar-parse.ts b/fullcalendar-main/packages/core/src/toolbar-parse.ts new file mode 100644 index 0000000..28c10b7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/toolbar-parse.ts @@ -0,0 +1,168 @@ +import { ViewSpec, ViewSpecHash } from './structs/view-spec.js' +import { Theme } from './theme/Theme.js' +import { CalendarImpl } from './api/CalendarImpl.js' +import { CalendarOptionsRefined, CalendarOptions } from './options.js' +import { ToolbarInput, ToolbarModel, ToolbarWidget, CustomButtonInput } from './toolbar-struct.js' +import { formatWithOrdinals } from './util/misc.js' + +export function parseToolbars( + calendarOptions: CalendarOptionsRefined, + calendarOptionOverrides: CalendarOptions, + theme: Theme, + viewSpecs: ViewSpecHash, + calendarApi: CalendarImpl, +) { + let header = calendarOptions.headerToolbar ? parseToolbar( + calendarOptions.headerToolbar, + calendarOptions, + calendarOptionOverrides, + theme, + viewSpecs, + calendarApi, + ) : null + let footer = calendarOptions.footerToolbar ? parseToolbar( + calendarOptions.footerToolbar, + calendarOptions, + calendarOptionOverrides, + theme, + viewSpecs, + calendarApi, + ) : null + + return { header, footer } +} + +function parseToolbar( + sectionStrHash: ToolbarInput, + calendarOptions: CalendarOptionsRefined, + calendarOptionOverrides: CalendarOptions, + theme: Theme, + viewSpecs: ViewSpecHash, + calendarApi: CalendarImpl, +) : ToolbarModel { + let sectionWidgets: { [sectionName: string]: ToolbarWidget[][] } = {} + let viewsWithButtons: string[] = [] + let hasTitle = false + + for (let sectionName in sectionStrHash) { + let sectionStr = sectionStrHash[sectionName] + let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) + sectionWidgets[sectionName] = sectionRes.widgets + viewsWithButtons.push(...sectionRes.viewsWithButtons) + hasTitle = hasTitle || sectionRes.hasTitle + } + + return { sectionWidgets, viewsWithButtons, hasTitle } +} + +/* +BAD: querying icons and text here. should be done at render time +*/ +function parseSection( + sectionStr: string, + calendarOptions: CalendarOptionsRefined, // defaults+overrides, then refined + calendarOptionOverrides: CalendarOptions, // overrides only!, unrefined :( + theme: Theme, + viewSpecs: ViewSpecHash, + calendarApi: CalendarImpl, +): { widgets: ToolbarWidget[][], viewsWithButtons: string[], hasTitle: boolean } { + let isRtl = calendarOptions.direction === 'rtl' + let calendarCustomButtons = calendarOptions.customButtons || {} + let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {} + let calendarButtonText = calendarOptions.buttonText || {} + let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {} + let calendarButtonHints = calendarOptions.buttonHints || {} + let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [] + let viewsWithButtons: string[] = [] + let hasTitle = false + + let widgets = sectionSubstrs.map( + (buttonGroupStr): ToolbarWidget[] => ( + buttonGroupStr.split(',').map((buttonName): ToolbarWidget => { + if (buttonName === 'title') { + hasTitle = true + return { buttonName } + } + + let customButtonProps: CustomButtonInput + let viewSpec: ViewSpec + let buttonClick + let buttonIcon // only one of these will be set + let buttonText // " + let buttonHint: string | ((navUnit: string) => string) + // ^ for the title="" attribute, for accessibility + + if ((customButtonProps = calendarCustomButtons[buttonName])) { + buttonClick = (ev: UIEvent) => { + if (customButtonProps.click) { + customButtonProps.click.call(ev.target, ev, ev.target) // TODO: use Calendar this context? + } + }; + + (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = customButtonProps.text) + + buttonHint = customButtonProps.hint || customButtonProps.text + } else if ((viewSpec = viewSpecs[buttonName])) { + viewsWithButtons.push(buttonName) + + buttonClick = () => { + calendarApi.changeView(buttonName) + }; + + (buttonText = viewSpec.buttonTextOverride) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = viewSpec.buttonTextDefault) + + let textFallback = + viewSpec.buttonTextOverride || + viewSpec.buttonTextDefault + + buttonHint = formatWithOrdinals( + viewSpec.buttonTitleOverride || + viewSpec.buttonTitleDefault || + calendarOptions.viewHint, + [textFallback, buttonName], // view-name = buttonName + textFallback, + ) + } else if (calendarApi[buttonName]) { // a calendarApi method + buttonClick = () => { + calendarApi[buttonName]() + }; + + (buttonText = calendarButtonTextOverrides[buttonName]) || + (buttonIcon = theme.getIconClass(buttonName, isRtl)) || + (buttonText = calendarButtonText[buttonName]) // everything else is considered default + + if (buttonName === 'prevYear' || buttonName === 'nextYear') { + let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next' + buttonHint = formatWithOrdinals( + calendarButtonHintOverrides[prevOrNext] || + calendarButtonHints[prevOrNext], + [ + calendarButtonText.year || 'year', // localize unit + 'year', + ], + calendarButtonText[buttonName], + ) + } else { + buttonHint = (navUnit: string) => formatWithOrdinals( + calendarButtonHintOverrides[buttonName] || + calendarButtonHints[buttonName], + [ + calendarButtonText[navUnit] || navUnit, // localized unit + navUnit, + ], + calendarButtonText[buttonName], + ) + } + } + + return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint } + }) + ), + ) + + return { widgets, viewsWithButtons, hasTitle } +} diff --git a/fullcalendar-main/packages/core/src/toolbar-struct.ts b/fullcalendar-main/packages/core/src/toolbar-struct.ts new file mode 100644 index 0000000..5273eb6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/toolbar-struct.ts @@ -0,0 +1,64 @@ +export interface ToolbarModel { + sectionWidgets: { [sectionName: string]: ToolbarWidget[][] } + viewsWithButtons: string[] + hasTitle: boolean +} + +export interface ToolbarWidget { + buttonName: string + buttonClick?: any + buttonIcon?: any + buttonText?: any + buttonHint?: string | ((navUnit: string) => string) + // ^ if depends on the unit which will move forward/backward, will be a function +} + +export interface ToolbarInput { + left?: string + center?: string + right?: string + start?: string + end?: string +} + +export interface CustomButtonInput { + text?: string + hint?: string + icon?: string + themeIcon?: string + bootstrapFontAwesome?: string + click?(ev: MouseEvent, element: HTMLElement): void +} + +export interface ButtonIconsInput { + prev?: string + next?: string + prevYear?: string + nextYear?: string + today?: string + [viewOrCustomButton: string]: string | undefined +} + +export interface ButtonTextCompoundInput { + prev?: string + next?: string + prevYear?: string // derive these somehow? + nextYear?: string + today?: string + month?: string + week?: string + day?: string + [viewOrCustomButton: string]: string | undefined // needed b/c of other optional types +} + +export interface ButtonHintCompoundInput { // not DRY with ButtonTextCompoundInput + prev?: string | ((...args: any[]) => string) + next?: string | ((...args: any[]) => string) + prevYear?: string | ((...args: any[]) => string) + nextYear?: string | ((...args: any[]) => string) + today?: string | ((...args: any[]) => string) + month?: string | ((...args: any[]) => string) + week?: string | ((...args: any[]) => string) + day?: string | ((...args: any[]) => string) + [viewOrCustomButton: string]: string | ((...args: any[]) => string) | undefined // needed b/c of other optional types +} diff --git a/fullcalendar-main/packages/core/src/util/DelayedRunner.ts b/fullcalendar-main/packages/core/src/util/DelayedRunner.ts new file mode 100644 index 0000000..a12a236 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/DelayedRunner.ts @@ -0,0 +1,91 @@ +export class DelayedRunner { + private isRunning = false + private isDirty = false + private pauseDepths: { [scope: string]: number } = {} + private timeoutId: number = 0 + + constructor( + private drainedOption?: () => void, + ) { + } + + request(delay?: number) { + this.isDirty = true + + if (!this.isPaused()) { + this.clearTimeout() + + if (delay == null) { + this.tryDrain() + } else { + this.timeoutId = setTimeout( // NOT OPTIMAL! TODO: look at debounce + this.tryDrain.bind(this), + delay, + ) as unknown as number + } + } + } + + pause(scope = '') { + let { pauseDepths } = this + + pauseDepths[scope] = (pauseDepths[scope] || 0) + 1 + + this.clearTimeout() + } + + resume(scope = '', force?: boolean) { + let { pauseDepths } = this + + if (scope in pauseDepths) { + if (force) { + delete pauseDepths[scope] + } else { + pauseDepths[scope] -= 1 + let depth = pauseDepths[scope] + + if (depth <= 0) { + delete pauseDepths[scope] + } + } + + this.tryDrain() + } + } + + isPaused() { + return Object.keys(this.pauseDepths).length + } + + tryDrain() { + if (!this.isRunning && !this.isPaused()) { + this.isRunning = true + + while (this.isDirty) { + this.isDirty = false + this.drained() // might set isDirty to true again + } + + this.isRunning = false + } + } + + clear() { + this.clearTimeout() + this.isDirty = false + this.pauseDepths = {} + } + + private clearTimeout() { + if (this.timeoutId) { + clearTimeout(this.timeoutId) + this.timeoutId = 0 + } + } + + protected drained() { // subclasses can implement + if (this.drainedOption) { + this.drainedOption() + } + } +} diff --git a/fullcalendar-main/packages/core/src/util/RefMap.ts b/fullcalendar-main/packages/core/src/util/RefMap.ts new file mode 100644 index 0000000..82f43d6 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/RefMap.ts @@ -0,0 +1,73 @@ +import { hashValuesToArray, collectFromHash } from './object.js' + +/* +TODO: somehow infer OtherArgs from masterCallback? +TODO: infer RefType from masterCallback if provided +*/ +export class RefMap<RefType> { + public currentMap: { [key: string]: RefType } = {} + private depths: { [key: string]: number } = {} + private callbackMap: { [key: string]: (val: RefType | null) => void } = {} + + constructor(public masterCallback?: (val: RefType | null, key: string) => void) { + } + + createRef(key: string | number) { + let refCallback = this.callbackMap[key] + + if (!refCallback) { + refCallback = this.callbackMap[key] = (val: RefType | null) => { + this.handleValue(val, String(key)) + } + } + + return refCallback + } + + handleValue = (val: RefType | null, key: string) => { // bind in case users want to pass it around + let { depths, currentMap } = this + let removed = false + let added = false + + if (val !== null) { + // for bug... ACTUALLY: can probably do away with this now that callers don't share numeric indices anymore + removed = (key in currentMap) + + currentMap[key] = val + depths[key] = (depths[key] || 0) + 1 + added = true + } else { + depths[key] -= 1 + + if (!depths[key]) { + delete currentMap[key] + delete this.callbackMap[key] + removed = true + } + } + + if (this.masterCallback) { + if (removed) { + this.masterCallback(null, String(key)) + } + if (added) { + this.masterCallback(val, String(key)) + } + } + } + + // TODO: check callers that don't care about order. should use getAll instead + // NOTE: this method has become less valuable now that we are encouraged to map order by some other index + // TODO: provide ONE array-export function, buildArray, which fails on non-numeric indexes. caller can manipulate and "collect" + collect( + startIndex?: number, + endIndex?: number, + step?: number, + ) { + return collectFromHash(this.currentMap, startIndex, endIndex, step) + } + + getAll(): RefType[] { // returns in no partical order! + return hashValuesToArray(this.currentMap) + } +} diff --git a/fullcalendar-main/packages/core/src/util/TaskRunner.ts b/fullcalendar-main/packages/core/src/util/TaskRunner.ts new file mode 100644 index 0000000..e2ad832 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/TaskRunner.ts @@ -0,0 +1,55 @@ +import { DelayedRunner } from './DelayedRunner.js' + +export class TaskRunner<Task> { // this class USES the DelayedRunner + private queue: Task[] = [] + + private delayedRunner: DelayedRunner // will most likely be used WITHOUT any delay + + constructor( + private runTaskOption?: (task: Task) => void, + private drainedOption?: (completedTasks: Task[]) => void, + ) { + this.delayedRunner = new DelayedRunner(this.drain.bind(this)) + } + + request(task: Task, delay?: number) { + this.queue.push(task) + this.delayedRunner.request(delay) + } + + pause(scope?: string) { + this.delayedRunner.pause(scope) + } + + resume(scope?: string, force?: boolean) { + this.delayedRunner.resume(scope, force) + } + + drain() { + let { queue } = this + + while (queue.length) { + let completedTasks: Task[] = [] + let task: Task + + while ((task = queue.shift())) { + this.runTask(task) + completedTasks.push(task) + } + + this.drained(completedTasks) + } // keep going, in case new tasks were added in the drained handler + } + + protected runTask(task: Task) { // subclasses can implement + if (this.runTaskOption) { + this.runTaskOption(task) + } + } + + protected drained(completedTasks: Task[]) { // subclasses can implement + if (this.drainedOption) { + this.drainedOption(completedTasks) + } + } +} diff --git a/fullcalendar-main/packages/core/src/util/array.ts b/fullcalendar-main/packages/core/src/util/array.ts new file mode 100644 index 0000000..f75f97e --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/array.ts @@ -0,0 +1,55 @@ +// TODO: new util arrayify? +// Array.prototype.slice.call( + +export function removeMatching(array, testFunc) { + let removeCnt = 0 + let i = 0 + + while (i < array.length) { + if (testFunc(array[i])) { // truthy value means *remove* + array.splice(i, 1) + removeCnt += 1 + } else { + i += 1 + } + } + + return removeCnt +} + +export function removeExact(array, exactVal) { + let removeCnt = 0 + let i = 0 + + while (i < array.length) { + if (array[i] === exactVal) { + array.splice(i, 1) + removeCnt += 1 + } else { + i += 1 + } + } + + return removeCnt +} + +export function isArraysEqual(a0, a1, equalityFunc?: (v0, v1) => boolean) { // TODO: better typing + if (a0 === a1) { + return true + } + + let len = a0.length + let i + + if (len !== a1.length) { // not array? or not same length? + return false + } + + for (i = 0; i < len; i += 1) { + if (!(equalityFunc ? equalityFunc(a0[i], a1[i]) : a0[i] === a1[i])) { + return false + } + } + + return true +} diff --git a/fullcalendar-main/packages/core/src/util/date.ts b/fullcalendar-main/packages/core/src/util/date.ts new file mode 100644 index 0000000..a268d91 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/date.ts @@ -0,0 +1,66 @@ +import { DateMarker, startOfDay, addDays, diffDays, diffDayAndTime } from '../datelib/marker.js' +import { Duration, asRoughMs, createDuration } from '../datelib/duration.js' +import { DateEnv } from '../datelib/env.js' +import { DateRange, OpenDateRange } from '../datelib/date-range.js' + +/* Date stuff that doesn't belong in datelib core +----------------------------------------------------------------------------------------------------------------------*/ + +// given a timed range, computes an all-day range that has the same exact duration, +// but whose start time is aligned with the start of the day. +export function computeAlignedDayRange(timedRange: DateRange): DateRange { + let dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1 + let start = startOfDay(timedRange.start) + let end = addDays(start, dayCnt) + return { start, end } +} + +// given a timed range, computes an all-day range based on how for the end date bleeds into the next day +// TODO: give nextDayThreshold a default arg +export function computeVisibleDayRange(timedRange: OpenDateRange, nextDayThreshold: Duration = createDuration(0)): OpenDateRange { + let startDay: DateMarker = null + let endDay: DateMarker = null + + if (timedRange.end) { + endDay = startOfDay(timedRange.end) + + let endTimeMS: number = timedRange.end.valueOf() - endDay.valueOf() // # of milliseconds into `endDay` + + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) { + endDay = addDays(endDay, 1) + } + } + + if (timedRange.start) { + startDay = startOfDay(timedRange.start) // the beginning of the day the range starts + + // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day. + if (endDay && endDay <= startDay) { + endDay = addDays(startDay, 1) + } + } + + return { start: startDay, end: endDay } +} + +// spans from one day into another? +export function isMultiDayRange(range: DateRange) { + let visibleRange = computeVisibleDayRange(range) + + return diffDays(visibleRange.start, visibleRange.end) > 1 +} + +export function diffDates(date0: DateMarker, date1: DateMarker, dateEnv: DateEnv, largeUnit?: string) { + if (largeUnit === 'year') { + return createDuration(dateEnv.diffWholeYears(date0, date1), 'year')! + } + + if (largeUnit === 'month') { + return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month')! + } + + return diffDayAndTime(date0, date1) // returns a duration +} diff --git a/fullcalendar-main/packages/core/src/util/dom-event.ts b/fullcalendar-main/packages/core/src/util/dom-event.ts new file mode 100644 index 0000000..fe3b657 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/dom-event.ts @@ -0,0 +1,109 @@ +import { elementClosest } from './dom-manip.js' + +// Stops a mouse/touch event from doing it's native browser action +export function preventDefault(ev) { + ev.preventDefault() +} + +// Event Delegation +// ---------------------------------------------------------------------------------------------------------------- + +export function buildDelegationHandler<EventType extends (Event | UIEvent)>( + selector: string, + handler: (ev: EventType, matchedTarget: HTMLElement) => void, +) { + return (ev: EventType) => { + let matchedChild = elementClosest(ev.target as HTMLElement, selector) + + if (matchedChild) { + handler.call(matchedChild, ev, matchedChild) + } + } +} + +export function listenBySelector( + container: HTMLElement, + eventType: string, + selector: string, + handler: (ev: Event, matchedTarget: HTMLElement) => void, +) { + let attachedHandler = buildDelegationHandler(selector, handler) + + container.addEventListener(eventType, attachedHandler) + + return () => { + container.removeEventListener(eventType, attachedHandler) + } +} + +export function listenToHoverBySelector( + container: HTMLElement, + selector: string, + onMouseEnter: (ev: Event, matchedTarget: HTMLElement) => void, + onMouseLeave: (ev: Event, matchedTarget: HTMLElement) => void, +) { + let currentMatchedChild + + return listenBySelector(container, 'mouseover', selector, (mouseOverEv, matchedChild) => { + if (matchedChild !== currentMatchedChild) { + currentMatchedChild = matchedChild + onMouseEnter(mouseOverEv, matchedChild) + + let realOnMouseLeave = (mouseLeaveEv) => { + currentMatchedChild = null + onMouseLeave(mouseLeaveEv, matchedChild) + matchedChild.removeEventListener('mouseleave', realOnMouseLeave) + } + + // listen to the next mouseleave, and then unattach + matchedChild.addEventListener('mouseleave', realOnMouseLeave) + } + }) +} + +// Animation +// ---------------------------------------------------------------------------------------------------------------- + +const transitionEventNames = [ + 'webkitTransitionEnd', + 'otransitionend', + 'oTransitionEnd', + 'msTransitionEnd', + 'transitionend', +] + +// triggered only when the next single subsequent transition finishes +export function whenTransitionDone(el: HTMLElement, callback: (ev: Event) => void) { + let realCallback = (ev) => { + callback(ev) + transitionEventNames.forEach((eventName) => { + el.removeEventListener(eventName, realCallback) + }) + } + + transitionEventNames.forEach((eventName) => { + el.addEventListener(eventName, realCallback) // cross-browser way to determine when the transition finishes + }) +} + +// ARIA workarounds +// ---------------------------------------------------------------------------------------------------------------- + +export function createAriaClickAttrs(handler: ((ev: UIEvent) => void)) { + return { + onClick: handler, + ...createAriaKeyboardAttrs(handler), + } +} + +export function createAriaKeyboardAttrs(handler: ((ev: UIEvent) => void)) { + return { + tabIndex: 0, + onKeyDown(ev: KeyboardEvent) { + if (ev.key === 'Enter' || ev.key === ' ') { + handler(ev) + ev.preventDefault() // if space, don't scroll down page + } + }, + } +} diff --git a/fullcalendar-main/packages/core/src/util/dom-geom.ts b/fullcalendar-main/packages/core/src/util/dom-geom.ts new file mode 100644 index 0000000..c0deb8b --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/dom-geom.ts @@ -0,0 +1,132 @@ +import { intersectRects, Rect } from './geom.js' +import { getIsRtlScrollbarOnLeft } from './scrollbar-side.js' +import { computeScrollbarWidthsForEl } from './scrollbar-width.js' + +export interface EdgeInfo { + borderLeft: number + borderRight: number + borderTop: number + borderBottom: number + scrollbarLeft: number + scrollbarRight: number + scrollbarBottom: number + paddingLeft?: number + paddingRight?: number + paddingTop?: number + paddingBottom?: number +} + +export function computeEdges(el: HTMLElement, getPadding = false): EdgeInfo { // cache somehow? + let computedStyle = window.getComputedStyle(el) + let borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0 + let borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0 + let borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0 + let borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0 + let badScrollbarWidths = computeScrollbarWidthsForEl(el) // includes border! + let scrollbarLeftRight = badScrollbarWidths.y - borderLeft - borderRight + let scrollbarBottom = badScrollbarWidths.x - borderTop - borderBottom + + let res: EdgeInfo = { + borderLeft, + borderRight, + borderTop, + borderBottom, + scrollbarBottom, + scrollbarLeft: 0, + scrollbarRight: 0, + } + + if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side? + res.scrollbarLeft = scrollbarLeftRight + } else { + res.scrollbarRight = scrollbarLeftRight + } + + if (getPadding) { + res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0 + res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0 + res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0 + res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0 + } + + return res +} + +export function computeInnerRect(el, goWithinPadding = false, doFromWindowViewport?: boolean) { + let outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el) + let edges = computeEdges(el, goWithinPadding) + let res = { + left: outerRect.left + edges.borderLeft + edges.scrollbarLeft, + right: outerRect.right - edges.borderRight - edges.scrollbarRight, + top: outerRect.top + edges.borderTop, + bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom, + } + + if (goWithinPadding) { + res.left += edges.paddingLeft + res.right -= edges.paddingRight + res.top += edges.paddingTop + res.bottom -= edges.paddingBottom + } + + return res +} + +export function computeRect(el): Rect { + let rect = el.getBoundingClientRect() + + return { + left: rect.left + window.pageXOffset, + top: rect.top + window.pageYOffset, + right: rect.right + window.pageXOffset, + bottom: rect.bottom + window.pageYOffset, + } +} + +export function computeClippedClientRect(el: HTMLElement): Rect | null { + let clippingParents = getClippingParents(el) + let rect: Rect = el.getBoundingClientRect() + + for (let clippingParent of clippingParents) { + let intersection = intersectRects(rect, clippingParent.getBoundingClientRect()) + if (intersection) { + rect = intersection + } else { + return null + } + } + + return rect +} + +export function computeHeightAndMargins(el: HTMLElement) { + return el.getBoundingClientRect().height + computeVMargins(el) +} + +export function computeVMargins(el: HTMLElement) { + let computed = window.getComputedStyle(el) + + return parseInt(computed.marginTop, 10) + + parseInt(computed.marginBottom, 10) +} + +// does not return window +export function getClippingParents(el: HTMLElement): HTMLElement[] { + let parents: HTMLElement[] = [] + + while (el instanceof HTMLElement) { // will stop when gets to document or null + let computedStyle = window.getComputedStyle(el) + + if (computedStyle.position === 'fixed') { + break + } + + if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) { + parents.push(el) + } + + el = el.parentNode as HTMLElement + } + + return parents +} diff --git a/fullcalendar-main/packages/core/src/util/dom-manip.ts b/fullcalendar-main/packages/core/src/util/dom-manip.ts new file mode 100644 index 0000000..1ab0ff7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/dom-manip.ts @@ -0,0 +1,114 @@ +import { Dictionary } from '../options.js' + +export function removeElement(el: HTMLElement) { // removes nodes in addition to elements. bad name + if (el.parentNode) { + el.parentNode.removeChild(el) + } +} + +// Querying +// ---------------------------------------------------------------------------------------------------------------- + +export function elementClosest(el: HTMLElement, selector: string): HTMLElement { + if (el.closest) { + return el.closest(selector) + + // really bad fallback for IE + // from https://developer.mozilla.org/en-US/docs/Web/API/Element/closest + } + if (!document.documentElement.contains(el)) { + return null + } + do { + if (elementMatches(el, selector)) { + return el + } + el = (el.parentElement || el.parentNode) as HTMLElement + } while (el !== null && el.nodeType === 1) + return null +} + +export function elementMatches(el: HTMLElement, selector: string): HTMLElement { + let method = el.matches || (el as any).matchesSelector || (el as any).msMatchesSelector + + return method.call(el, selector) +} + +// accepts multiple subject els +// returns a real array. good for methods like forEach +// TODO: accept the document +export function findElements(container: HTMLElement[] | HTMLElement | NodeListOf<HTMLElement>, selector: string): HTMLElement[] { + let containers = container instanceof HTMLElement ? [container] : container + let allMatches: HTMLElement[] = [] + + for (let i = 0; i < containers.length; i += 1) { + let matches = containers[i].querySelectorAll(selector) + + for (let j = 0; j < matches.length; j += 1) { + allMatches.push(matches[j] as HTMLElement) + } + } + + return allMatches +} + +// accepts multiple subject els +// only queries direct child elements // TODO: rename to findDirectChildren! +export function findDirectChildren(parent: HTMLElement[] | HTMLElement, selector?: string): HTMLElement[] { + let parents = parent instanceof HTMLElement ? [parent] : parent + let allMatches = [] + + for (let i = 0; i < parents.length; i += 1) { + let childNodes = parents[i].children // only ever elements + + for (let j = 0; j < childNodes.length; j += 1) { + let childNode = childNodes[j] + + if (!selector || elementMatches(childNode as HTMLElement, selector)) { + allMatches.push(childNode) + } + } + } + + return allMatches +} + +// Style +// ---------------------------------------------------------------------------------------------------------------- + +const PIXEL_PROP_RE = /(top|left|right|bottom|width|height)$/i + +export function applyStyle(el: HTMLElement, props: Dictionary) { + for (let propName in props) { + applyStyleProp(el, propName, props[propName]) + } +} + +export function applyStyleProp(el: HTMLElement, name: string, val) { + if (val == null) { + el.style[name] = '' + } else if (typeof val === 'number' && PIXEL_PROP_RE.test(name)) { + el.style[name] = `${val}px` + } else { + el.style[name] = val + } +} + +// Event Handling +// ---------------------------------------------------------------------------------------------------------------- + +// if intercepting bubbled events at the document/window/body level, +// and want to see originating element (the 'target'), use this util instead +// of `ev.target` because it goes within web-component boundaries. +export function getEventTargetViaRoot(ev: Event) { + return ev.composedPath?.()[0] ?? ev.target +} + +// Unique ID for DOM attribute + +let guid = 0 + +export function getUniqueDomId() { + guid += 1 + return 'fc-dom-' + guid +} diff --git a/fullcalendar-main/packages/core/src/util/geom.ts b/fullcalendar-main/packages/core/src/util/geom.ts new file mode 100644 index 0000000..8b01e50 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/geom.ts @@ -0,0 +1,67 @@ +export interface Point { + left: number + top: number +} + +export interface Rect { + left: number + right: number + top: number + bottom: number +} + +export function pointInsideRect(point: Point, rect: Rect): boolean { + return point.left >= rect.left && + point.left < rect.right && + point.top >= rect.top && + point.top < rect.bottom +} + +// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false +export function intersectRects(rect1: Rect, rect2: Rect): Rect | false { + let res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom), + } + + if (res.left < res.right && res.top < res.bottom) { + return res + } + + return false +} + +export function translateRect(rect: Rect, deltaX: number, deltaY: number): Rect { + return { + left: rect.left + deltaX, + right: rect.right + deltaX, + top: rect.top + deltaY, + bottom: rect.bottom + deltaY, + } +} + +// Returns a new point that will have been moved to reside within the given rectangle +export function constrainPoint(point: Point, rect: Rect): Point { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom), + } +} + +// Returns a point that is the center of the given rectangle +export function getRectCenter(rect: Rect): Point { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2, + } +} + +// Subtracts point2's coordinates from point1's coordinates, returning a delta +export function diffPoints(point1: Point, point2: Point): Point { + return { + left: point1.left - point2.left, + top: point1.top - point2.top, + } +} diff --git a/fullcalendar-main/packages/core/src/util/html.ts b/fullcalendar-main/packages/core/src/util/html.ts new file mode 100644 index 0000000..40b05b8 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/html.ts @@ -0,0 +1,13 @@ +export type ClassNamesInput = string | string[] + +export function parseClassNames(raw: ClassNamesInput) { + if (Array.isArray(raw)) { + return raw + } + + if (typeof raw === 'string') { + return raw.split(/\s+/) + } + + return [] +} diff --git a/fullcalendar-main/packages/core/src/util/memoize.ts b/fullcalendar-main/packages/core/src/util/memoize.ts new file mode 100644 index 0000000..46efc4f --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/memoize.ts @@ -0,0 +1,145 @@ +import { isArraysEqual } from './array.js' +import { isPropsEqual } from './object.js' +import { Dictionary } from '../options.js' + +export function memoize<Args extends any[], Res>( + workerFunc: (...args: Args) => Res, + resEquality?: (res0: Res, res1: Res) => boolean, + teardownFunc?: (res: Res) => void, +): (...args: Args) => Res { + let currentArgs: Args | undefined + let currentRes: Res | undefined + + return function (...newArgs: Args) { // eslint-disable-line func-names + if (!currentArgs) { + currentRes = workerFunc.apply(this, newArgs) + } else if (!isArraysEqual(currentArgs, newArgs)) { + if (teardownFunc) { + teardownFunc(currentRes) + } + + let res = workerFunc.apply(this, newArgs) + + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res + } + } + + currentArgs = newArgs + + return currentRes + } +} + +export function memoizeObjArg<Arg extends Dictionary, Res>( + workerFunc: (arg: Arg) => Res, + resEquality?: (res0: Res, res1: Res) => boolean, + teardownFunc?: (res: Res) => void, +): (arg: Arg) => Res { + let currentArg: Arg | undefined + let currentRes: Res | undefined + + return (newArg: Arg) => { + if (!currentArg) { + currentRes = workerFunc.call(this, newArg) + } else if (!isPropsEqual(currentArg, newArg)) { + if (teardownFunc) { + teardownFunc(currentRes) + } + + let res = workerFunc.call(this, newArg) + + if (!resEquality || !resEquality(res, currentRes)) { + currentRes = res + } + } + + currentArg = newArg + + return currentRes + } +} + +export type MemoiseArrayFunc<Args extends any[], Res> = + (argSets: Args[]) => Res[] + +export function memoizeArraylike<Args extends any[], Res>( // used at all? + workerFunc: (...args: Args) => Res, + resEquality?: (res0: Res, res1: Res) => boolean, + teardownFunc?: (res: Res) => void, +): MemoiseArrayFunc<Args, Res> { + let currentArgSets: Args[] = [] + let currentResults: Res[] = [] + + return (newArgSets: Args[]) => { + let currentLen = currentArgSets.length + let newLen = newArgSets.length + let i = 0 + + for (; i < currentLen; i += 1) { + if (!newArgSets[i]) { // one of the old sets no longer exists + if (teardownFunc) { + teardownFunc(currentResults[i]) + } + } else if (!isArraysEqual(currentArgSets[i], newArgSets[i])) { + if (teardownFunc) { + teardownFunc(currentResults[i]) + } + + let res = workerFunc.apply(this, newArgSets[i]) + + if (!resEquality || !resEquality(res, currentResults[i])) { + currentResults[i] = res + } + } + } + + for (; i < newLen; i += 1) { + currentResults[i] = workerFunc.apply(this, newArgSets[i]) + } + + currentArgSets = newArgSets + currentResults.splice(newLen) // remove excess + + return currentResults + } +} + +export type MemoizeHashFunc<Args extends any[], Res> = + (argHash: { [key: string]: Args }) => { [key: string]: Res } + +export function memoizeHashlike<Args extends any[], Res>( + workerFunc: (...args: Args) => Res, + resEquality?: (res0: Res, res1: Res) => boolean, + teardownFunc?: (res: Res) => void, // TODO: change arg order +): MemoizeHashFunc<Args, Res> { + let currentArgHash: { [key: string]: Args } = {} + let currentResHash: { [key: string]: Res } = {} + + return (newArgHash: { [key: string]: Args }) => { + let newResHash: { [key: string]: Res } = {} + + for (let key in newArgHash) { + if (!currentResHash[key]) { + newResHash[key] = workerFunc.apply(this, newArgHash[key]) + } else if (!isArraysEqual(currentArgHash[key], newArgHash[key])) { + if (teardownFunc) { + teardownFunc(currentResHash[key]) + } + + let res = workerFunc.apply(this, newArgHash[key]) + + newResHash[key] = (resEquality && resEquality(res, currentResHash[key])) + ? currentResHash[key] + : res + } else { + newResHash[key] = currentResHash[key] + } + } + + currentArgHash = newArgHash + currentResHash = newResHash + + return newResHash + } +} diff --git a/fullcalendar-main/packages/core/src/util/misc.ts b/fullcalendar-main/packages/core/src/util/misc.ts new file mode 100644 index 0000000..9e01a8a --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/misc.ts @@ -0,0 +1,196 @@ +import { preventDefault } from './dom-event.js' + +export type GenericHash = { [key: string]: any } // already did this somewhere + +let guidNumber = 0 + +export function guid() { + guidNumber += 1 + return String(guidNumber) +} + +/* FullCalendar-specific DOM Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +// Make the mouse cursor express that an event is not allowed in the current area +export function disableCursor() { + document.body.classList.add('fc-not-allowed') +} + +// Returns the mouse cursor to its original look +export function enableCursor() { + document.body.classList.remove('fc-not-allowed') +} + +/* Selection +----------------------------------------------------------------------------------------------------------------------*/ + +export function preventSelection(el: HTMLElement) { + el.style.userSelect = 'none' + el.style.webkitUserSelect = 'none' + el.addEventListener('selectstart', preventDefault) +} + +export function allowSelection(el: HTMLElement) { + el.style.userSelect = '' + el.style.webkitUserSelect = '' + el.removeEventListener('selectstart', preventDefault) +} + +/* Context Menu +----------------------------------------------------------------------------------------------------------------------*/ + +export function preventContextMenu(el: HTMLElement) { + el.addEventListener('contextmenu', preventDefault) +} + +export function allowContextMenu(el: HTMLElement) { + el.removeEventListener('contextmenu', preventDefault) +} + +/* Object Ordering by Field +----------------------------------------------------------------------------------------------------------------------*/ + +export interface OrderSpec<Subject> { + field?: string + order?: number + func?: FieldSpecInputFunc<Subject> +} + +export type FieldSpecInput<Subject> = string | string[] | FieldSpecInputFunc<Subject> | FieldSpecInputFunc<Subject>[] +export type FieldSpecInputFunc<Subject> = (a: Subject, b: Subject) => number + +export function parseFieldSpecs<Subject>(input: FieldSpecInput<Subject>): OrderSpec<Subject>[] { + let specs: OrderSpec<Subject>[] = [] + let tokens = [] + let i + let token + + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/) + } else if (typeof input === 'function') { + tokens = [input] + } else if (Array.isArray(input)) { + tokens = input + } + + for (i = 0; i < tokens.length; i += 1) { + token = tokens[i] + + if (typeof token === 'string') { + specs.push( + token.charAt(0) === '-' ? + { field: token.substring(1), order: -1 } : + { field: token, order: 1 }, + ) + } else if (typeof token === 'function') { + specs.push({ func: token }) + } + } + + return specs +} + +export function compareByFieldSpecs<Subject>(obj0: Subject, obj1: Subject, fieldSpecs: OrderSpec<Subject>[]): number { + let i + let cmp + + for (i = 0; i < fieldSpecs.length; i += 1) { + cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]) + if (cmp) { + return cmp + } + } + + return 0 +} + +export function compareByFieldSpec<Subject>(obj0: Subject, obj1: Subject, fieldSpec: OrderSpec<Subject>): number { + if (fieldSpec.func) { + return fieldSpec.func(obj0, obj1) + } + + return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field]) + * (fieldSpec.order || 1) +} + +export function flexibleCompare(a, b) { + if (!a && !b) { + return 0 + } + if (b == null) { + return -1 + } + if (a == null) { + return 1 + } + if (typeof a === 'string' || typeof b === 'string') { + return String(a).localeCompare(String(b)) + } + return a - b +} + +/* String Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +export function padStart(val, len) { // doesn't work with total length more than 3 + let s = String(val) + return '000'.substr(0, len - s.length) + s +} + +export function formatWithOrdinals<Args extends any[]>( + formatter: string | ((...formatterArgs: Args) => string), + args: Args, + fallbackText: string, +) { + if (typeof formatter === 'function') { + return formatter(...args) + } + if (typeof formatter === 'string') { // non-blank string + return args.reduce((str, arg, index) => ( + str.replace('$' + index, arg || '') + ), formatter) + } + return fallbackText +} + +/* Number Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +export function compareNumbers(a, b) { // for .sort() + return a - b +} + +export function isInt(n) { + return n % 1 === 0 +} + +/* Weird Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +export function firstDefined(...args) { + for (let i = 0; i < args.length; i += 1) { + if (args[i] !== undefined) { + return args[i] + } + } + return undefined +} + +/* FC-specific DOM dimension stuff +----------------------------------------------------------------------------------------------------------------------*/ + +export function computeSmallestCellWidth(cellEl: HTMLElement) { + let allWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-frame') + let contentWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-cushion') + + if (!allWidthEl) { + throw new Error('needs fc-scrollgrid-shrink-frame className') // TODO: use const + } + if (!contentWidthEl) { + throw new Error('needs fc-scrollgrid-shrink-cushion className') + } + + return cellEl.getBoundingClientRect().width - allWidthEl.getBoundingClientRect().width + // the cell padding+border + contentWidthEl.getBoundingClientRect().width +} diff --git a/fullcalendar-main/packages/core/src/util/object.ts b/fullcalendar-main/packages/core/src/util/object.ts new file mode 100644 index 0000000..17d7922 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/object.ts @@ -0,0 +1,232 @@ +import { isMaybeObjectsEqual } from '../options.js' + +const { hasOwnProperty } = Object.prototype + +// Merges an array of objects into a single object. +// The second argument allows for an array of property names who's object values will be merged together. +export function mergeProps(propObjs, complexPropsMap?): any { + let dest = {} + + if (complexPropsMap) { + for (let name in complexPropsMap) { + if (complexPropsMap[name] === isMaybeObjectsEqual) { // implies that it's object-mergeable + let complexObjs = [] + + // collect the trailing object values, stopping when a non-object is discovered + for (let i = propObjs.length - 1; i >= 0; i -= 1) { + let val = propObjs[i][name] + + if (typeof val === 'object' && val) { // non-null object + complexObjs.unshift(val) + } else if (val !== undefined) { + dest[name] = val // if there were no objects, this value will be used + break + } + } + + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name] = mergeProps(complexObjs) + } + } + } + } + + // copy values into the destination, going from last to first + for (let i = propObjs.length - 1; i >= 0; i -= 1) { + let props = propObjs[i] + + for (let name in props) { + if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name] = props[name] + } + } + } + + return dest +} + +export function filterHash(hash, func) { + let filtered = {} + + for (let key in hash) { + if (func(hash[key], key)) { + filtered[key] = hash[key] + } + } + + return filtered +} + +export function mapHash<InputItem, OutputItem>( + hash: { [key: string]: InputItem }, + func: (input: InputItem, key: string) => OutputItem, +): { [key: string]: OutputItem } { + let newHash = {} + + for (let key in hash) { + newHash[key] = func(hash[key], key) + } + + return newHash +} + +export function arrayToHash(a): { [key: string]: true } { // TODO: rename to strinArrayToHash or something + let hash = {} + + for (let item of a) { + hash[item] = true + } + + return hash +} + +export function buildHashFromArray<Item, ItemRes>(a: Item[], func: (item: Item, index: number) => [ string, ItemRes ]) { + let hash: { [key: string]: ItemRes } = {} + + for (let i = 0; i < a.length; i += 1) { + let tuple = func(a[i], i) + + hash[tuple[0]] = tuple[1] + } + + return hash +} + +// TODO: reassess browser support +// https://caniuse.com/?search=object.values +export function hashValuesToArray(obj) { // can't use Object.values yet because no es2015 support + let a = [] + + for (let key in obj) { + a.push(obj[key]) + } + + return a +} + +export function isPropsEqual(obj0, obj1) { // TODO: merge with compareObjs + if (obj0 === obj1) { + return true + } + + for (let key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + return false + } + } + } + + for (let key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + return false + } + } + } + + return true +} + +const HANDLER_RE = /^on[A-Z]/ + +export function isNonHandlerPropsEqual(obj0, obj1) { + const keys = getUnequalProps(obj0, obj1) + + for (let key of keys) { + if (!HANDLER_RE.test(key)) { + return false + } + } + + return true +} + +export function getUnequalProps(obj0, obj1) { + let keys: string[] = [] + + for (let key in obj0) { + if (hasOwnProperty.call(obj0, key)) { + if (!(key in obj1)) { + keys.push(key) + } + } + } + + for (let key in obj1) { + if (hasOwnProperty.call(obj1, key)) { + if (obj0[key] !== obj1[key]) { + keys.push(key) + } + } + } + + return keys +} + +export type EqualityFunc<T> = (a: T, b: T) => boolean +export type EqualityThing<T> = EqualityFunc<T> | true + +export type EqualityFuncs<ObjType> = { // not really just a "func" anymore + [K in keyof ObjType]?: EqualityThing<ObjType[K]> +} + +export function compareObjs(oldProps, newProps, equalityFuncs: EqualityFuncs<any> = {}) { + if (oldProps === newProps) { + return true + } + + for (let key in newProps) { + if (key in oldProps && isObjValsEqual(oldProps[key], newProps[key], equalityFuncs[key])) { + // equal + } else { + return false + } + } + + // check for props that were omitted in the new + for (let key in oldProps) { + if (!(key in newProps)) { + return false + } + } + + return true +} + +/* +assumed "true" equality for handler names like "onReceiveSomething" +*/ +function isObjValsEqual<T>(val0: T, val1: T, comparator: EqualityThing<T>) { + if (val0 === val1 || comparator === true) { + return true + } + if (comparator) { + return comparator(val0, val1) + } + return false +} + +export function collectFromHash<Item>( + hash: { [key: string]: Item }, + startIndex = 0, + endIndex?: number, + step = 1, +) { + let res: Item[] = [] + + if (endIndex == null) { + endIndex = Object.keys(hash).length + } + + for (let i = startIndex; i < endIndex; i += step) { + let val = hash[i] + + if (val !== undefined) { // will disregard undefined for sparse arrays + res.push(val) + } + } + + return res +} diff --git a/fullcalendar-main/packages/core/src/util/promise.ts b/fullcalendar-main/packages/core/src/util/promise.ts new file mode 100644 index 0000000..764c8bd --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/promise.ts @@ -0,0 +1,35 @@ +/* +given a function that resolves a result asynchronously. +the function can either call passed-in success and failure callbacks, +or it can return a promise. +if you need to pass additional params to func, bind them first. +*/ +export function unpromisify<Res>( + func: ( + successCallback: (res: Res) => void, + failureCallback: (error: Error) => void, + ) => Promise<Res> | void, + normalizedSuccessCallback: (res: Res) => void, + normalizedFailureCallback: (error: Error) => void, +) { + // guard against success/failure callbacks being called more than once + // and guard against a promise AND callback being used together. + let isResolved = false + let wrappedSuccess = function(res: Res) { + if (!isResolved) { + isResolved = true + normalizedSuccessCallback(res) + } + } + let wrappedFailure = function(error: Error) { + if (!isResolved) { + isResolved = true + normalizedFailureCallback(error) + } + } + + let res = func(wrappedSuccess, wrappedFailure) + if (res && typeof res.then === 'function') { + res.then(wrappedSuccess, wrappedFailure) + } +} diff --git a/fullcalendar-main/packages/core/src/util/requestJson.ts b/fullcalendar-main/packages/core/src/util/requestJson.ts new file mode 100644 index 0000000..d812433 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/requestJson.ts @@ -0,0 +1,43 @@ +import { Dictionary } from '../options.js' + +export class JsonRequestError extends Error { + constructor( + message: string, + public response: Response, + ) { + super(message) + } +} + +export function requestJson<ParsedResponse>( + method: string, + url: string, + params: Dictionary, +): Promise<[ParsedResponse, Response]> { + method = method.toUpperCase() + const fetchOptions: RequestInit = { + method, + } + + if (method === 'GET') { + url += (url.indexOf('?') === -1 ? '?' : '&') + + new URLSearchParams(params) + } else { + fetchOptions.body = new URLSearchParams(params) + fetchOptions.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + } + + return fetch(url, fetchOptions).then((fetchRes) => { + if (fetchRes.ok) { + return fetchRes.json().then((parsedResponse: ParsedResponse) => { + return [parsedResponse, fetchRes] + }, () => { + throw new JsonRequestError('Failure parsing JSON', fetchRes) + }) + } else { + throw new JsonRequestError('Request failed', fetchRes) + } + }) +} diff --git a/fullcalendar-main/packages/core/src/util/scrollbar-side.ts b/fullcalendar-main/packages/core/src/util/scrollbar-side.ts new file mode 100644 index 0000000..0732cee --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/scrollbar-side.ts @@ -0,0 +1,31 @@ +import { removeElement, applyStyle } from './dom-manip.js' + +let _isRtlScrollbarOnLeft: boolean | null = null + +export function getIsRtlScrollbarOnLeft() { // responsible for caching the computation + if (_isRtlScrollbarOnLeft === null) { + _isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft() + } + return _isRtlScrollbarOnLeft +} + +function computeIsRtlScrollbarOnLeft() { // creates an offscreen test element, then removes it + let outerEl = document.createElement('div') + applyStyle(outerEl, { + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl', + }) + outerEl.innerHTML = '<div></div>' + + document.body.appendChild(outerEl) + let innerEl = outerEl.firstChild as HTMLElement + let res = innerEl.getBoundingClientRect().left > outerEl.getBoundingClientRect().left + + removeElement(outerEl) + return res +} diff --git a/fullcalendar-main/packages/core/src/util/scrollbar-width.ts b/fullcalendar-main/packages/core/src/util/scrollbar-width.ts new file mode 100644 index 0000000..81f2b0e --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/scrollbar-width.ts @@ -0,0 +1,34 @@ +export interface ScrollbarWidths { + x: number + y: number // TODO: rename to vertical. less confusing when dealing with width/height verbage +} + +let _scrollbarWidths: ScrollbarWidths | undefined + +export function getScrollbarWidths() { // TODO: way to force recompute? + if (!_scrollbarWidths) { + _scrollbarWidths = computeScrollbarWidths() + } + + return _scrollbarWidths +} + +function computeScrollbarWidths(): ScrollbarWidths { + let el = document.createElement('div') + el.style.overflow = 'scroll' + el.style.position = 'absolute' + el.style.top = '-9999px' + el.style.left = '-9999px' + document.body.appendChild(el) + let res = computeScrollbarWidthsForEl(el) + document.body.removeChild(el) + return res +} + +// WARNING: will include border +export function computeScrollbarWidthsForEl(el: HTMLElement): ScrollbarWidths { + return { + x: el.offsetHeight - el.clientHeight, + y: el.offsetWidth - el.clientWidth, + } +} diff --git a/fullcalendar-main/packages/core/src/util/table-styling.ts b/fullcalendar-main/packages/core/src/util/table-styling.ts new file mode 100644 index 0000000..b3e7dc7 --- /dev/null +++ b/fullcalendar-main/packages/core/src/util/table-styling.ts @@ -0,0 +1,31 @@ +let canVGrowWithinCell: boolean + +export function getCanVGrowWithinCell() { + if (canVGrowWithinCell == null) { + canVGrowWithinCell = computeCanVGrowWithinCell() + } + return canVGrowWithinCell +} + +function computeCanVGrowWithinCell() { + // for SSR, because this function is call immediately at top-level + // TODO: just make this logic execute top-level, immediately, instead of doing lazily + if (typeof document === 'undefined') { + return true + } + + let el = document.createElement('div') + el.style.position = 'absolute' + el.style.top = '0px' + el.style.left = '0px' + el.innerHTML = '<table><tr><td><div></div></td></tr></table>' + el.querySelector('table').style.height = '100px' + el.querySelector('div').style.height = '100%' + + document.body.appendChild(el) + + let div = el.querySelector('div') + let possible = div.offsetHeight > 0 + document.body.removeChild(el) + return possible +} diff --git a/fullcalendar-main/packages/core/src/validation.ts b/fullcalendar-main/packages/core/src/validation.ts new file mode 100644 index 0000000..240c19f --- /dev/null +++ b/fullcalendar-main/packages/core/src/validation.ts @@ -0,0 +1,305 @@ +import { EventStore, filterEventStoreDefs } from './structs/event-store.js' +import { DateSpan } from './structs/date-span.js' +import { rangeContainsRange, rangesIntersect, DateRange, OpenDateRange } from './datelib/date-range.js' +import { EventImpl } from './api/EventImpl.js' +import { compileEventUis } from './component/event-rendering.js' +import { excludeInstances } from './reducers/eventStore.js' +import { EventInteractionState } from './interactions/event-interaction-state.js' +import { SplittableProps } from './component/event-splitting.js' +import { mapHash } from './util/object.js' +import { CalendarContext } from './CalendarContext.js' +import { buildDateSpanApiWithContext } from './calendar-utils.js' +import { Constraint } from './structs/constraint.js' +import { expandRecurring } from './structs/recurring-event.js' +import { DateProfile } from './DateProfileGenerator.js' + +// high-level segmenting-aware tester functions +// ------------------------------------------------------------------------------------------------------------------------ + +export function isInteractionValid( + interaction: EventInteractionState, + dateProfile: DateProfile, + context: CalendarContext, +) { + let { instances } = interaction.mutatedEvents + for (let instanceId in instances) { + if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) { + return false + } + } + return isNewPropsValid({ eventDrag: interaction }, context) // HACK: the eventDrag props is used for ALL interactions +} + +export function isDateSelectionValid( + dateSelection: DateSpan, + dateProfile: DateProfile, + context: CalendarContext, +) { + if (!rangeContainsRange(dateProfile.validRange, dateSelection.range)) { + return false + } + return isNewPropsValid({ dateSelection }, context) +} + +function isNewPropsValid(newProps, context: CalendarContext) { + let calendarState = context.getCurrentData() + + let props = { + businessHours: calendarState.businessHours, + dateSelection: '', + eventStore: calendarState.eventStore, + eventUiBases: calendarState.eventUiBases, + eventSelection: '', + eventDrag: null, + eventResize: null, + ...newProps, + } + + return (context.pluginHooks.isPropsValid || isPropsValid)(props, context) +} + +export function isPropsValid(state: SplittableProps, context: CalendarContext, dateSpanMeta = {}, filterConfig?): boolean { + if (state.eventDrag && !isInteractionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false + } + + if (state.dateSelection && !isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig)) { + return false + } + + return true +} + +// Moving Event Validation +// ------------------------------------------------------------------------------------------------------------------------ + +function isInteractionPropsValid(state: SplittableProps, context: CalendarContext, dateSpanMeta: any, filterConfig): boolean { + let currentState = context.getCurrentData() + let interaction = state.eventDrag // HACK: the eventDrag props is used for ALL interactions + + let subjectEventStore = interaction.mutatedEvents + let subjectDefs = subjectEventStore.defs + let subjectInstances = subjectEventStore.instances + let subjectConfigs = compileEventUis( + subjectDefs, + interaction.isEvent ? + state.eventUiBases : + { '': currentState.selectionConfig }, // if not a real event, validate as a selection + ) + + if (filterConfig) { + subjectConfigs = mapHash(subjectConfigs, filterConfig) + } + + // exclude the subject events. TODO: exclude defs too? + let otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances) + + let otherDefs = otherEventStore.defs + let otherInstances = otherEventStore.instances + let otherConfigs = compileEventUis(otherDefs, state.eventUiBases) + + for (let subjectInstanceId in subjectInstances) { + let subjectInstance = subjectInstances[subjectInstanceId] + let subjectRange = subjectInstance.range + let subjectConfig = subjectConfigs[subjectInstance.defId] + let subjectDef = subjectDefs[subjectInstance.defId] + + // constraint + if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, context)) { + return false + } + + // overlap + + let { eventOverlap } = context.options + let eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null + + for (let otherInstanceId in otherInstances) { + let otherInstance = otherInstances[otherInstanceId] + + // intersect! evaluate + if (rangesIntersect(subjectRange, otherInstance.range)) { + let otherOverlap = otherConfigs[otherInstance.defId].overlap + + // consider the other event's overlap. only do this if the subject event is a "real" event + if (otherOverlap === false && interaction.isEvent) { + return false + } + + if (subjectConfig.overlap === false) { + return false + } + + if (eventOverlapFunc && !eventOverlapFunc( + new EventImpl(context, otherDefs[otherInstance.defId], otherInstance), // still event + new EventImpl(context, subjectDef, subjectInstance), // moving event + )) { + return false + } + } + } + + // allow (a function) + + let calendarEventStore = currentState.eventStore // need global-to-calendar, not local to component (splittable)state + + for (let subjectAllow of subjectConfig.allows) { + let subjectDateSpan: DateSpan = { + ...dateSpanMeta, + range: subjectInstance.range, + allDay: subjectDef.allDay, + } + + let origDef = calendarEventStore.defs[subjectDef.defId] + let origInstance = calendarEventStore.instances[subjectInstanceId] + let eventApi + + if (origDef) { // was previously in the calendar + eventApi = new EventImpl(context, origDef, origInstance) + } else { // was an external event + eventApi = new EventImpl(context, subjectDef) // no instance, because had no dates + } + + if (!subjectAllow( + buildDateSpanApiWithContext(subjectDateSpan, context), + eventApi, + )) { + return false + } + } + } + + return true +} + +// Date Selection Validation +// ------------------------------------------------------------------------------------------------------------------------ + +function isDateSelectionPropsValid(state: SplittableProps, context: CalendarContext, dateSpanMeta: any, filterConfig): boolean { + let relevantEventStore = state.eventStore + let relevantDefs = relevantEventStore.defs + let relevantInstances = relevantEventStore.instances + + let selection = state.dateSelection + let selectionRange = selection.range + let { selectionConfig } = context.getCurrentData() + + if (filterConfig) { + selectionConfig = filterConfig(selectionConfig) + } + + // constraint + if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, context)) { + return false + } + + // overlap + + let { selectOverlap } = context.options + let selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null + + for (let relevantInstanceId in relevantInstances) { + let relevantInstance = relevantInstances[relevantInstanceId] + + // intersect! evaluate + if (rangesIntersect(selectionRange, relevantInstance.range)) { + if (selectionConfig.overlap === false) { + return false + } + + if (selectOverlapFunc && !selectOverlapFunc( + new EventImpl(context, relevantDefs[relevantInstance.defId], relevantInstance), + null, + )) { + return false + } + } + } + + // allow (a function) + for (let selectionAllow of selectionConfig.allows) { + let fullDateSpan = { ...dateSpanMeta, ...selection } + + if (!selectionAllow( + buildDateSpanApiWithContext(fullDateSpan, context), + null, + )) { + return false + } + } + + return true +} + +// Constraint Utils +// ------------------------------------------------------------------------------------------------------------------------ + +function allConstraintsPass( + constraints: Constraint[], + subjectRange: DateRange, + otherEventStore: EventStore, + businessHoursUnexpanded: EventStore, + context: CalendarContext, +): boolean { + for (let constraint of constraints) { + if (!anyRangesContainRange( + constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, context), + subjectRange, + )) { + return false + } + } + + return true +} + +function constraintToRanges( + constraint: Constraint, + subjectRange: DateRange, // for expanding a recurring constraint, or expanding business hours + otherEventStore: EventStore, // for if constraint is an even group ID + businessHoursUnexpanded: EventStore, // for if constraint is 'businessHours' + context: CalendarContext, // for expanding businesshours +): OpenDateRange[] { + if (constraint === 'businessHours') { + return eventStoreToRanges( + expandRecurring(businessHoursUnexpanded, subjectRange, context), + ) + } + + if (typeof constraint === 'string') { // an group ID + return eventStoreToRanges( + filterEventStoreDefs(otherEventStore, (eventDef) => eventDef.groupId === constraint), + ) + } + + if (typeof constraint === 'object' && constraint) { // non-null object + return eventStoreToRanges( + expandRecurring(constraint, subjectRange, context), + ) + } + + return [] // if it's false +} + +// TODO: move to event-store file? +function eventStoreToRanges(eventStore: EventStore): DateRange[] { + let { instances } = eventStore + let ranges: DateRange[] = [] + + for (let instanceId in instances) { + ranges.push(instances[instanceId].range) + } + + return ranges +} + +// TODO: move to geom file? +function anyRangesContainRange(outerRanges: DateRange[], innerRange: DateRange): boolean { + for (let outerRange of outerRanges) { + if (rangeContainsRange(outerRange, innerRange)) { + return true + } + } + + return false +} diff --git a/fullcalendar-main/packages/core/src/vdom-util.tsx b/fullcalendar-main/packages/core/src/vdom-util.tsx new file mode 100644 index 0000000..ef61416 --- /dev/null +++ b/fullcalendar-main/packages/core/src/vdom-util.tsx @@ -0,0 +1,66 @@ +/* eslint max-classes-per-file: off */ + +import { Component, Ref } from './preact.js' +import { ViewContextType, ViewContext } from './ViewContext.js' +import { compareObjs, EqualityFuncs, getUnequalProps } from './util/object.js' +import { Dictionary } from './options.js' + +export abstract class PureComponent<Props=Dictionary, State=Dictionary> extends Component<Props, State> { + static addPropsEquality = addPropsEquality + static addStateEquality = addStateEquality + static contextType: any = ViewContextType + + context: ViewContext + propEquality: EqualityFuncs<Props> + stateEquality: EqualityFuncs<State> + + debug: boolean + + shouldComponentUpdate(nextProps: Props, nextState: State) { + if (this.debug) { + // eslint-disable-next-line no-console + console.log(getUnequalProps(nextProps, this.props), getUnequalProps(nextState, this.state)) + } + + return !compareObjs(this.props, nextProps, this.propEquality) || + !compareObjs(this.state, nextState, this.stateEquality) + } + + // HACK for freakin' React StrictMode + safeSetState(newState: Partial<State>): void { + if (!compareObjs(this.state, { ...this.state, ...newState }, this.stateEquality)) { + this.setState(newState) + } + } +} + +PureComponent.prototype.propEquality = {} +PureComponent.prototype.stateEquality = {} + +export abstract class BaseComponent<Props=Dictionary, State=Dictionary> extends PureComponent<Props, State> { + static contextType: any = ViewContextType + + context: ViewContext +} + +function addPropsEquality(this: { prototype: { propEquality: any } }, propEquality) { + let hash = Object.create(this.prototype.propEquality) + Object.assign(hash, propEquality) + this.prototype.propEquality = hash +} + +function addStateEquality(this: { prototype: { stateEquality: any } }, stateEquality) { + let hash = Object.create(this.prototype.stateEquality) + Object.assign(hash, stateEquality) + this.prototype.stateEquality = hash +} + +// use other one +export function setRef<RefType>(ref: Ref<RefType> | void, current: RefType) { + if (typeof ref === 'function') { + ref(current) + } else if (ref) { + // see https://github.com/facebook/react/issues/13029 + (ref as any).current = current + } +} diff --git a/fullcalendar-main/packages/daygrid/.eslintrc.cjs b/fullcalendar-main/packages/daygrid/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/daygrid/README.md b/fullcalendar-main/packages/daygrid/README.md new file mode 100644 index 0000000..93f9916 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/README.md @@ -0,0 +1,32 @@ + +# FullCalendar Day Grid Plugin + +Display events on a [month view](https://fullcalendar.io/docs/month-view) or ["day grid" view](https://fullcalendar.io/docs/daygrid-view) + +## Installation + +Install the necessary packages: + +```sh +npm install @fullcalendar/core @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [dayGridPlugin], + initialView: 'dayGridMonth', + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/daygrid/package.json b/fullcalendar-main/packages/daygrid/package.json new file mode 100644 index 0000000..5901fb4 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/daygrid", + "version": "6.1.11", + "title": "FullCalendar Day Grid Plugin", + "description": "Display events on a month view or \"day grid\" view", + "homepage": "https://fullcalendar.io/docs/month-view", + "keywords": [ + "month", + "month-view" + ], + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./internal": {} + }, + "iifeGlobals": { + ".": "FullCalendar.DayGrid", + "./internal": "FullCalendar.DayGrid.Internal" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/daygrid/src/DayTable.tsx b/fullcalendar-main/packages/daygrid/src/DayTable.tsx new file mode 100644 index 0000000..ce0f9da --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/DayTable.tsx @@ -0,0 +1,72 @@ +import { Duration, CssDimValue } from '@fullcalendar/core' +import { + EventStore, + EventUiHash, + DateSpan, + EventInteractionState, + DayTableModel, + DateComponent, + ViewContext, + DateProfile, +} from '@fullcalendar/core/internal' +import { + createElement, + createRef, + VNode, + RefObject, +} from '@fullcalendar/core/preact' +import { Table } from './Table.js' +import { DayTableSlicer } from './DayTableSlicer.js' + +export interface DayTableProps { + dateProfile: DateProfile, + dayTableModel: DayTableModel + nextDayThreshold: Duration + businessHours: EventStore + eventStore: EventStore + eventUiBases: EventUiHash + dateSelection: DateSpan | null + eventSelection: string + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null + colGroupNode: VNode + tableMinWidth: CssDimValue + renderRowIntro?: () => VNode + dayMaxEvents: boolean | number + dayMaxEventRows: boolean | number + expandRows: boolean + showWeekNumbers: boolean + headerAlignElRef?: RefObject<HTMLElement> // for more popover alignment + clientWidth: number | null + clientHeight: number | null + forPrint: boolean +} + +export class DayTable extends DateComponent<DayTableProps, ViewContext> { + private slicer = new DayTableSlicer() + private tableRef = createRef<Table>() + + render() { + let { props, context } = this + + return ( + <Table + ref={this.tableRef} + {...this.slicer.sliceProps(props, props.dateProfile, props.nextDayThreshold, context, props.dayTableModel)} + dateProfile={props.dateProfile} + cells={props.dayTableModel.cells} + colGroupNode={props.colGroupNode} + tableMinWidth={props.tableMinWidth} + renderRowIntro={props.renderRowIntro} + dayMaxEvents={props.dayMaxEvents} + dayMaxEventRows={props.dayMaxEventRows} + showWeekNumbers={props.showWeekNumbers} + expandRows={props.expandRows} + headerAlignElRef={props.headerAlignElRef} + clientWidth={props.clientWidth} + clientHeight={props.clientHeight} + forPrint={props.forPrint} + /> + ) + } +} diff --git a/fullcalendar-main/packages/daygrid/src/DayTableSlicer.ts b/fullcalendar-main/packages/daygrid/src/DayTableSlicer.ts new file mode 100644 index 0000000..94cbb71 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/DayTableSlicer.ts @@ -0,0 +1,10 @@ +import { DayTableModel, DateRange, Slicer } from '@fullcalendar/core/internal' +import { TableSeg } from './TableSeg.js' + +export class DayTableSlicer extends Slicer<TableSeg, [DayTableModel]> { + forceDayIfListItem = true + + sliceRange(dateRange: DateRange, dayTableModel: DayTableModel): TableSeg[] { + return dayTableModel.sliceRange(dateRange) + } +} diff --git a/fullcalendar-main/packages/daygrid/src/DayTableView.tsx b/fullcalendar-main/packages/daygrid/src/DayTableView.tsx new file mode 100644 index 0000000..62de093 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/DayTableView.tsx @@ -0,0 +1,74 @@ +import { + DayHeader, + DateProfileGenerator, + DateProfile, + memoize, + DaySeriesModel, + DayTableModel, + ChunkContentCallbackArgs, +} from '@fullcalendar/core/internal' +import { createElement, createRef } from '@fullcalendar/core/preact' +import { TableView } from './TableView.js' +import { DayTable } from './DayTable.js' + +export class DayTableView extends TableView { + private buildDayTableModel = memoize(buildDayTableModel) + private headerRef = createRef<DayHeader>() + private tableRef = createRef<DayTable>() + + render() { + let { options, dateProfileGenerator } = this.context + let { props } = this + let dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator) + + let headerContent = options.dayHeaders && ( + <DayHeader + ref={this.headerRef} + dateProfile={props.dateProfile} + dates={dayTableModel.headerDates} + datesRepDistinctDays={dayTableModel.rowCnt === 1} + /> + ) + + let bodyContent = (contentArg: ChunkContentCallbackArgs) => ( + <DayTable + ref={this.tableRef} + dateProfile={props.dateProfile} + dayTableModel={dayTableModel} + businessHours={props.businessHours} + dateSelection={props.dateSelection} + eventStore={props.eventStore} + eventUiBases={props.eventUiBases} + eventSelection={props.eventSelection} + eventDrag={props.eventDrag} + eventResize={props.eventResize} + nextDayThreshold={options.nextDayThreshold} + colGroupNode={contentArg.tableColGroupNode} + tableMinWidth={contentArg.tableMinWidth} + dayMaxEvents={options.dayMaxEvents} + dayMaxEventRows={options.dayMaxEventRows} + showWeekNumbers={options.weekNumbers} + expandRows={!props.isHeightAuto} + headerAlignElRef={this.headerElRef} + clientWidth={contentArg.clientWidth} + clientHeight={contentArg.clientHeight} + forPrint={props.forPrint} + /> + ) + + return options.dayMinWidth + ? this.renderHScrollLayout(headerContent, bodyContent, dayTableModel.colCnt, options.dayMinWidth) + : this.renderSimpleLayout(headerContent, bodyContent) + } + + // can't override any lifecycle methods from parent +} + +export function buildDayTableModel(dateProfile: DateProfile, dateProfileGenerator: DateProfileGenerator) { + let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator) + + return new DayTableModel( + daySeries, + /year|month|week/.test(dateProfile.currentRangeUnit), + ) +} diff --git a/fullcalendar-main/packages/daygrid/src/Table.tsx b/fullcalendar-main/packages/daygrid/src/Table.tsx new file mode 100644 index 0000000..58cac55 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/Table.tsx @@ -0,0 +1,135 @@ +import { CssDimValue } from '@fullcalendar/core' +import { DateComponent, formatIsoMonthStr, formatDayString, DateProfile } from '@fullcalendar/core/internal' +import { VNode, RefObject, createElement, createRef } from '@fullcalendar/core/preact' +import { TableRows, TableRowsProps } from './TableRows.js' + +export interface TableProps extends TableRowsProps { + colGroupNode: VNode + tableMinWidth: CssDimValue + expandRows: boolean + headerAlignElRef?: RefObject<HTMLElement> +} + +export class Table extends DateComponent<TableProps> { + private elRef = createRef<HTMLDivElement>() + private needsScrollReset = false + + render() { + let { props } = this + let { dayMaxEventRows, dayMaxEvents, expandRows } = props + let limitViaBalanced = dayMaxEvents === true || dayMaxEventRows === true + + // if rows can't expand to fill fixed height, can't do balanced-height event limit + // TODO: best place to normalize these options? + if (limitViaBalanced && !expandRows) { + limitViaBalanced = false + dayMaxEventRows = null + dayMaxEvents = null + } + + let classNames = [ + 'fc-daygrid-body', // necessary for TableRows DnD parent + limitViaBalanced ? 'fc-daygrid-body-balanced' : 'fc-daygrid-body-unbalanced', // will all row heights be equal? + expandRows ? '' : 'fc-daygrid-body-natural', // will height of one row depend on the others? + ] + + return ( + <div + ref={this.elRef} + className={classNames.join(' ')} + style={{ + // these props are important to give this wrapper correct dimensions for interactions + // TODO: if we set it here, can we avoid giving to inner tables? + width: props.clientWidth, + minWidth: props.tableMinWidth, + }} + > + <table + role="presentation" + className="fc-scrollgrid-sync-table" + style={{ + width: props.clientWidth, + minWidth: props.tableMinWidth, + height: expandRows ? props.clientHeight : '', + }} + > + {props.colGroupNode} + <tbody role="presentation"> + <TableRows + dateProfile={props.dateProfile} + cells={props.cells} + renderRowIntro={props.renderRowIntro} + showWeekNumbers={props.showWeekNumbers} + clientWidth={props.clientWidth} + clientHeight={props.clientHeight} + businessHourSegs={props.businessHourSegs} + bgEventSegs={props.bgEventSegs} + fgEventSegs={props.fgEventSegs} + dateSelectionSegs={props.dateSelectionSegs} + eventSelection={props.eventSelection} + eventDrag={props.eventDrag} + eventResize={props.eventResize} + dayMaxEvents={dayMaxEvents} + dayMaxEventRows={dayMaxEventRows} + forPrint={props.forPrint} + isHitComboAllowed={props.isHitComboAllowed} + /> + </tbody> + </table> + </div> + ) + } + + componentDidMount(): void { + this.requestScrollReset() + } + + componentDidUpdate(prevProps: TableProps): void { + if (prevProps.dateProfile !== this.props.dateProfile) { + this.requestScrollReset() + } else { + this.flushScrollReset() + } + } + + requestScrollReset() { + this.needsScrollReset = true + this.flushScrollReset() + } + + flushScrollReset() { + if ( + this.needsScrollReset && + this.props.clientWidth // sizes computed? + ) { + const subjectEl = getScrollSubjectEl(this.elRef.current, this.props.dateProfile) + + if (subjectEl) { + const originEl = subjectEl.closest('.fc-daygrid-body') + const scrollEl = originEl.closest('.fc-scroller') + const scrollTop = subjectEl.getBoundingClientRect().top - + originEl.getBoundingClientRect().top + + scrollEl.scrollTop = scrollTop ? (scrollTop + 1) : 0 // overcome border + } + + this.needsScrollReset = false + } + } +} + +function getScrollSubjectEl(containerEl: HTMLElement, dateProfile: DateProfile): HTMLElement | undefined { + let el: HTMLElement + + if (dateProfile.currentRangeUnit.match(/year|month/)) { + el = containerEl.querySelector(`[data-date="${formatIsoMonthStr(dateProfile.currentDate)}-01"]`) + // even if view is month-based, first-of-month might be hidden... + } + + if (!el) { + el = containerEl.querySelector(`[data-date="${formatDayString(dateProfile.currentDate)}"]`) + // could still be hidden if an interior-view hidden day + } + + return el +} diff --git a/fullcalendar-main/packages/daygrid/src/TableBlockEvent.tsx b/fullcalendar-main/packages/daygrid/src/TableBlockEvent.tsx new file mode 100644 index 0000000..09e97c8 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableBlockEvent.tsx @@ -0,0 +1,23 @@ +import { StandardEvent, BaseComponent, MinimalEventProps } from '@fullcalendar/core/internal' +import { createElement } from '@fullcalendar/core/preact' +import { DEFAULT_TABLE_EVENT_TIME_FORMAT } from './event-rendering.js' + +export interface TableBlockEventProps extends MinimalEventProps { + defaultDisplayEventEnd: boolean +} + +export class TableBlockEvent extends BaseComponent<TableBlockEventProps> { + render() { + let { props } = this + + return ( + <StandardEvent + {...props} + elClasses={['fc-daygrid-event', 'fc-daygrid-block-event', 'fc-h-event']} + defaultTimeFormat={DEFAULT_TABLE_EVENT_TIME_FORMAT} + defaultDisplayEventEnd={props.defaultDisplayEventEnd} + disableResizing={!props.seg.eventRange.def.allDay} + /> + ) + } +} diff --git a/fullcalendar-main/packages/daygrid/src/TableCell.tsx b/fullcalendar-main/packages/daygrid/src/TableCell.tsx new file mode 100644 index 0000000..4c5ea9e --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableCell.tsx @@ -0,0 +1,185 @@ +import { CssDimValue, DayCellContentArg } from '@fullcalendar/core' +import { + DateMarker, + DateComponent, + DateRange, + buildNavLinkAttrs, + WeekNumberContainer, + DayCellContainer, + DateProfile, + setRef, + createFormatter, + Dictionary, + EventSegUiInteractionState, + getUniqueDomId, + hasCustomDayCellContent, + addMs, + DateEnv, +} from '@fullcalendar/core/internal' +import { + Ref, + ComponentChildren, + createElement, + createRef, + ComponentChild, + Fragment, +} from '@fullcalendar/core/preact' +import { TableCellMoreLink } from './TableCellMoreLink.js' +import { TableSegPlacement } from './event-placement.js' + +export interface TableCellProps { + elRef?: Ref<HTMLTableCellElement> + date: DateMarker + dateProfile: DateProfile + extraRenderProps?: Dictionary + extraDataAttrs?: Dictionary + extraClassNames?: string[] + extraDateSpan?: Dictionary + innerElRef?: Ref<HTMLDivElement> + bgContent: ComponentChildren + fgContentElRef?: Ref<HTMLDivElement> // TODO: rename!!! classname confusion. is the "event" div + fgContent: ComponentChildren + moreCnt: number + moreMarginTop: number + showDayNumber: boolean + showWeekNumber: boolean + forceDayTop: boolean + todayRange: DateRange + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + singlePlacements: TableSegPlacement[] + minHeight?: CssDimValue +} + +const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'narrow' }) + +export class TableCell extends DateComponent<TableCellProps> { + private rootElRef = createRef<HTMLElement>() + state = { + dayNumberId: getUniqueDomId(), + } + + render() { + let { context, props, state, rootElRef } = this + let { options, dateEnv } = context + let { date, dateProfile } = props + + // TODO: memoize this? + const isMonthStart = props.showDayNumber && + shouldDisplayMonthStart(date, dateProfile.currentRange, dateEnv) + + return ( + <DayCellContainer + elTag="td" + elRef={this.handleRootEl} + elClasses={[ + 'fc-daygrid-day', + ...(props.extraClassNames || []), + ]} + elAttrs={{ + ...props.extraDataAttrs, + ...(props.showDayNumber ? { 'aria-labelledby': state.dayNumberId } : {}), + role: 'gridcell', + }} + defaultGenerator={renderTopInner} + date={date} + dateProfile={dateProfile} + todayRange={props.todayRange} + showDayNumber={props.showDayNumber} + isMonthStart={isMonthStart} + extraRenderProps={props.extraRenderProps} + > + {(InnerContent, renderProps) => ( + <div + ref={props.innerElRef} + className="fc-daygrid-day-frame fc-scrollgrid-sync-inner" + style={{ minHeight: props.minHeight }} + > + {props.showWeekNumber && ( + <WeekNumberContainer + elTag="a" + elClasses={['fc-daygrid-week-number']} + elAttrs={buildNavLinkAttrs(context, date, 'week')} + date={date} + defaultFormat={DEFAULT_WEEK_NUM_FORMAT} + /> + )} + {!renderProps.isDisabled && + (props.showDayNumber || hasCustomDayCellContent(options) || props.forceDayTop) ? ( + <div className="fc-daygrid-day-top"> + <InnerContent + elTag="a" + elClasses={[ + 'fc-daygrid-day-number', + isMonthStart && 'fc-daygrid-month-start', + ]} + elAttrs={{ + ...buildNavLinkAttrs(context, date), + id: state.dayNumberId, + }} + /> + </div> + ) : props.showDayNumber ? ( + // for creating correct amount of space (see issue #7162) + <div className="fc-daygrid-day-top" style={{ visibility: 'hidden' }}> + <a className="fc-daygrid-day-number"> </a> + </div> + ) : undefined} + <div + className="fc-daygrid-day-events" + ref={props.fgContentElRef} + > + {props.fgContent} + <div className="fc-daygrid-day-bottom" style={{ marginTop: props.moreMarginTop }}> + <TableCellMoreLink + allDayDate={date} + singlePlacements={props.singlePlacements} + moreCnt={props.moreCnt} + alignmentElRef={rootElRef} + alignGridTop={!props.showDayNumber} + extraDateSpan={props.extraDateSpan} + dateProfile={props.dateProfile} + eventSelection={props.eventSelection} + eventDrag={props.eventDrag} + eventResize={props.eventResize} + todayRange={props.todayRange} + /> + </div> + </div> + <div className="fc-daygrid-day-bg"> + {props.bgContent} + </div> + </div> + )} + </DayCellContainer> + ) + } + + handleRootEl = (el: HTMLElement) => { + setRef(this.rootElRef, el) + setRef(this.props.elRef, el) + } +} + +function renderTopInner(props: DayCellContentArg): ComponentChild { + return props.dayNumberText || <Fragment> </Fragment> +} + +function shouldDisplayMonthStart(date: DateMarker, currentRange: DateRange, dateEnv: DateEnv): boolean { + const { start: currentStart, end: currentEnd } = currentRange + const currentEndIncl = addMs(currentEnd, -1) + const currentFirstYear = dateEnv.getYear(currentStart) + const currentFirstMonth = dateEnv.getMonth(currentStart) + const currentLastYear = dateEnv.getYear(currentEndIncl) + const currentLastMonth = dateEnv.getMonth(currentEndIncl) + + // spans more than one month? + return !(currentFirstYear === currentLastYear && currentFirstMonth === currentLastMonth) && + Boolean( + // first date in current view? + date.valueOf() === currentStart.valueOf() || + // a month-start that's within the current range? + (dateEnv.getDay(date) === 1 && date.valueOf() < currentEnd.valueOf()), + ) +} diff --git a/fullcalendar-main/packages/daygrid/src/TableCellMoreLink.tsx b/fullcalendar-main/packages/daygrid/src/TableCellMoreLink.tsx new file mode 100644 index 0000000..668c6d7 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableCellMoreLink.tsx @@ -0,0 +1,115 @@ +import { + MoreLinkContainer, + BaseComponent, + memoize, + DateMarker, + Dictionary, + DateProfile, + DateRange, + EventSegUiInteractionState, + getSegMeta, +} from '@fullcalendar/core/internal' +import { createElement, RefObject, Fragment } from '@fullcalendar/core/preact' +import { TableSegPlacement } from './event-placement.js' +import { hasListItemDisplay } from './event-rendering.js' +import { TableBlockEvent } from './TableBlockEvent.js' +import { TableListItemEvent } from './TableListItemEvent.js' +import { TableSeg } from './TableSeg.js' + +export interface TableCellMoreLinkProps { + allDayDate: DateMarker + singlePlacements: TableSegPlacement[] + moreCnt: number + alignmentElRef: RefObject<HTMLElement> + alignGridTop: boolean // for popover + extraDateSpan?: Dictionary + dateProfile: DateProfile + todayRange: DateRange + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null +} + +export class TableCellMoreLink extends BaseComponent<TableCellMoreLinkProps> { + compileSegs = memoize(compileSegs) + + render() { + let { props } = this + let { allSegs, invisibleSegs } = this.compileSegs(props.singlePlacements) + + return ( + <MoreLinkContainer + elClasses={['fc-daygrid-more-link']} + dateProfile={props.dateProfile} + todayRange={props.todayRange} + allDayDate={props.allDayDate} + moreCnt={props.moreCnt} + allSegs={allSegs} + hiddenSegs={invisibleSegs} + alignmentElRef={props.alignmentElRef} + alignGridTop={props.alignGridTop} + extraDateSpan={props.extraDateSpan} + popoverContent={() => { + let isForcedInvisible = + (props.eventDrag ? props.eventDrag.affectedInstances : null) || + (props.eventResize ? props.eventResize.affectedInstances : null) || + {} + return ( + <Fragment> + {allSegs.map((seg) => { + let instanceId = seg.eventRange.instance.instanceId + return ( + <div + className="fc-daygrid-event-harness" + key={instanceId} + style={{ + visibility: isForcedInvisible[instanceId] ? 'hidden' : ('' as any), + }} + > + {hasListItemDisplay(seg) ? ( + <TableListItemEvent + seg={seg} + isDragging={false} + isSelected={instanceId === props.eventSelection} + defaultDisplayEventEnd={false} + {...getSegMeta(seg, props.todayRange)} + /> + ) : ( + <TableBlockEvent + seg={seg} + isDragging={false} + isResizing={false} + isDateSelecting={false} + isSelected={instanceId === props.eventSelection} + defaultDisplayEventEnd={false} + {...getSegMeta(seg, props.todayRange)} + /> + )} + </div> + ) + })} + </Fragment> + ) + }} + /> + ) + } +} + +function compileSegs(singlePlacements: TableSegPlacement[]): { + allSegs: TableSeg[] + invisibleSegs: TableSeg[] +} { + let allSegs: TableSeg[] = [] + let invisibleSegs: TableSeg[] = [] + + for (let placement of singlePlacements) { + allSegs.push(placement.seg) + + if (!placement.isVisible) { + invisibleSegs.push(placement.seg) + } + } + + return { allSegs, invisibleSegs } +} diff --git a/fullcalendar-main/packages/daygrid/src/TableDateProfileGenerator.ts b/fullcalendar-main/packages/daygrid/src/TableDateProfileGenerator.ts new file mode 100644 index 0000000..8d15dba --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableDateProfileGenerator.ts @@ -0,0 +1,60 @@ +import { + DateProfileGenerator, + addWeeks, diffWeeks, + DateRange, + DateEnv, + addDays, +} from '@fullcalendar/core/internal' + +export class TableDateProfileGenerator extends DateProfileGenerator { + // Computes the date range that will be rendered + buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay): DateRange { + let renderRange = super.buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) + let { props } = this + + return buildDayTableRenderRange({ + currentRange: renderRange, // ??? + snapToWeek: /^(year|month)$/.test(currentRangeUnit), + fixedWeekCount: props.fixedWeekCount, + dateEnv: props.dateEnv, + }) + } +} + +export function buildDayTableRenderRange(props: { + currentRange: DateRange, + snapToWeek: boolean, + fixedWeekCount: boolean, + dateEnv: DateEnv, +}): DateRange { + let { dateEnv, currentRange } = props + let { start, end } = currentRange + let endOfWeek + + // year and month views should be aligned with weeks. this is already done for week + if (props.snapToWeek) { + start = dateEnv.startOfWeek(start) + + // make end-of-week if not already + endOfWeek = dateEnv.startOfWeek(end) + if (endOfWeek.valueOf() !== end.valueOf()) { + end = addWeeks(endOfWeek, 1) + } + } + + // ensure 6 weeks + if (props.fixedWeekCount) { + // TODO: instead of these date-math gymnastics (for multimonth view), + // compute dateprofiles of all months, then use start of first and end of last. + let lastMonthRenderStart = dateEnv.startOfWeek( + dateEnv.startOfMonth(addDays(currentRange.end, -1)), + ) + + let rowCnt = Math.ceil( // could be partial weeks due to hiddenDays + diffWeeks(lastMonthRenderStart, end), + ) + end = addWeeks(end, 6 - rowCnt) + } + + return { start, end } +} diff --git a/fullcalendar-main/packages/daygrid/src/TableListItemEvent.tsx b/fullcalendar-main/packages/daygrid/src/TableListItemEvent.tsx new file mode 100644 index 0000000..9ad557d --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableListItemEvent.tsx @@ -0,0 +1,67 @@ +import { EventContentArg } from '@fullcalendar/core' +import { + BaseComponent, + Seg, + buildSegTimeText, + EventContainer, + getSegAnchorAttrs, +} from '@fullcalendar/core/internal' +import { createElement, Fragment } from '@fullcalendar/core/preact' +import { DEFAULT_TABLE_EVENT_TIME_FORMAT } from './event-rendering.js' + +export interface DotTableEventProps { + seg: Seg + isDragging: boolean + isSelected: boolean + isPast: boolean + isFuture: boolean + isToday: boolean + defaultDisplayEventEnd: boolean + children?: never +} + +export class TableListItemEvent extends BaseComponent<DotTableEventProps> { + render() { + let { props, context } = this + let { options } = context + let { seg } = props + let timeFormat = options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT + let timeText = buildSegTimeText( + seg, + timeFormat, + context, + true, + props.defaultDisplayEventEnd, + ) + + return ( + <EventContainer + {...props} + elTag="a" + elClasses={['fc-daygrid-event', 'fc-daygrid-dot-event']} + elAttrs={getSegAnchorAttrs(props.seg, context)} + defaultGenerator={renderInnerContent} + timeText={timeText} + isResizing={false} + isDateSelecting={false} + /> + ) + } +} + +function renderInnerContent(renderProps: EventContentArg) { + return ( + <Fragment> + <div + className="fc-daygrid-event-dot" + style={{ borderColor: renderProps.borderColor || renderProps.backgroundColor }} + /> + {renderProps.timeText && ( + <div className="fc-event-time">{renderProps.timeText}</div> + )} + <div className="fc-event-title"> + {renderProps.event.title || <Fragment> </Fragment>} + </div> + </Fragment> + ) +} diff --git a/fullcalendar-main/packages/daygrid/src/TableRow.tsx b/fullcalendar-main/packages/daygrid/src/TableRow.tsx new file mode 100644 index 0000000..0e93792 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableRow.tsx @@ -0,0 +1,424 @@ +import { CssDimValue } from '@fullcalendar/core' +import { + EventSegUiInteractionState, + DateComponent, + PositionCache, + RefMap, + DateRange, + getSegMeta, + DateProfile, + BgEvent, + renderFill, + isPropsEqual, + buildEventRangeKey, + sortEventSegs, + DayTableCell, +} from '@fullcalendar/core/internal' +import { + VNode, + createElement, + Fragment, + createRef, +} from '@fullcalendar/core/preact' +import { TableSeg, splitSegsByFirstCol } from './TableSeg.js' +import { TableCell } from './TableCell.js' +import { TableListItemEvent } from './TableListItemEvent.js' +import { TableBlockEvent } from './TableBlockEvent.js' +import { computeFgSegPlacement, generateSegKey, generateSegUid, TableSegPlacement } from './event-placement.js' +import { hasListItemDisplay } from './event-rendering.js' + +// TODO: attach to window resize? + +export interface TableRowProps { + cells: DayTableCell[] + renderIntro?: () => VNode + businessHourSegs: TableSeg[] + bgEventSegs: TableSeg[] + fgEventSegs: TableSeg[] + dateSelectionSegs: TableSeg[] + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + dayMaxEvents: boolean | number + dayMaxEventRows: boolean | number + clientWidth: number | null + clientHeight: number | null // simply for causing an updateSize, for when liquid height + dateProfile: DateProfile + todayRange: DateRange + showDayNumbers: boolean + showWeekNumbers: boolean + forPrint: boolean + cellMinHeight?: CssDimValue +} + +interface TableRowState { + framePositions: PositionCache + maxContentHeight: number | null + segHeights: { [segUid: string]: number } // integers +} + +export class TableRow extends DateComponent<TableRowProps, TableRowState> { + private cellElRefs = new RefMap<HTMLTableCellElement>() // the <td> + private frameElRefs = new RefMap<HTMLElement>() // the fc-daygrid-day-frame + private fgElRefs = new RefMap<HTMLDivElement>() // the fc-daygrid-day-events + private segHarnessRefs = new RefMap<HTMLDivElement>() // indexed by "instanceId:firstCol" + private rootElRef = createRef<HTMLTableRowElement>() + + state: TableRowState = { + framePositions: null, + maxContentHeight: null, + segHeights: {}, + } + + render() { + let { props, state, context } = this + let { options } = context + let colCnt = props.cells.length + + let businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt) + let bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt) + let highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt) + let mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt) + + let { singleColPlacements, multiColPlacements, moreCnts, moreMarginTops } = computeFgSegPlacement( + sortEventSegs(props.fgEventSegs, options.eventOrder) as TableSeg[], + props.dayMaxEvents, + props.dayMaxEventRows, + options.eventOrderStrict, + state.segHeights, + state.maxContentHeight, + props.cells, + ) + + let isForcedInvisible = // TODO: messy way to compute this + (props.eventDrag && props.eventDrag.affectedInstances) || + (props.eventResize && props.eventResize.affectedInstances) || + {} + + return ( + <tr ref={this.rootElRef} role="row"> + {props.renderIntro && props.renderIntro()} + {props.cells.map((cell, col) => { + let normalFgNodes = this.renderFgSegs( + col, + props.forPrint ? singleColPlacements[col] : multiColPlacements[col], + props.todayRange, + isForcedInvisible, + ) + + let mirrorFgNodes = this.renderFgSegs( + col, + buildMirrorPlacements(mirrorSegsByCol[col], multiColPlacements), + props.todayRange, + {}, + Boolean(props.eventDrag), + Boolean(props.eventResize), + false, // date-selecting (because mirror is never drawn for date selection) + ) + + return ( + <TableCell + key={cell.key} + elRef={this.cellElRefs.createRef(cell.key)} + innerElRef={this.frameElRefs.createRef(cell.key) /* FF <td> problem, but okay to use for left/right. TODO: rename prop */} + dateProfile={props.dateProfile} + date={cell.date} + showDayNumber={props.showDayNumbers} + showWeekNumber={props.showWeekNumbers && col === 0} + forceDayTop={props.showWeekNumbers /* even displaying weeknum for row, not necessarily day */} + todayRange={props.todayRange} + eventSelection={props.eventSelection} + eventDrag={props.eventDrag} + eventResize={props.eventResize} + extraRenderProps={cell.extraRenderProps} + extraDataAttrs={cell.extraDataAttrs} + extraClassNames={cell.extraClassNames} + extraDateSpan={cell.extraDateSpan} + moreCnt={moreCnts[col]} + moreMarginTop={moreMarginTops[col]} + singlePlacements={singleColPlacements[col]} + fgContentElRef={this.fgElRefs.createRef(cell.key)} + fgContent={( // Fragment scopes the keys + <Fragment> + <Fragment>{normalFgNodes}</Fragment> + <Fragment>{mirrorFgNodes}</Fragment> + </Fragment> + )} + bgContent={( // Fragment scopes the keys + <Fragment> + {this.renderFillSegs(highlightSegsByCol[col], 'highlight')} + {this.renderFillSegs(businessHoursByCol[col], 'non-business')} + {this.renderFillSegs(bgEventSegsByCol[col], 'bg-event')} + </Fragment> + )} + minHeight={props.cellMinHeight} + /> + ) + })} + </tr> + ) + } + + componentDidMount() { + this.updateSizing(true) + this.context.addResizeHandler(this.handleResize) + } + + componentDidUpdate(prevProps: TableRowProps, prevState: TableRowState) { + let currentProps = this.props + + this.updateSizing( + !isPropsEqual(prevProps, currentProps), + ) + } + + componentWillUnmount() { + this.context.removeResizeHandler(this.handleResize) + } + + handleResize = (isForced: boolean) => { + if (isForced) { + this.updateSizing(true) // isExternal=true + } + } + + getHighlightSegs(): TableSeg[] { + let { props } = this + + if (props.eventDrag && props.eventDrag.segs.length) { // messy check + return props.eventDrag.segs as TableSeg[] + } + + if (props.eventResize && props.eventResize.segs.length) { // messy check + return props.eventResize.segs as TableSeg[] + } + + return props.dateSelectionSegs + } + + getMirrorSegs(): TableSeg[] { + let { props } = this + + if (props.eventResize && props.eventResize.segs.length) { // messy check + return props.eventResize.segs as TableSeg[] + } + + return [] + } + + renderFgSegs( + col: number, + segPlacements: TableSegPlacement[], + todayRange: DateRange, + isForcedInvisible: { [instanceId: string]: any }, + isDragging?: boolean, + isResizing?: boolean, + isDateSelecting?: boolean, + ): VNode[] { + let { context } = this + let { eventSelection } = this.props + let { framePositions } = this.state + let defaultDisplayEventEnd = this.props.cells.length === 1 // colCnt === 1 + let isMirror = isDragging || isResizing || isDateSelecting + let nodes: VNode[] = [] + + if (framePositions) { + for (let placement of segPlacements) { + let { seg } = placement + let { instanceId } = seg.eventRange.instance + let isVisible = placement.isVisible && !isForcedInvisible[instanceId] + let isAbsolute = placement.isAbsolute + let left: CssDimValue = '' + let right: CssDimValue = '' + + if (isAbsolute) { + if (context.isRtl) { + right = 0 + left = framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol] + } else { + left = 0 + right = framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol] + } + } + + /* + known bug: events that are force to be list-item but span multiple days still take up space in later columns + todo: in print view, for multi-day events, don't display title within non-start/end segs + */ + nodes.push( + <div + className={'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : '')} + key={generateSegKey(seg)} + ref={isMirror ? null : this.segHarnessRefs.createRef(generateSegUid(seg))} + style={{ + visibility: isVisible ? ('' as any) : 'hidden', + marginTop: isAbsolute ? '' : placement.marginTop, + top: isAbsolute ? placement.absoluteTop : '', + left, + right, + }} + > + {hasListItemDisplay(seg) ? ( + <TableListItemEvent + seg={seg} + isDragging={isDragging} + isSelected={instanceId === eventSelection} + defaultDisplayEventEnd={defaultDisplayEventEnd} + {...getSegMeta(seg, todayRange)} + /> + ) : ( + <TableBlockEvent + seg={seg} + isDragging={isDragging} + isResizing={isResizing} + isDateSelecting={isDateSelecting} + isSelected={instanceId === eventSelection} + defaultDisplayEventEnd={defaultDisplayEventEnd} + {...getSegMeta(seg, todayRange)} + /> + )} + </div>, + ) + } + } + + return nodes + } + + renderFillSegs(segs: TableSeg[], fillType: string): VNode { + let { isRtl } = this.context + let { todayRange } = this.props + let { framePositions } = this.state + let nodes: VNode[] = [] + + if (framePositions) { + for (let seg of segs) { + let leftRightCss = isRtl ? { + right: 0, + left: framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol], + } : { + left: 0, + right: framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol], + } + + nodes.push( + <div + key={buildEventRangeKey(seg.eventRange)} + className="fc-daygrid-bg-harness" + style={leftRightCss} + > + {fillType === 'bg-event' ? + <BgEvent seg={seg} {...getSegMeta(seg, todayRange)} /> : + renderFill(fillType)} + </div>, + ) + } + } + + return createElement(Fragment, {}, ...nodes) + } + + updateSizing(isExternalSizingChange) { + let { props, state, frameElRefs } = this + + if ( + !props.forPrint && + props.clientWidth !== null // positioning ready? + ) { + if (isExternalSizingChange) { + let frameEls = props.cells.map((cell) => frameElRefs.currentMap[cell.key]) + + if (frameEls.length) { + let originEl = this.rootElRef.current + let newPositionCache = new PositionCache( + originEl, + frameEls, + true, // isHorizontal + false, + ) + + if (!state.framePositions || !state.framePositions.similarTo(newPositionCache)) { + this.setState({ // will trigger isCellPositionsChanged... + framePositions: new PositionCache( + originEl, + frameEls, + true, // isHorizontal + false, + ), + }) + } + } + } + + const oldSegHeights = this.state.segHeights + const newSegHeights = this.querySegHeights() + const limitByContentHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true + + this.safeSetState({ + // HACK to prevent oscillations of events being shown/hidden from max-event-rows + // Essentially, once you compute an element's height, never null-out. + // TODO: always display all events, as visibility:hidden? + segHeights: { ...oldSegHeights, ...newSegHeights }, + + maxContentHeight: limitByContentHeight ? this.computeMaxContentHeight() : null, + }) + } + } + + querySegHeights() { + let segElMap = this.segHarnessRefs.currentMap + let segHeights: { [segUid: string]: number } = {} + + // get the max height amongst instance segs + for (let segUid in segElMap) { + let height = Math.round(segElMap[segUid].getBoundingClientRect().height) + segHeights[segUid] = Math.max(segHeights[segUid] || 0, height) + } + + return segHeights + } + + computeMaxContentHeight() { + let firstKey = this.props.cells[0].key + let cellEl = this.cellElRefs.currentMap[firstKey] + let fcContainerEl = this.fgElRefs.currentMap[firstKey] + + return cellEl.getBoundingClientRect().bottom - fcContainerEl.getBoundingClientRect().top + } + + public getCellEls() { + let elMap = this.cellElRefs.currentMap + + return this.props.cells.map((cell) => elMap[cell.key]) + } +} + +TableRow.addStateEquality({ + segHeights: isPropsEqual, +}) + +function buildMirrorPlacements(mirrorSegs: TableSeg[], colPlacements: TableSegPlacement[][]): TableSegPlacement[] { + if (!mirrorSegs.length) { + return [] + } + let topsByInstanceId = buildAbsoluteTopHash(colPlacements) // TODO: cache this at first render? + return mirrorSegs.map((seg: TableSeg) => ({ + seg, + isVisible: true, + isAbsolute: true, + absoluteTop: topsByInstanceId[seg.eventRange.instance.instanceId], + marginTop: 0, + })) +} + +function buildAbsoluteTopHash(colPlacements: TableSegPlacement[][]): { [instanceId: string]: number } { + let topsByInstanceId: { [instanceId: string]: number } = {} + + for (let placements of colPlacements) { + for (let placement of placements) { + topsByInstanceId[placement.seg.eventRange.instance.instanceId] = placement.absoluteTop + } + } + + return topsByInstanceId +} diff --git a/fullcalendar-main/packages/daygrid/src/TableRows.tsx b/fullcalendar-main/packages/daygrid/src/TableRows.tsx new file mode 100644 index 0000000..80b6ce1 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableRows.tsx @@ -0,0 +1,200 @@ +import { + EventSegUiInteractionState, + DateComponent, + PositionCache, + memoize, + addDays, + RefMap, + DateRange, + NowTimer, + DateMarker, + DateProfile, + Hit, + DayTableCell, +} from '@fullcalendar/core/internal' +import { VNode, createElement, Fragment } from '@fullcalendar/core/preact' +import { TableSeg, splitSegsByRow, splitInteractionByRow } from './TableSeg.js' +import { TableRow } from './TableRow.js' + +export interface TableRowsProps { + dateProfile: DateProfile + cells: DayTableCell[][] // cells-BY-ROW + renderRowIntro?: () => VNode + showWeekNumbers: boolean + clientWidth: number | null // of outer view container. weird, i know + clientHeight: number | null // of outer view container. weird, i know + businessHourSegs: TableSeg[] + bgEventSegs: TableSeg[] + fgEventSegs: TableSeg[] + dateSelectionSegs: TableSeg[] + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + dayMaxEvents: boolean | number + dayMaxEventRows: boolean | number + forPrint: boolean + isHitComboAllowed?: (hit0: Hit, hit1: Hit) => boolean +} + +export class TableRows extends DateComponent<TableRowsProps> { + private splitBusinessHourSegs = memoize(splitSegsByRow) + private splitBgEventSegs = memoize(splitSegsByRow) + private splitFgEventSegs = memoize(splitSegsByRow) + private splitDateSelectionSegs = memoize(splitSegsByRow) + private splitEventDrag = memoize(splitInteractionByRow) + private splitEventResize = memoize(splitInteractionByRow) + private rootEl: HTMLElement + private rowRefs = new RefMap<TableRow>() + private rowPositions: PositionCache + private colPositions: PositionCache + + render() { + let { props, context } = this + let rowCnt = props.cells.length + + let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt) + let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt) + let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt) + let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt) + let eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt) + let eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt) + + // for DayGrid view with many rows, force a min-height on cells so doesn't appear squished + // choose 7 because a month view will have max 6 rows + let cellMinHeight = (rowCnt >= 7 && props.clientWidth) ? + props.clientWidth / context.options.aspectRatio / 6 : + null + + return ( + <NowTimer unit="day">{(nowDate: DateMarker, todayRange: DateRange) => ( + <Fragment> + {props.cells.map((cells, row) => ( + <TableRow + ref={this.rowRefs.createRef(row)} + key={ + cells.length + ? cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */ + : row // in case there are no cells (like when resource view is loading) + } + showDayNumbers={rowCnt > 1} + showWeekNumbers={props.showWeekNumbers} + todayRange={todayRange} + dateProfile={props.dateProfile} + cells={cells} + renderIntro={props.renderRowIntro} + businessHourSegs={businessHourSegsByRow[row]} + eventSelection={props.eventSelection} + bgEventSegs={bgEventSegsByRow[row].filter(isSegAllDay) /* hack */} + fgEventSegs={fgEventSegsByRow[row]} + dateSelectionSegs={dateSelectionSegsByRow[row]} + eventDrag={eventDragByRow[row]} + eventResize={eventResizeByRow[row]} + dayMaxEvents={props.dayMaxEvents} + dayMaxEventRows={props.dayMaxEventRows} + clientWidth={props.clientWidth} + clientHeight={props.clientHeight} + cellMinHeight={cellMinHeight} + forPrint={props.forPrint} + /> + ))} + </Fragment> + )}</NowTimer> + ) + } + + componentDidMount(): void { + this.registerInteractiveComponent() + } + + componentDidUpdate(): void { + // for if started with zero cells + this.registerInteractiveComponent() + } + + registerInteractiveComponent() { + if (!this.rootEl) { + // HACK: need a daygrid wrapper parent to do positioning + // NOTE: a daygrid resource view w/o resources can have zero cells + const firstCellEl = this.rowRefs.currentMap[0].getCellEls()[0] + const rootEl = firstCellEl ? firstCellEl.closest('.fc-daygrid-body')! as HTMLElement : null + + if (rootEl) { + this.rootEl = rootEl + + this.context.registerInteractiveComponent(this, { + el: rootEl, + isHitComboAllowed: this.props.isHitComboAllowed, + }) + } + } + } + + componentWillUnmount(): void { + if (this.rootEl) { + this.context.unregisterInteractiveComponent(this) + this.rootEl = null + } + } + + // Hit System + // ---------------------------------------------------------------------------------------------------- + + prepareHits() { + this.rowPositions = new PositionCache( + this.rootEl, + this.rowRefs.collect().map((rowObj) => rowObj.getCellEls()[0]), // first cell el in each row. TODO: not optimal + false, + true, // vertical + ) + + this.colPositions = new PositionCache( + this.rootEl, + this.rowRefs.currentMap[0].getCellEls(), // cell els in first row + true, // horizontal + false, + ) + } + + queryHit(positionLeft: number, positionTop: number): Hit { + let { colPositions, rowPositions } = this + let col = colPositions.leftToIndex(positionLeft) + let row = rowPositions.topToIndex(positionTop) + + if (row != null && col != null) { + let cell = this.props.cells[row][col] + + return { + dateProfile: this.props.dateProfile, + dateSpan: { + range: this.getCellRange(row, col), + allDay: true, + ...cell.extraDateSpan, + }, + dayEl: this.getCellEl(row, col), + rect: { + left: colPositions.lefts[col], + right: colPositions.rights[col], + top: rowPositions.tops[row], + bottom: rowPositions.bottoms[row], + }, + layer: 0, + } + } + + return null + } + + private getCellEl(row, col) { + return this.rowRefs.currentMap[row].getCellEls()[col] // TODO: not optimal + } + + private getCellRange(row, col) { + let start = this.props.cells[row][col].date + let end = addDays(start, 1) + return { start, end } + } +} + +function isSegAllDay(seg: TableSeg) { + return seg.eventRange.def.allDay +} diff --git a/fullcalendar-main/packages/daygrid/src/TableSeg.ts b/fullcalendar-main/packages/daygrid/src/TableSeg.ts new file mode 100644 index 0000000..fc9fbf9 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableSeg.ts @@ -0,0 +1,61 @@ +import { EventSegUiInteractionState, Seg } from '@fullcalendar/core/internal' + +// this is a DATA STRUCTURE, not a component + +export interface TableSeg extends Seg { + row: number + firstCol: number + lastCol: number +} + +export function splitSegsByRow(segs: TableSeg[], rowCnt: number) { + let byRow: TableSeg[][] = [] + + for (let i = 0; i < rowCnt; i += 1) { + byRow[i] = [] + } + + for (let seg of segs) { + byRow[seg.row].push(seg) + } + + return byRow +} + +export function splitSegsByFirstCol(segs: TableSeg[], colCnt: number) { + let byCol: TableSeg[][] = [] + + for (let i = 0; i < colCnt; i += 1) { + byCol[i] = [] + } + + for (let seg of segs) { + byCol[seg.firstCol].push(seg) + } + + return byCol +} + +export function splitInteractionByRow(ui: EventSegUiInteractionState | null, rowCnt: number) { + let byRow: EventSegUiInteractionState[] = [] + + if (!ui) { + for (let i = 0; i < rowCnt; i += 1) { + byRow[i] = null + } + } else { + for (let i = 0; i < rowCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + } + } + + for (let seg of ui.segs) { + byRow[seg.row].segs.push(seg) + } + } + + return byRow +} diff --git a/fullcalendar-main/packages/daygrid/src/TableView.tsx b/fullcalendar-main/packages/daygrid/src/TableView.tsx new file mode 100644 index 0000000..983ce13 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/TableView.tsx @@ -0,0 +1,135 @@ +import { + SimpleScrollGrid, + SimpleScrollGridSection, + ChunkContentCallbackArgs, + ScrollGridSectionConfig, + ViewContainer, + DateComponent, + ViewProps, + renderScrollShim, + getStickyHeaderDates, + getStickyFooterScrollbar, + ChunkConfigRowContent, + Dictionary, +} from '@fullcalendar/core/internal' +import { + VNode, + createElement, + createRef, + RefObject, +} from '@fullcalendar/core/preact' + +/* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells. +----------------------------------------------------------------------------------------------------------------------*/ +// It is a manager for a Table subcomponent, which does most of the heavy lifting. +// It is responsible for managing width/height. + +export abstract class TableView<State=Dictionary> extends DateComponent<ViewProps, State> { + protected headerElRef: RefObject<HTMLTableCellElement> = createRef<HTMLTableCellElement>() + + renderSimpleLayout( + headerRowContent: ChunkConfigRowContent, + bodyContent: (contentArg: ChunkContentCallbackArgs) => VNode, + ) { + let { props, context } = this + let sections: SimpleScrollGridSection[] = [] + let stickyHeaderDates = getStickyHeaderDates(context.options) + + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunk: { + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + }) + } + + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunk: { content: bodyContent }, + }) + + return ( + <ViewContainer elClasses={['fc-daygrid']} viewSpec={context.viewSpec}> + <SimpleScrollGrid + liquid={!props.isHeightAuto && !props.forPrint} + collapsibleWidth={props.forPrint} + cols={[] /* TODO: make optional? */} + sections={sections} + /> + </ViewContainer> + ) + } + + renderHScrollLayout( + headerRowContent: ChunkConfigRowContent, + bodyContent: (contentArg: ChunkContentCallbackArgs) => VNode, + colCnt: number, + dayMinWidth: number, + ) { + let ScrollGrid = this.context.pluginHooks.scrollGridImpl + + if (!ScrollGrid) { + throw new Error('No ScrollGrid implementation') + } + + let { props, context } = this + let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options) + let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options) + let sections: ScrollGridSectionConfig[] = [] + + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunks: [{ + key: 'main', + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }], + }) + } + + sections.push({ + type: 'body', + key: 'body', + liquid: true, + chunks: [{ + key: 'main', + content: bodyContent, + }], + }) + + if (stickyFooterScrollbar) { + sections.push({ + type: 'footer', + key: 'footer', + isSticky: true, + chunks: [{ + key: 'main', + content: renderScrollShim, + }], + }) + } + + return ( + <ViewContainer elClasses={['fc-daygrid']} viewSpec={context.viewSpec}> + <ScrollGrid + liquid={!props.isHeightAuto && !props.forPrint} + forPrint={props.forPrint} + collapsibleWidth={props.forPrint} + colGroups={[{ cols: [{ span: colCnt, minWidth: dayMinWidth }] }]} + sections={sections} + /> + </ViewContainer> + ) + } +} diff --git a/fullcalendar-main/packages/daygrid/src/event-placement.ts b/fullcalendar-main/packages/daygrid/src/event-placement.ts new file mode 100644 index 0000000..bf15f34 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/event-placement.ts @@ -0,0 +1,313 @@ +import { EventRenderRange } from '@fullcalendar/core' +import { + SegHierarchy, + SegRect, + SegEntry, + SegInsertion, + buildEntryKey, + intersectRanges, + addDays, + DayTableCell, + intersectSpans, +} from '@fullcalendar/core/internal' +import { TableSeg } from './TableSeg.js' + +export interface TableSegPlacement { + seg: TableSeg + isVisible: boolean + isAbsolute: boolean + absoluteTop: number // populated regardless of isAbsolute + marginTop: number +} + +export function generateSegKey(seg: TableSeg): string { + return seg.eventRange.instance.instanceId + ':' + seg.firstCol +} + +export function generateSegUid(seg: TableSeg): string { + return generateSegKey(seg) + ':' + seg.lastCol +} + +export function computeFgSegPlacement( + segs: TableSeg[], // assumed already sorted + dayMaxEvents: boolean | number, + dayMaxEventRows: boolean | number, + strictOrder: boolean, + segHeights: { [segUid: string]: number }, + maxContentHeight: number | null, + cells: DayTableCell[], +) { + let hierarchy = new DayGridSegHierarchy((segEntry: SegEntry) => { + // TODO: more DRY with generateSegUid + let segUid = segs[segEntry.index].eventRange.instance.instanceId + + ':' + segEntry.span.start + + ':' + (segEntry.span.end - 1) + + // if no thickness known, assume 1 (if 0, so small it always fits) + return segHeights[segUid] || 1 + }) + hierarchy.allowReslicing = true + hierarchy.strictOrder = strictOrder + + if (dayMaxEvents === true || dayMaxEventRows === true) { + hierarchy.maxCoord = maxContentHeight + hierarchy.hiddenConsumes = true + } else if (typeof dayMaxEvents === 'number') { + hierarchy.maxStackCnt = dayMaxEvents + } else if (typeof dayMaxEventRows === 'number') { + hierarchy.maxStackCnt = dayMaxEventRows + hierarchy.hiddenConsumes = true + } + + // create segInputs only for segs with known heights + let segInputs: SegEntry[] = [] + let unknownHeightSegs: TableSeg[] = [] + for (let i = 0; i < segs.length; i += 1) { + let seg = segs[i] + let segUid = generateSegUid(seg) + let eventHeight = segHeights[segUid] + + if (eventHeight != null) { + segInputs.push({ + index: i, + span: { + start: seg.firstCol, + end: seg.lastCol + 1, + }, + }) + } else { + unknownHeightSegs.push(seg) + } + } + + let hiddenEntries = hierarchy.addSegs(segInputs) + let segRects = hierarchy.toRects() + let { singleColPlacements, multiColPlacements, leftoverMargins } = placeRects(segRects, segs, cells) + + let moreCnts: number[] = [] + let moreMarginTops: number[] = [] + + // add segs with unknown heights + for (let seg of unknownHeightSegs) { + multiColPlacements[seg.firstCol].push({ + seg, + isVisible: false, + isAbsolute: true, + absoluteTop: 0, + marginTop: 0, + }) + + for (let col = seg.firstCol; col <= seg.lastCol; col += 1) { + singleColPlacements[col].push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: false, + isAbsolute: false, + absoluteTop: 0, + marginTop: 0, + }) + } + } + + // add the hidden entries + for (let col = 0; col < cells.length; col += 1) { + moreCnts.push(0) + } + for (let hiddenEntry of hiddenEntries) { + let seg = segs[hiddenEntry.index] + let hiddenSpan = hiddenEntry.span + + multiColPlacements[hiddenSpan.start].push({ + seg: resliceSeg(seg, hiddenSpan.start, hiddenSpan.end, cells), + isVisible: false, + isAbsolute: true, + absoluteTop: 0, + marginTop: 0, + }) + + for (let col = hiddenSpan.start; col < hiddenSpan.end; col += 1) { + moreCnts[col] += 1 + singleColPlacements[col].push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: false, + isAbsolute: false, + absoluteTop: 0, + marginTop: 0, + }) + } + } + + // deal with leftover margins + for (let col = 0; col < cells.length; col += 1) { + moreMarginTops.push(leftoverMargins[col]) + } + + return { singleColPlacements, multiColPlacements, moreCnts, moreMarginTops } +} + +// rects ordered by top coord, then left +function placeRects(allRects: SegRect[], segs: TableSeg[], cells: DayTableCell[]) { + let rectsByEachCol = groupRectsByEachCol(allRects, cells.length) + let singleColPlacements: TableSegPlacement[][] = [] + let multiColPlacements: TableSegPlacement[][] = [] + let leftoverMargins: number[] = [] + + for (let col = 0; col < cells.length; col += 1) { + let rects = rectsByEachCol[col] + + // compute all static segs in singlePlacements + let singlePlacements: TableSegPlacement[] = [] + let currentHeight = 0 + let currentMarginTop = 0 + for (let rect of rects) { + let seg = segs[rect.index] + singlePlacements.push({ + seg: resliceSeg(seg, col, col + 1, cells), + isVisible: true, + isAbsolute: false, + absoluteTop: rect.levelCoord, + marginTop: rect.levelCoord - currentHeight, + }) + currentHeight = rect.levelCoord + rect.thickness + } + + // compute mixed static/absolute segs in multiPlacements + let multiPlacements: TableSegPlacement[] = [] + currentHeight = 0 + currentMarginTop = 0 + for (let rect of rects) { + let seg = segs[rect.index] + let isAbsolute = rect.span.end - rect.span.start > 1 // multi-column? + let isFirstCol = rect.span.start === col + + currentMarginTop += rect.levelCoord - currentHeight // amount of space since bottom of previous seg + currentHeight = rect.levelCoord + rect.thickness // height will now be bottom of current seg + + if (isAbsolute) { + currentMarginTop += rect.thickness + if (isFirstCol) { + multiPlacements.push({ + seg: resliceSeg(seg, rect.span.start, rect.span.end, cells), + isVisible: true, + isAbsolute: true, + absoluteTop: rect.levelCoord, + marginTop: 0, + }) + } + } else if (isFirstCol) { + multiPlacements.push({ + seg: resliceSeg(seg, rect.span.start, rect.span.end, cells), + isVisible: true, + isAbsolute: false, + absoluteTop: rect.levelCoord, + marginTop: currentMarginTop, // claim the margin + }) + currentMarginTop = 0 + } + } + + singleColPlacements.push(singlePlacements) + multiColPlacements.push(multiPlacements) + leftoverMargins.push(currentMarginTop) + } + + return { singleColPlacements, multiColPlacements, leftoverMargins } +} + +function groupRectsByEachCol(rects: SegRect[], colCnt: number): SegRect[][] { + let rectsByEachCol: SegRect[][] = [] + + for (let col = 0; col < colCnt; col += 1) { + rectsByEachCol.push([]) + } + + for (let rect of rects) { + for (let col = rect.span.start; col < rect.span.end; col += 1) { + rectsByEachCol[col].push(rect) + } + } + + return rectsByEachCol +} + +function resliceSeg(seg: TableSeg, spanStart: number, spanEnd: number, cells: DayTableCell[]): TableSeg { + if (seg.firstCol === spanStart && seg.lastCol === spanEnd - 1) { + return seg + } + + let eventRange = seg.eventRange + let origRange = eventRange.range + let slicedRange = intersectRanges(origRange, { + start: cells[spanStart].date, + end: addDays(cells[spanEnd - 1].date, 1), + }) + + return { + ...seg, + firstCol: spanStart, + lastCol: spanEnd - 1, + eventRange: { + def: eventRange.def, + ui: { ...eventRange.ui, durationEditable: false }, // hack to disable resizing + instance: eventRange.instance, + range: slicedRange, + } as EventRenderRange, + isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), + isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf(), + } +} + +class DayGridSegHierarchy extends SegHierarchy { + // config + hiddenConsumes: boolean = false + + // allows us to keep hidden entries in the hierarchy so they take up space + forceHidden: { [entryId: string]: true } = {} + + addSegs(segInputs: SegEntry[]): SegEntry[] { + const hiddenSegs = super.addSegs(segInputs) + const { entriesByLevel } = this + const excludeHidden = (entry: SegEntry) => !this.forceHidden[buildEntryKey(entry)] + + // remove the forced-hidden segs + for (let level = 0; level < entriesByLevel.length; level += 1) { + entriesByLevel[level] = entriesByLevel[level].filter(excludeHidden) + } + + return hiddenSegs + } + + handleInvalidInsertion(insertion: SegInsertion, entry: SegEntry, hiddenEntries: SegEntry[]) { + const { entriesByLevel, forceHidden } = this + const { touchingEntry, touchingLevel, touchingLateral } = insertion + + // the entry that the new insertion is touching must be hidden + if (this.hiddenConsumes && touchingEntry) { + const touchingEntryId = buildEntryKey(touchingEntry) + + if (!forceHidden[touchingEntryId]) { + if (this.allowReslicing) { + // split up the touchingEntry, reinsert it + const hiddenEntry = { + ...touchingEntry, + span: intersectSpans(touchingEntry.span, entry.span), // hit the `entry` barrier + } + + // reinsert the area that turned into a "more" link (so no other entries try to + // occupy the space) but mark it forced-hidden + const hiddenEntryId = buildEntryKey(hiddenEntry) + forceHidden[hiddenEntryId] = true + entriesByLevel[touchingLevel][touchingLateral] = hiddenEntry + + hiddenEntries.push(hiddenEntry) + this.splitEntry(touchingEntry, entry, hiddenEntries) + } else { + forceHidden[touchingEntryId] = true + hiddenEntries.push(touchingEntry) + } + } + } + + // will try to reslice... + super.handleInvalidInsertion(insertion, entry, hiddenEntries) + } +} diff --git a/fullcalendar-main/packages/daygrid/src/event-rendering.ts b/fullcalendar-main/packages/daygrid/src/event-rendering.ts new file mode 100644 index 0000000..b269080 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/event-rendering.ts @@ -0,0 +1,21 @@ +import { createFormatter, DateFormatter } from '@fullcalendar/core/internal' +import { TableSeg } from './TableSeg.js' + +export const DEFAULT_TABLE_EVENT_TIME_FORMAT: DateFormatter = createFormatter({ + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'narrow', +}) + +export function hasListItemDisplay(seg: TableSeg) { + let { display } = seg.eventRange.ui + + return display === 'list-item' || ( + display === 'auto' && + !seg.eventRange.def.allDay && + seg.firstCol === seg.lastCol && // can't be multi-day + seg.isStart && // " + seg.isEnd // " + ) +} diff --git a/fullcalendar-main/packages/daygrid/src/index.css b/fullcalendar-main/packages/daygrid/src/index.css new file mode 100644 index 0000000..1fb3d64 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/index.css @@ -0,0 +1,6 @@ + +@import '../../core/src/styles/mixins'; +@import './styles/vars'; + +@import './styles/daygrid'; +@import './styles/daygrid-event'; diff --git a/fullcalendar-main/packages/daygrid/src/index.global.ts b/fullcalendar-main/packages/daygrid/src/index.global.ts new file mode 100644 index 0000000..c70b8e6 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/index.global.ts @@ -0,0 +1,8 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' +import * as Internal from './internal.js' + +globalPlugins.push(plugin) + +export { plugin as default, Internal } +export * from './index.js' diff --git a/fullcalendar-main/packages/daygrid/src/index.ts b/fullcalendar-main/packages/daygrid/src/index.ts new file mode 100644 index 0000000..ba212e0 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/index.ts @@ -0,0 +1,32 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { DayTableView } from './DayTableView.js' +import { TableDateProfileGenerator } from './TableDateProfileGenerator.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + initialView: 'dayGridMonth', + views: { + dayGrid: { + component: DayTableView, + dateProfileGeneratorClass: TableDateProfileGenerator, + }, + dayGridDay: { + type: 'dayGrid', + duration: { days: 1 }, + }, + dayGridWeek: { + type: 'dayGrid', + duration: { weeks: 1 }, + }, + dayGridMonth: { + type: 'dayGrid', + duration: { months: 1 }, + fixedWeekCount: true, + }, + dayGridYear: { + type: 'dayGrid', + duration: { years: 1 }, + }, + }, +}) as PluginDef diff --git a/fullcalendar-main/packages/daygrid/src/internal.ts b/fullcalendar-main/packages/daygrid/src/internal.ts new file mode 100644 index 0000000..71a55c3 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/internal.ts @@ -0,0 +1,11 @@ +import './index.css' + +export { DayTable } from './DayTable.js' +export { DayTableSlicer } from './DayTableSlicer.js' +export { TableDateProfileGenerator, buildDayTableRenderRange } from './TableDateProfileGenerator.js' +export { Table } from './Table.js' +export { TableRows } from './TableRows.js' +export { TableSeg } from './TableSeg.js' +export { TableView } from './TableView.js' +export { buildDayTableModel } from './DayTableView.js' +export { DayTableView as DayGridView } from './DayTableView.js' diff --git a/fullcalendar-main/packages/daygrid/src/styles/daygrid-event.css b/fullcalendar-main/packages/daygrid/src/styles/daygrid-event.css new file mode 100644 index 0000000..8876824 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/styles/daygrid-event.css @@ -0,0 +1,76 @@ + +.fc-daygrid-event { // make root-level, because will be dragged-and-dropped outside of a component root + position: relative; // for z-indexes assigned later + white-space: nowrap; + border-radius: 3px; // dot event needs this to when selected + font-size: var(--fc-small-font-size); +} + +// --- the rectangle ("block") style of event --- + +.fc-daygrid-block-event { + + & .fc-event-time { + font-weight: bold; + } + + & .fc-event-time, + & .fc-event-title { + padding: 1px; + } + +} + +// --- the dot style of event --- + +.fc-daygrid-dot-event { + display: flex; + align-items: center; + padding: 2px 0; + + & .fc-event-title { + flex-grow: 1; + flex-shrink: 1; + min-width: 0; // important for allowing to shrink all the way + overflow: hidden; + font-weight: bold; + } + + &:hover, + &.fc-event-mirror { + background: rgba(0, 0, 0, 0.1); + } + + &.fc-event-selected:before { + // expand hit area + top: -10px; + bottom: -10px; + } + +} + +.fc-daygrid-event-dot { // the actual dot + margin: 0 4px; + box-sizing: content-box; + width: 0; + height: 0; + border: calc(var(--fc-daygrid-event-dot-width) / 2) solid var(--fc-event-border-color); + border-radius: calc(var(--fc-daygrid-event-dot-width) / 2); +} + + +// --- spacing between time and title --- + +$daygrid-event-inner-margin: 3px; + +.fc-direction-ltr { + & .fc-daygrid-event .fc-event-time { + margin-right: $daygrid-event-inner-margin; + } +} + +.fc-direction-rtl { + & .fc-daygrid-event .fc-event-time { + margin-left: $daygrid-event-inner-margin; + } +} diff --git a/fullcalendar-main/packages/daygrid/src/styles/daygrid.css b/fullcalendar-main/packages/daygrid/src/styles/daygrid.css new file mode 100644 index 0000000..2fa4a13 --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/styles/daygrid.css @@ -0,0 +1,221 @@ + +$daygrid-non-business-z: 1; +$daygrid-bg-event-z: 2; +$daygrid-highlight-z: 3; +$daygrid-topbottom-z: 4; +$daygrid-weeknum-z: 5; +$daygrid-event-z: 6; +$daygrid-event-mirror-z: 7; + + +// help things clear margins of inner content +.fc-daygrid-day-frame, +.fc-daygrid-day-events, +.fc-daygrid-event-harness { // for event top/bottom margins + &:before { @include clearfix; } + &:after { @include clearfix; } +} + + +.fc { + + & .fc-daygrid-body { // a <div> that wraps the table + position: relative; + z-index: 1; // container inner z-index's because <tr>s can't do it + } + + & .fc-daygrid-day { + &.fc-day-today { + background-color: var(--fc-today-bg-color); + } + } + + & .fc-daygrid-day-frame { + position: relative; + min-height: 100%; // seems to work better than `height` because sets height after rows/cells naturally do it + } + + // cell top + + & .fc-daygrid-day-top { + display: flex; + flex-direction: row-reverse; + } + + & .fc-day-other .fc-daygrid-day-top { + opacity: 0.3; + } + + // day number (within cell top) + + & .fc-daygrid-day-number { + position: relative; + z-index: $daygrid-topbottom-z; + padding: 4px; + } + + & .fc-daygrid-month-start { // on .fc-daygrid-day-number + font-weight: bold; + font-size: 1.1em; + } + + // event container + + & .fc-daygrid-day-events { + margin-top: 1px; // needs to be margin, not padding, so that available cell height can be computed + } + + // positioning for balanced vs natural + + & .fc-daygrid-body-balanced { + & .fc-daygrid-day-events { + position: absolute; + left: 0; + right: 0; + } + } + + & .fc-daygrid-body-unbalanced { + & .fc-daygrid-day-events { + position: relative; // for containing abs positioned event harnesses + min-height: 2em; // in addition to being a min-height during natural height, equalizes the heights a little bit + } + } + + & .fc-daygrid-body-natural { // can coexist with -unbalanced + & .fc-daygrid-day-events { + margin-bottom: 1em; + } + } + + // event harness + + & .fc-daygrid-event-harness { + position: relative; + } + + & .fc-daygrid-event-harness-abs { + position: absolute; + top: 0; // fallback coords for when cannot yet be computed + left: 0; // + right: 0; // + } + + & .fc-daygrid-bg-harness { + position: absolute; + top: 0; + bottom: 0; + } + + // bg content + + & .fc-daygrid-day-bg { + & .fc-non-business { z-index: $daygrid-non-business-z } + & .fc-bg-event { z-index: $daygrid-bg-event-z } + & .fc-highlight { z-index: $daygrid-highlight-z } + } + + // events + + & .fc-daygrid-event { + z-index: $daygrid-event-z; + margin-top: 1px; + } + + & .fc-daygrid-event.fc-event-mirror { + z-index: $daygrid-event-mirror-z; + } + + // cell bottom (within day-events) + + & .fc-daygrid-day-bottom { + font-size: .85em; + margin: 0 2px; + + // can't use flexbox well inside tables. for floating + &:before { @include clearfix; } + &:after { @include clearfix; } + } + + & .fc-daygrid-more-link { + position: relative; + z-index: $daygrid-topbottom-z; + margin-top: 1px; + max-width: 100%; + border-radius: 3px; + padding: 2px; + overflow: hidden; + white-space: nowrap; + line-height: 1; // undo themes that have thick line-height + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } + } + + // week number (within frame) + + & .fc-daygrid-week-number { + position: absolute; + z-index: $daygrid-weeknum-z; + top: 0; + padding: 2px; + min-width: 1.5em; + text-align: center; + background-color: var(--fc-neutral-bg-color); + color: var(--fc-neutral-text-color); + } + + // popover + + & .fc-more-popover .fc-popover-body { + min-width: 220px; + padding: 10px; + } + +} + +.fc-direction-ltr .fc-daygrid-event.fc-event-start, +.fc-direction-rtl .fc-daygrid-event.fc-event-end { + margin-left: 2px; +} + +.fc-direction-ltr .fc-daygrid-event.fc-event-end, +.fc-direction-rtl .fc-daygrid-event.fc-event-start { + margin-right: 2px; +} + +.fc-direction-ltr { + + & .fc-daygrid-more-link { + float: left; + } + + & .fc-daygrid-week-number { + left: 0; + border-radius: 0 0 3px 0; + } + +} + +.fc-direction-rtl { + + & .fc-daygrid-more-link { + float: right; + } + + & .fc-daygrid-week-number { + right: 0; + border-radius: 0 0 0 3px; + } + +} + +.fc-liquid-hack { + + & .fc-daygrid-day-frame { + position: static; // will cause inner absolute stuff to expand to <td> + } + +} diff --git a/fullcalendar-main/packages/daygrid/src/styles/vars.css b/fullcalendar-main/packages/daygrid/src/styles/vars.css new file mode 100644 index 0000000..32093df --- /dev/null +++ b/fullcalendar-main/packages/daygrid/src/styles/vars.css @@ -0,0 +1,4 @@ + +:root { + --fc-daygrid-event-dot-width: 8px; +} diff --git a/fullcalendar-main/packages/google-calendar/.eslintrc.cjs b/fullcalendar-main/packages/google-calendar/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/google-calendar/README.md b/fullcalendar-main/packages/google-calendar/README.md new file mode 100644 index 0000000..5750c94 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/README.md @@ -0,0 +1,36 @@ + +# FullCalendar Google Calendar Plugin + +Display events from a public [Google Calendar feed](https://support.google.com/calendar/answer/37648?hl=en) + +## Installation + +Install the FullCalendar core package, the Google Calendar plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/google-calendar @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import googleCalendarPlugin from '@fullcalendar/google-calendar' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + googleCalendarPlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + events: { + googleCalendarId: 'abcd1234@group.calendar.google.com' + } +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/google-calendar/package.json b/fullcalendar-main/packages/google-calendar/package.json new file mode 100644 index 0000000..8ae24ae --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/package.json @@ -0,0 +1,47 @@ +{ + "name": "@fullcalendar/google-calendar", + "version": "6.1.11", + "title": "FullCalendar Google Calendar Plugin", + "description": "Display events from a public Google Calendar feed", + "homepage": "https://fullcalendar.io/docs/google-calendar", + "keywords": [ + "google-calendar" + ], + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.GoogleCalendar" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/google-calendar/src/ambient.ts b/fullcalendar-main/packages/google-calendar/src/ambient.ts new file mode 100644 index 0000000..6675417 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/ambient.ts @@ -0,0 +1,13 @@ +import { OPTION_REFINERS } from './options-refiners.js' +import { EVENT_SOURCE_REFINERS } from './event-source-refiners.js' + +type ExtraOptionRefiners = typeof OPTION_REFINERS +type ExtraEventSourceRefiners = typeof EVENT_SOURCE_REFINERS + +declare module '@fullcalendar/core/internal' { + interface BaseOptionRefiners extends ExtraOptionRefiners {} +} + +declare module '@fullcalendar/core/internal' { + interface EventSourceRefiners extends ExtraEventSourceRefiners {} +} diff --git a/fullcalendar-main/packages/google-calendar/src/event-source-def.ts b/fullcalendar-main/packages/google-calendar/src/event-source-def.ts new file mode 100644 index 0000000..147f6be --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/event-source-def.ts @@ -0,0 +1,175 @@ +import { JsonRequestError } from '@fullcalendar/core' +import { EventSourceDef, addDays, DateEnv, requestJson, Dictionary } from '@fullcalendar/core/internal' + +// TODO: expose somehow +const API_BASE = 'https://www.googleapis.com/calendar/v3/calendars' + +interface GCalMeta { + googleCalendarId: string + googleCalendarApiKey?: string + googleCalendarApiBase?: string, + extraParams?: Dictionary | (() => Dictionary) +} + +export const eventSourceDef: EventSourceDef<GCalMeta> = { + + parseMeta(refined): GCalMeta | null { + let { googleCalendarId } = refined + + if (!googleCalendarId && refined.url) { + googleCalendarId = parseGoogleCalendarId(refined.url) + } + + if (googleCalendarId) { + return { + googleCalendarId, + googleCalendarApiKey: refined.googleCalendarApiKey, + googleCalendarApiBase: refined.googleCalendarApiBase, + extraParams: refined.extraParams, + } + } + + return null + }, + + fetch(arg, successCallback, errorCallback) { + let { dateEnv, options } = arg.context + let meta: GCalMeta = arg.eventSource.meta + let apiKey = meta.googleCalendarApiKey || options.googleCalendarApiKey + + if (!apiKey) { + errorCallback( + new Error('Specify a googleCalendarApiKey. See https://fullcalendar.io/docs/google-calendar'), + ) + } else { + let url = buildUrl(meta) + + // TODO: make DRY with json-feed-event-source + let { extraParams } = meta + let extraParamsObj = typeof extraParams === 'function' ? extraParams() : extraParams + + let requestParams = buildRequestParams( + arg.range, + apiKey, + extraParamsObj, + dateEnv, + ) + + return requestJson( + 'GET', + url, + requestParams, + ).then( + ([body, response]: [any, Response]) => { + if (body.error) { + errorCallback(new JsonRequestError('Google Calendar API: ' + body.error.message, response)) + } else { + successCallback({ + rawEvents: gcalItemsToRawEventDefs( + body.items, + requestParams.timeZone, + ), + response, + }) + } + }, + errorCallback, + ) + } + }, +} + +function parseGoogleCalendarId(url) { + let match + + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^/]+@([^/.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + return url + } + + if ( + (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^/]*)/.exec(url)) + ) { + return decodeURIComponent(match[1]) + } + + return null +} + +function buildUrl(meta) { + let apiBase = meta.googleCalendarApiBase + if (!apiBase) { + apiBase = API_BASE + } + return apiBase + '/' + encodeURIComponent(meta.googleCalendarId) + '/events' +} + +function buildRequestParams(range, apiKey: string, extraParams: Dictionary, dateEnv: DateEnv) { + let params + let startStr + let endStr + + if (dateEnv.canComputeOffset) { + // strings will naturally have offsets, which GCal needs + startStr = dateEnv.formatIso(range.start) + endStr = dateEnv.formatIso(range.end) + } else { + // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day + // from the UTC day-start to guarantee we're getting all the events + // (start/end will be UTC-coerced dates, so toISOString is okay) + startStr = addDays(range.start, -1).toISOString() + endStr = addDays(range.end, 1).toISOString() + } + + params = { + ...(extraParams || {}), + key: apiKey, + timeMin: startStr, + timeMax: endStr, + singleEvents: true, + maxResults: 9999, + } + + if (dateEnv.timeZone !== 'local') { + params.timeZone = dateEnv.timeZone + } + + return params +} + +function gcalItemsToRawEventDefs(items, gcalTimezone) { + return items.map((item) => gcalItemToRawEventDef(item, gcalTimezone)) +} + +function gcalItemToRawEventDef(item, gcalTimezone) { + let url = item.htmlLink || null + + // make the URLs for each event show times in the correct timezone + if (url && gcalTimezone) { + url = injectQsComponent(url, 'ctz=' + gcalTimezone) + } + + return { + id: item.id, + title: item.summary, + start: item.start.dateTime || item.start.date, // try timed. will fall back to all-day + end: item.end.dateTime || item.end.date, // same + url, + location: item.location, + description: item.description, + attachments: item.attachments || [], + extendedProps: (item.extendedProperties || {}).shared || {}, + } +} + +// Injects a string like "arg=value" into the querystring of a URL +// TODO: move to a general util file? +function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace( + /(\?.*?)?(#|$)/, + (whole, qs, hash) => (qs ? qs + '&' : '?') + component + hash, + ) +} diff --git a/fullcalendar-main/packages/google-calendar/src/event-source-refiners.ts b/fullcalendar-main/packages/google-calendar/src/event-source-refiners.ts new file mode 100644 index 0000000..71bc567 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/event-source-refiners.ts @@ -0,0 +1,8 @@ +import { identity, Identity, Dictionary } from '@fullcalendar/core/internal' + +export const EVENT_SOURCE_REFINERS = { + googleCalendarApiKey: String, // TODO: rename with no prefix? + googleCalendarId: String, + googleCalendarApiBase: String, + extraParams: identity as Identity<Dictionary | (() => Dictionary)>, +} diff --git a/fullcalendar-main/packages/google-calendar/src/index.global.ts b/fullcalendar-main/packages/google-calendar/src/index.global.ts new file mode 100644 index 0000000..d8cf7b8 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/index.global.ts @@ -0,0 +1,6 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } diff --git a/fullcalendar-main/packages/google-calendar/src/index.ts b/fullcalendar-main/packages/google-calendar/src/index.ts new file mode 100644 index 0000000..37d2f86 --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/index.ts @@ -0,0 +1,12 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { eventSourceDef } from './event-source-def.js' +import { OPTION_REFINERS } from './options-refiners.js' +import { EVENT_SOURCE_REFINERS } from './event-source-refiners.js' +import './ambient.js' + +export default createPlugin({ + name: '<%= pkgName %>', + eventSourceDefs: [eventSourceDef], + optionRefiners: OPTION_REFINERS, + eventSourceRefiners: EVENT_SOURCE_REFINERS, +}) as PluginDef diff --git a/fullcalendar-main/packages/google-calendar/src/options-refiners.ts b/fullcalendar-main/packages/google-calendar/src/options-refiners.ts new file mode 100644 index 0000000..0c304de --- /dev/null +++ b/fullcalendar-main/packages/google-calendar/src/options-refiners.ts @@ -0,0 +1,4 @@ + +export const OPTION_REFINERS = { + googleCalendarApiKey: String, +} diff --git a/fullcalendar-main/packages/icalendar/.eslintrc.cjs b/fullcalendar-main/packages/icalendar/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/icalendar/README.md b/fullcalendar-main/packages/icalendar/README.md new file mode 100644 index 0000000..ce84897 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/README.md @@ -0,0 +1,43 @@ + +# FullCalendar iCalendar Plugin + +Display events from a public [iCalendar feed](https://icalendar.org/) + +## Installation + +First, ensure ical.js is installed: + +```sh +npm install ical.js +``` + +Then, install the FullCalendar core package, the iCalendar plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/icalendar @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import iCalendarPlugin from '@fullcalendar/icalendar' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + iCalendarPlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + events: { + url: 'https://mywebsite.com/icalendar-feed.ics', + format: 'ics' // important! + } +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/icalendar/package.json b/fullcalendar-main/packages/icalendar/package.json new file mode 100644 index 0000000..2802f58 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/package.json @@ -0,0 +1,51 @@ +{ + "name": "@fullcalendar/icalendar", + "version": "6.1.11", + "title": "FullCalendar iCalendar Plugin", + "description": "Display events from a public iCalendar feed", + "keywords": [ + "icalendar", + "ics" + ], + "homepage": "https://fullcalendar.io/docs/icalendar", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "ical.js": "^1.4.0" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "ical.js": "^1.4.0" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.ICalendar", + "ical.js": "ICAL" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/icalendar/src/event-source-def.ts b/fullcalendar-main/packages/icalendar/src/event-source-def.ts new file mode 100644 index 0000000..227b529 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/src/event-source-def.ts @@ -0,0 +1,125 @@ +import { EventInput } from '@fullcalendar/core' +import { EventSourceDef, DateRange, addDays } from '@fullcalendar/core/internal' +import * as ICAL from 'ical.js' +import { IcalExpander } from './ical-expander/IcalExpander.js' + +interface ICalFeedMeta { + url: string + format: 'ics', // for EventSourceApi + internalState?: InternalState // HACK. TODO: use classes in future +} + +interface InternalState { + iCalExpanderPromise: Promise<IcalExpander> + response: Response | null +} + +export const eventSourceDef: EventSourceDef<ICalFeedMeta> = { + + parseMeta(refined) { + if (refined.url && refined.format === 'ics') { + return { + url: refined.url, + format: 'ics', + } + } + return null + }, + + fetch(arg, successCallback, errorCallback) { + let meta: ICalFeedMeta = arg.eventSource.meta + let { internalState } = meta + + /* + NOTE: isRefetch is a HACK. we would do the recurring-expanding in a separate plugin hook, + but we couldn't leverage built-in allDay-guessing, among other things. + */ + if (!internalState || arg.isRefetch) { + internalState = meta.internalState = { + response: null, + iCalExpanderPromise: fetch( + meta.url, + { method: 'GET' }, + ).then((response) => { + return response.text().then((icsText) => { + internalState.response = response + return new IcalExpander({ + ics: icsText, + skipInvalidDates: true, + }) + }) + }), + } + } + + internalState.iCalExpanderPromise.then( + (iCalExpander) => { + successCallback({ + rawEvents: expandICalEvents(iCalExpander, arg.range), + response: internalState.response, + }) + }, + errorCallback, + ) + }, +} + +function expandICalEvents(iCalExpander: IcalExpander, range: DateRange): EventInput[] { + // expand the range. because our `range` is timeZone-agnostic UTC + // or maybe because ical.js always produces dates in local time? i forget + let rangeStart = addDays(range.start, -1) + let rangeEnd = addDays(range.end, 1) + + let iCalRes = iCalExpander.between(rangeStart, rangeEnd) // end inclusive. will give extra results + let expanded: EventInput[] = [] + + // TODO: instead of using startDate/endDate.toString to communicate allDay, + // we can query startDate/endDate.isDate. More efficient to avoid formatting/reparsing. + + // single events + for (let iCalEvent of iCalRes.events) { + expanded.push({ + ...buildNonDateProps(iCalEvent), + start: iCalEvent.startDate.toString(), + end: (specifiesEnd(iCalEvent) && iCalEvent.endDate) + ? iCalEvent.endDate.toString() + : null, + }) + } + + // recurring event instances + for (let iCalOccurence of iCalRes.occurrences) { + let iCalEvent = iCalOccurence.item + expanded.push({ + ...buildNonDateProps(iCalEvent), + start: iCalOccurence.startDate.toString(), + end: (specifiesEnd(iCalEvent) && iCalOccurence.endDate) + ? iCalOccurence.endDate.toString() + : null, + }) + } + + return expanded +} + +function buildNonDateProps(iCalEvent: ICAL.Event): EventInput { + return { + title: iCalEvent.summary, + url: extractEventUrl(iCalEvent), + extendedProps: { + location: iCalEvent.location, + organizer: iCalEvent.organizer, + description: iCalEvent.description, + }, + } +} + +function extractEventUrl(iCalEvent: ICAL.Event): string { + let urlProp = iCalEvent.component.getFirstProperty('url') + return urlProp ? urlProp.getFirstValue() : '' +} + +function specifiesEnd(iCalEvent: ICAL.Event) { + return Boolean(iCalEvent.component.getFirstProperty('dtend')) || + Boolean(iCalEvent.component.getFirstProperty('duration')) +} diff --git a/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.d.ts b/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.d.ts new file mode 100644 index 0000000..d1564d1 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.d.ts @@ -0,0 +1,20 @@ +import * as ICAL from 'ical.js' + +export interface IcalExpanderResults { + events: ICAL.Event[] + occurrences: ICAL.Event.occurrenceDetails +} + +export interface IcalExpanderOptions { + ics: string + maxIterations?: number + skipInvalidDates?: boolean +} + +export class IcalExpander { + constructor(opts: IcalExpanderOptions) + between(after?: Date, before?: Date): IcalExpanderResults + before(before: Date): IcalExpanderResults + after(after: Date): IcalExpanderResults + all(): IcalExpanderResults +} diff --git a/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.js b/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.js new file mode 100644 index 0000000..da0ed50 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/src/ical-expander/IcalExpander.js @@ -0,0 +1,129 @@ +/* eslint-disable */ +/* +from https://github.com/mifi/ical-expander +released under https://github.com/mifi/ical-expander/blob/master/LICENSE +operates entirely in UTC +*/ + +import * as ICAL from 'ical.js' + +export class IcalExpander { + constructor(opts) { + this.maxIterations = opts.maxIterations != null ? opts.maxIterations : 1000; + this.skipInvalidDates = opts.skipInvalidDates != null ? opts.skipInvalidDates : false; + + this.jCalData = ICAL.parse(opts.ics); + this.component = new ICAL.Component(this.jCalData); + this.events = this.component.getAllSubcomponents('vevent').map(vevent => new ICAL.Event(vevent)); + + if (this.skipInvalidDates) { + this.events = this.events.filter((evt) => { + try { + evt.startDate.toJSDate(); + evt.endDate.toJSDate(); + return true; + } catch (err) { + // skipping events with invalid time + return false; + } + }); + } + } + + between(after, before) { + function isEventWithinRange(startTime, endTime) { + return (!after || endTime >= after.getTime()) && + (!before || startTime <= before.getTime()); + } + + function getTimes(eventOrOccurrence) { + const startTime = eventOrOccurrence.startDate.toJSDate().getTime(); + let endTime = eventOrOccurrence.endDate.toJSDate().getTime(); + + // If it is an all day event, the end date is set to 00:00 of the next day + // So we need to make it be 23:59:59 to compare correctly with the given range + if (eventOrOccurrence.endDate.isDate && (endTime > startTime)) { + endTime -= 1; + } + + return { startTime, endTime }; + } + + const exceptions = []; + + this.events.forEach((event) => { + if (event.isRecurrenceException()) exceptions.push(event); + }); + + const ret = { + events: [], + occurrences: [], + }; + + this.events.filter(e => !e.isRecurrenceException()).forEach((event) => { + const exdates = []; + + event.component.getAllProperties('exdate').forEach((exdateProp) => { + const exdate = exdateProp.getFirstValue(); + exdates.push(exdate.toJSDate().getTime()); + }); + + // Recurring event is handled differently + if (event.isRecurring()) { + const iterator = event.iterator(); + + let next; + let i = 0; + + do { + i += 1; + next = iterator.next(); + if (next) { + const occurrence = event.getOccurrenceDetails(next); + + const { startTime, endTime } = getTimes(occurrence); + + const isOccurrenceExcluded = exdates.indexOf(startTime) !== -1; + + // TODO check that within same day? + const exception = exceptions.find(ex => ex.uid === event.uid && ex.recurrenceId.toJSDate().getTime() === occurrence.startDate.toJSDate().getTime()); + + // We have passed the max date, stop + if (before && startTime > before.getTime()) break; + + // Check that we are within our range + if (isEventWithinRange(startTime, endTime)) { + if (exception) { + ret.events.push(exception); + } else if (!isOccurrenceExcluded) { + ret.occurrences.push(occurrence); + } + } + } + } + while (next && (!this.maxIterations || i < this.maxIterations)); + + return; + } + + // Non-recurring event: + const { startTime, endTime } = getTimes(event); + + if (isEventWithinRange(startTime, endTime)) ret.events.push(event); + }); + + return ret; + } + + before(before) { + return this.between(undefined, before); + } + + after(after) { + return this.between(after); + } + + all() { + return this.between(); + } +} diff --git a/fullcalendar-main/packages/icalendar/src/index.global.ts b/fullcalendar-main/packages/icalendar/src/index.global.ts new file mode 100644 index 0000000..d8cf7b8 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/src/index.global.ts @@ -0,0 +1,6 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } diff --git a/fullcalendar-main/packages/icalendar/src/index.ts b/fullcalendar-main/packages/icalendar/src/index.ts new file mode 100644 index 0000000..62efe90 --- /dev/null +++ b/fullcalendar-main/packages/icalendar/src/index.ts @@ -0,0 +1,7 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { eventSourceDef } from './event-source-def.js' + +export default createPlugin({ + name: '<%= pkgName %>', + eventSourceDefs: [eventSourceDef], +}) as PluginDef diff --git a/fullcalendar-main/packages/interaction/.eslintrc.cjs b/fullcalendar-main/packages/interaction/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/interaction/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/interaction/README.md b/fullcalendar-main/packages/interaction/README.md new file mode 100644 index 0000000..8c38ddd --- /dev/null +++ b/fullcalendar-main/packages/interaction/README.md @@ -0,0 +1,38 @@ + +# FullCalendar Interaction Plugin + +Calendar functionality for event drag-n-drop, event resizing, date clicking, and date selecting + +## Installation + +Install the FullCalendar core package, the interaction plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/interaction @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + interactionPlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + editable: true, // important for activating event interactions! + selectable: true, // important for activating date selectability! + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/interaction/package.json b/fullcalendar-main/packages/interaction/package.json new file mode 100644 index 0000000..3c8188e --- /dev/null +++ b/fullcalendar-main/packages/interaction/package.json @@ -0,0 +1,49 @@ +{ + "name": "@fullcalendar/interaction", + "version": "6.1.11", + "title": "FullCalendar Interaction Plugin", + "description": "Calendar functionality for event drag-n-drop, event resizing, date clicking, and date selecting", + "keywords": [ + "drag-n-drop", + "resizing", + "selecting" + ], + "homepage": "https://fullcalendar.io/docs/editable", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.Interaction" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/interaction/src/ElementScrollGeomCache.ts b/fullcalendar-main/packages/interaction/src/ElementScrollGeomCache.ts new file mode 100644 index 0000000..3f4d7d0 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/ElementScrollGeomCache.ts @@ -0,0 +1,16 @@ +import { computeInnerRect, ElementScrollController } from '@fullcalendar/core/internal' +import { ScrollGeomCache } from './ScrollGeomCache.js' + +export class ElementScrollGeomCache extends ScrollGeomCache { + constructor(el: HTMLElement, doesListening: boolean) { + super(new ElementScrollController(el), doesListening) + } + + getEventTarget(): EventTarget { + return (this.scrollController as ElementScrollController).el + } + + computeClientRect() { + return computeInnerRect((this.scrollController as ElementScrollController).el) + } +} diff --git a/fullcalendar-main/packages/interaction/src/OffsetTracker.ts b/fullcalendar-main/packages/interaction/src/OffsetTracker.ts new file mode 100644 index 0000000..e79f1c9 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/OffsetTracker.ts @@ -0,0 +1,76 @@ +import { + getClippingParents, computeRect, + pointInsideRect, Rect, +} from '@fullcalendar/core/internal' +import { ElementScrollGeomCache } from './ElementScrollGeomCache.js' + +/* +When this class is instantiated, it records the offset of an element (relative to the document topleft), +and continues to monitor scrolling, updating the cached coordinates if it needs to. +Does not access the DOM after instantiation, so highly performant. + +Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element +and an determine if a given point is inside the combined clipping rectangle. +*/ +export class OffsetTracker { // ElementOffsetTracker + scrollCaches: ElementScrollGeomCache[] + origRect: Rect + + constructor(el: HTMLElement) { + this.origRect = computeRect(el) + + // will work fine for divs that have overflow:hidden + this.scrollCaches = getClippingParents(el).map( + (scrollEl) => new ElementScrollGeomCache(scrollEl, true), // listen=true + ) + } + + destroy() { + for (let scrollCache of this.scrollCaches) { + scrollCache.destroy() + } + } + + computeLeft() { + let left = this.origRect.left + + for (let scrollCache of this.scrollCaches) { + left += scrollCache.origScrollLeft - scrollCache.getScrollLeft() + } + + return left + } + + computeTop() { + let top = this.origRect.top + + for (let scrollCache of this.scrollCaches) { + top += scrollCache.origScrollTop - scrollCache.getScrollTop() + } + + return top + } + + isWithinClipping(pageX: number, pageY: number): boolean { + let point = { left: pageX, top: pageY } + + for (let scrollCache of this.scrollCaches) { + if ( + !isIgnoredClipping(scrollCache.getEventTarget()) && + !pointInsideRect(point, scrollCache.clientRect) + ) { + return false + } + } + + return true + } +} + +// certain clipping containers should never constrain interactions, like <html> and <body> +// https://github.com/fullcalendar/fullcalendar/issues/3615 +function isIgnoredClipping(node: EventTarget) { + let tagName = (node as HTMLElement).tagName + + return tagName === 'HTML' || tagName === 'BODY' +} diff --git a/fullcalendar-main/packages/interaction/src/ScrollGeomCache.ts b/fullcalendar-main/packages/interaction/src/ScrollGeomCache.ts new file mode 100644 index 0000000..3791161 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/ScrollGeomCache.ts @@ -0,0 +1,107 @@ +import { Rect, ScrollController } from '@fullcalendar/core/internal' + +/* +Is a cache for a given element's scroll information (all the info that ScrollController stores) +in addition the "client rectangle" of the element.. the area within the scrollbars. + +The cache can be in one of two modes: +- doesListening:false - ignores when the container is scrolled by someone else +- doesListening:true - watch for scrolling and update the cache +*/ +export abstract class ScrollGeomCache extends ScrollController { + clientRect: Rect + origScrollTop: number + origScrollLeft: number + + protected scrollController: ScrollController + protected doesListening: boolean + protected scrollTop: number + protected scrollLeft: number + protected scrollWidth: number + protected scrollHeight: number + protected clientWidth: number + protected clientHeight: number + + constructor(scrollController: ScrollController, doesListening: boolean) { + super() + this.scrollController = scrollController + this.doesListening = doesListening + this.scrollTop = this.origScrollTop = scrollController.getScrollTop() + this.scrollLeft = this.origScrollLeft = scrollController.getScrollLeft() + this.scrollWidth = scrollController.getScrollWidth() + this.scrollHeight = scrollController.getScrollHeight() + this.clientWidth = scrollController.getClientWidth() + this.clientHeight = scrollController.getClientHeight() + this.clientRect = this.computeClientRect() // do last in case it needs cached values + + if (this.doesListening) { + this.getEventTarget().addEventListener('scroll', this.handleScroll) + } + } + + abstract getEventTarget(): EventTarget + abstract computeClientRect(): Rect + + destroy() { + if (this.doesListening) { + this.getEventTarget().removeEventListener('scroll', this.handleScroll) + } + } + + handleScroll = () => { + this.scrollTop = this.scrollController.getScrollTop() + this.scrollLeft = this.scrollController.getScrollLeft() + this.handleScrollChange() + } + + getScrollTop() { + return this.scrollTop + } + + getScrollLeft() { + return this.scrollLeft + } + + setScrollTop(top: number) { + this.scrollController.setScrollTop(top) + + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0) + + this.handleScrollChange() + } + } + + setScrollLeft(top: number) { + this.scrollController.setScrollLeft(top) + + if (!this.doesListening) { + // we are not relying on the element to normalize out-of-bounds scroll values + // so we need to sanitize ourselves + this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0) + + this.handleScrollChange() + } + } + + getClientWidth() { + return this.clientWidth + } + + getClientHeight() { + return this.clientHeight + } + + getScrollWidth() { + return this.scrollWidth + } + + getScrollHeight() { + return this.scrollHeight + } + + handleScrollChange() { + } +} diff --git a/fullcalendar-main/packages/interaction/src/WindowScrollGeomCache.ts b/fullcalendar-main/packages/interaction/src/WindowScrollGeomCache.ts new file mode 100644 index 0000000..c0a150d --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/WindowScrollGeomCache.ts @@ -0,0 +1,27 @@ +import { Rect, WindowScrollController } from '@fullcalendar/core/internal' +import { ScrollGeomCache } from './ScrollGeomCache.js' + +export class WindowScrollGeomCache extends ScrollGeomCache { + constructor(doesListening: boolean) { + super(new WindowScrollController(), doesListening) + } + + getEventTarget(): EventTarget { + return window + } + + computeClientRect(): Rect { + return { + left: this.scrollLeft, + right: this.scrollLeft + this.clientWidth, + top: this.scrollTop, + bottom: this.scrollTop + this.clientHeight, + } + } + + // the window is the only scroll object that changes it's rectangle relative + // to the document's topleft as it scrolls + handleScrollChange() { + this.clientRect = this.computeClientRect() + } +} diff --git a/fullcalendar-main/packages/interaction/src/ambient.ts b/fullcalendar-main/packages/interaction/src/ambient.ts new file mode 100644 index 0000000..038f89a --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/ambient.ts @@ -0,0 +1,9 @@ +import { OPTION_REFINERS, LISTENER_REFINERS } from './option-refiners.js' + +type ExtraOptionRefiners = typeof OPTION_REFINERS +type ExtraListenerRefiners = typeof LISTENER_REFINERS + +declare module '@fullcalendar/core/internal' { + interface BaseOptionRefiners extends ExtraOptionRefiners {} + interface CalendarListenerRefiners extends ExtraListenerRefiners {} +} diff --git a/fullcalendar-main/packages/interaction/src/dnd/AutoScroller.ts b/fullcalendar-main/packages/interaction/src/dnd/AutoScroller.ts new file mode 100644 index 0000000..392a1ed --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/dnd/AutoScroller.ts @@ -0,0 +1,217 @@ +import { ScrollGeomCache } from '../ScrollGeomCache.js' +import { ElementScrollGeomCache } from '../ElementScrollGeomCache.js' +import { WindowScrollGeomCache } from '../WindowScrollGeomCache.js' + +interface Edge { + scrollCache: ScrollGeomCache + name: 'top' | 'left' | 'right' | 'bottom' + distance: number // how many pixels the current pointer is from the edge +} + +// If available we are using native "performance" API instead of "Date" +// Read more about it on MDN: +// https://developer.mozilla.org/en-US/docs/Web/API/Performance +const getTime = typeof performance === 'function' ? (performance as any).now : Date.now + +/* +For a pointer interaction, automatically scrolls certain scroll containers when the pointer +approaches the edge. + +The caller must call start + handleMove + stop. +*/ +export class AutoScroller { + // options that can be set by caller + isEnabled: boolean = true + scrollQuery: (Window | string)[] = [window, '.fc-scroller'] + edgeThreshold: number = 50 // pixels + maxVelocity: number = 300 // pixels per second + + // internal state + pointerScreenX: number | null = null + pointerScreenY: number | null = null + isAnimating: boolean = false + scrollCaches: ScrollGeomCache[] | null = null + msSinceRequest?: number + + // protect against the initial pointerdown being too close to an edge and starting the scroll + everMovedUp: boolean = false + everMovedDown: boolean = false + everMovedLeft: boolean = false + everMovedRight: boolean = false + + start(pageX: number, pageY: number, scrollStartEl: HTMLElement) { + if (this.isEnabled) { + this.scrollCaches = this.buildCaches(scrollStartEl) + this.pointerScreenX = null + this.pointerScreenY = null + this.everMovedUp = false + this.everMovedDown = false + this.everMovedLeft = false + this.everMovedRight = false + this.handleMove(pageX, pageY) + } + } + + handleMove(pageX: number, pageY: number) { + if (this.isEnabled) { + let pointerScreenX = pageX - window.pageXOffset + let pointerScreenY = pageY - window.pageYOffset + + let yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY + let xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX + + if (yDelta < 0) { + this.everMovedUp = true + } else if (yDelta > 0) { + this.everMovedDown = true + } + + if (xDelta < 0) { + this.everMovedLeft = true + } else if (xDelta > 0) { + this.everMovedRight = true + } + + this.pointerScreenX = pointerScreenX + this.pointerScreenY = pointerScreenY + + if (!this.isAnimating) { + this.isAnimating = true + this.requestAnimation(getTime()) + } + } + } + + stop() { + if (this.isEnabled) { + this.isAnimating = false // will stop animation + + for (let scrollCache of this.scrollCaches!) { + scrollCache.destroy() + } + + this.scrollCaches = null + } + } + + requestAnimation(now: number) { + this.msSinceRequest = now + requestAnimationFrame(this.animate) + } + + private animate = () => { + if (this.isAnimating) { // wasn't cancelled between animation calls + let edge = this.computeBestEdge( + this.pointerScreenX! + window.pageXOffset, + this.pointerScreenY! + window.pageYOffset, + ) + + if (edge) { + let now = getTime() + this.handleSide(edge, (now - this.msSinceRequest!) / 1000) + this.requestAnimation(now) + } else { + this.isAnimating = false // will stop animation + } + } + } + + private handleSide(edge: Edge, seconds: number) { + let { scrollCache } = edge + let { edgeThreshold } = this + let invDistance = edgeThreshold - edge.distance + let velocity = // the closer to the edge, the faster we scroll + ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic + this.maxVelocity * seconds + let sign = 1 + + switch (edge.name) { + case 'left': + sign = -1 + // falls through + case 'right': + scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign) + break + + case 'top': + sign = -1 + // falls through + case 'bottom': + scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign) + break + } + } + + // left/top are relative to document topleft + private computeBestEdge(left: number, top: number): Edge | null { + let { edgeThreshold } = this + let bestSide: Edge | null = null + let scrollCaches = this.scrollCaches || [] + + for (let scrollCache of scrollCaches) { + let rect = scrollCache.clientRect + let leftDist = left - rect.left + let rightDist = rect.right - left + let topDist = top - rect.top + let bottomDist = rect.bottom - top + + // completely within the rect? + if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { + if ( + topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && + (!bestSide || bestSide.distance > topDist) + ) { + bestSide = { scrollCache, name: 'top', distance: topDist } + } + + if ( + bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && + (!bestSide || bestSide.distance > bottomDist) + ) { + bestSide = { scrollCache, name: 'bottom', distance: bottomDist } + } + + if ( + leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && + (!bestSide || bestSide.distance > leftDist) + ) { + bestSide = { scrollCache, name: 'left', distance: leftDist } + } + + if ( + rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && + (!bestSide || bestSide.distance > rightDist) + ) { + bestSide = { scrollCache, name: 'right', distance: rightDist } + } + } + } + + return bestSide + } + + private buildCaches(scrollStartEl: HTMLElement) { + return this.queryScrollEls(scrollStartEl).map((el) => { + if (el === window) { + return new WindowScrollGeomCache(false) // false = don't listen to user-generated scrolls + } + return new ElementScrollGeomCache(el, false) // false = don't listen to user-generated scrolls + }) + } + + private queryScrollEls(scrollStartEl: HTMLElement) { + let els = [] + + for (let query of this.scrollQuery) { + if (typeof query === 'object') { + els.push(query) + } else { + els.push(...Array.prototype.slice.call( + (scrollStartEl.getRootNode() as ParentNode).querySelectorAll(query), + )) + } + } + + return els + } +} diff --git a/fullcalendar-main/packages/interaction/src/dnd/ElementMirror.ts b/fullcalendar-main/packages/interaction/src/dnd/ElementMirror.ts new file mode 100644 index 0000000..295f82c --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/dnd/ElementMirror.ts @@ -0,0 +1,147 @@ +import { removeElement, applyStyle, whenTransitionDone, Rect } from '@fullcalendar/core/internal' + +/* +An effect in which an element follows the movement of a pointer across the screen. +The moving element is a clone of some other element. +Must call start + handleMove + stop. +*/ +export class ElementMirror { + isVisible: boolean = false // must be explicitly enabled + origScreenX?: number + origScreenY?: number + deltaX?: number + deltaY?: number + sourceEl: HTMLElement | null = null + mirrorEl: HTMLElement | null = null + sourceElRect: Rect | null = null // screen coords relative to viewport + + // options that can be set directly by caller + parentNode: HTMLElement = document.body // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues + zIndex: number = 9999 + revertDuration: number = 0 + + start(sourceEl: HTMLElement, pageX: number, pageY: number) { + this.sourceEl = sourceEl + this.sourceElRect = this.sourceEl.getBoundingClientRect() + this.origScreenX = pageX - window.pageXOffset + this.origScreenY = pageY - window.pageYOffset + this.deltaX = 0 + this.deltaY = 0 + this.updateElPosition() + } + + handleMove(pageX: number, pageY: number) { + this.deltaX = (pageX - window.pageXOffset) - this.origScreenX! + this.deltaY = (pageY - window.pageYOffset) - this.origScreenY! + this.updateElPosition() + } + + // can be called before start + setIsVisible(bool: boolean) { + if (bool) { + if (!this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = '' + } + + this.isVisible = bool // needs to happen before updateElPosition + this.updateElPosition() // because was not updating the position while invisible + } + } else if (this.isVisible) { + if (this.mirrorEl) { + this.mirrorEl.style.display = 'none' + } + + this.isVisible = bool + } + } + + // always async + stop(needsRevertAnimation: boolean, callback: () => void) { + let done = () => { + this.cleanup() + callback() + } + + if ( + needsRevertAnimation && + this.mirrorEl && + this.isVisible && + this.revertDuration && // if 0, transition won't work + (this.deltaX || this.deltaY) // if same coords, transition won't work + ) { + this.doRevertAnimation(done, this.revertDuration) + } else { + setTimeout(done, 0) + } + } + + doRevertAnimation(callback: () => void, revertDuration: number) { + let mirrorEl = this.mirrorEl! + let finalSourceElRect = this.sourceEl!.getBoundingClientRect() // because autoscrolling might have happened + + mirrorEl.style.transition = + 'top ' + revertDuration + 'ms,' + + 'left ' + revertDuration + 'ms' + + applyStyle(mirrorEl, { + left: finalSourceElRect.left, + top: finalSourceElRect.top, + }) + + whenTransitionDone(mirrorEl, () => { + mirrorEl.style.transition = '' + callback() + }) + } + + cleanup() { + if (this.mirrorEl) { + removeElement(this.mirrorEl) + this.mirrorEl = null + } + + this.sourceEl = null + } + + updateElPosition() { + if (this.sourceEl && this.isVisible) { + applyStyle(this.getMirrorEl(), { + left: this.sourceElRect!.left + this.deltaX!, + top: this.sourceElRect!.top + this.deltaY!, + }) + } + } + + getMirrorEl(): HTMLElement { + let sourceElRect = this.sourceElRect! + let mirrorEl = this.mirrorEl + + if (!mirrorEl) { + mirrorEl = this.mirrorEl = this.sourceEl!.cloneNode(true) as HTMLElement // cloneChildren=true + + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + mirrorEl.style.userSelect = 'none' + mirrorEl.style.webkitUserSelect = 'none' + + mirrorEl.classList.add('fc-event-dragging') + + applyStyle(mirrorEl, { + position: 'fixed', + zIndex: this.zIndex, + visibility: '', // in case original element was hidden by the drag effect + boxSizing: 'border-box', // for easy width/height + width: sourceElRect.right - sourceElRect.left, // explicit height in case there was a 'right' value + height: sourceElRect.bottom - sourceElRect.top, // explicit width in case there was a 'bottom' value + right: 'auto', // erase and set width instead + bottom: 'auto', // erase and set height instead + margin: 0, + }) + + this.parentNode.appendChild(mirrorEl) + } + + return mirrorEl + } +} diff --git a/fullcalendar-main/packages/interaction/src/dnd/FeaturefulElementDragging.ts b/fullcalendar-main/packages/interaction/src/dnd/FeaturefulElementDragging.ts new file mode 100644 index 0000000..9f67d53 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/dnd/FeaturefulElementDragging.ts @@ -0,0 +1,213 @@ +import { + PointerDragEvent, + preventSelection, + allowSelection, + preventContextMenu, + allowContextMenu, + ElementDragging, +} from '@fullcalendar/core/internal' +import { PointerDragging } from './PointerDragging.js' +import { ElementMirror } from './ElementMirror.js' +import { AutoScroller } from './AutoScroller.js' + +/* +Monitors dragging on an element. Has a number of high-level features: +- minimum distance required before dragging +- minimum wait time ("delay") before dragging +- a mirror element that follows the pointer +*/ +export class FeaturefulElementDragging extends ElementDragging { + pointer: PointerDragging + mirror: ElementMirror + autoScroller: AutoScroller + + // options that can be directly set by caller + // the caller can also set the PointerDragging's options as well + delay: number | null = null + minDistance: number = 0 + touchScrollAllowed: boolean = true // prevents drag from starting and blocks scrolling during drag + + mirrorNeedsRevert: boolean = false + isInteracting: boolean = false // is the user validly moving the pointer? lasts until pointerup + isDragging: boolean = false // is it INTENTFULLY dragging? lasts until after revert animation + isDelayEnded: boolean = false + isDistanceSurpassed: boolean = false + delayTimeoutId: number | null = null + + constructor(private containerEl: HTMLElement, selector?: string) { + super(containerEl) + + let pointer = this.pointer = new PointerDragging(containerEl) + pointer.emitter.on('pointerdown', this.onPointerDown) + pointer.emitter.on('pointermove', this.onPointerMove) + pointer.emitter.on('pointerup', this.onPointerUp) + + if (selector) { + pointer.selector = selector + } + + this.mirror = new ElementMirror() + this.autoScroller = new AutoScroller() + } + + destroy() { + this.pointer.destroy() + + // HACK: simulate a pointer-up to end the current drag + // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire) + this.onPointerUp({} as any) + } + + onPointerDown = (ev: PointerDragEvent) => { + if (!this.isDragging) { // so new drag doesn't happen while revert animation is going + this.isInteracting = true + this.isDelayEnded = false + this.isDistanceSurpassed = false + + preventSelection(document.body) + preventContextMenu(document.body) + + // prevent links from being visited if there's an eventual drag. + // also prevents selection in older browsers (maybe?). + // not necessary for touch, besides, browser would complain about passiveness. + if (!ev.isTouch) { + ev.origEvent.preventDefault() + } + + this.emitter.trigger('pointerdown', ev) + + if ( + this.isInteracting && // not destroyed via pointerdown handler + !this.pointer.shouldIgnoreMove + ) { + // actions related to initiating dragstart+dragmove+dragend... + + this.mirror.setIsVisible(false) // reset. caller must set-visible + this.mirror.start(ev.subjectEl as HTMLElement, ev.pageX, ev.pageY) // must happen on first pointer down + + this.startDelay(ev) + + if (!this.minDistance) { + this.handleDistanceSurpassed(ev) + } + } + } + } + + onPointerMove = (ev: PointerDragEvent) => { + if (this.isInteracting) { + this.emitter.trigger('pointermove', ev) + + if (!this.isDistanceSurpassed) { + let minDistance = this.minDistance + let distanceSq // current distance from the origin, squared + let { deltaX, deltaY } = ev + + distanceSq = deltaX * deltaX + deltaY * deltaY + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.handleDistanceSurpassed(ev) + } + } + + if (this.isDragging) { + // a real pointer move? (not one simulated by scrolling) + if (ev.origEvent.type !== 'scroll') { + this.mirror.handleMove(ev.pageX, ev.pageY) + this.autoScroller.handleMove(ev.pageX, ev.pageY) + } + + this.emitter.trigger('dragmove', ev) + } + } + } + + onPointerUp = (ev: PointerDragEvent) => { + if (this.isInteracting) { + this.isInteracting = false + + allowSelection(document.body) + allowContextMenu(document.body) + + this.emitter.trigger('pointerup', ev) // can potentially set mirrorNeedsRevert + + if (this.isDragging) { + this.autoScroller.stop() + this.tryStopDrag(ev) // which will stop the mirror + } + + if (this.delayTimeoutId) { + clearTimeout(this.delayTimeoutId) + this.delayTimeoutId = null + } + } + } + + startDelay(ev: PointerDragEvent) { + if (typeof this.delay === 'number') { + this.delayTimeoutId = setTimeout(() => { + this.delayTimeoutId = null + this.handleDelayEnd(ev) + }, this.delay) as any // not assignable to number! + } else { + this.handleDelayEnd(ev) + } + } + + handleDelayEnd(ev: PointerDragEvent) { + this.isDelayEnded = true + this.tryStartDrag(ev) + } + + handleDistanceSurpassed(ev: PointerDragEvent) { + this.isDistanceSurpassed = true + this.tryStartDrag(ev) + } + + tryStartDrag(ev: PointerDragEvent) { + if (this.isDelayEnded && this.isDistanceSurpassed) { + if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { + this.isDragging = true + this.mirrorNeedsRevert = false + + this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl) + this.emitter.trigger('dragstart', ev) + + if (this.touchScrollAllowed === false) { + this.pointer.cancelTouchScroll() + } + } + } + } + + tryStopDrag(ev: PointerDragEvent) { + // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events + // that come from the document to fire beforehand. much more convenient this way. + this.mirror.stop( + this.mirrorNeedsRevert, + this.stopDrag.bind(this, ev), // bound with args + ) + } + + stopDrag(ev: PointerDragEvent) { + this.isDragging = false + this.emitter.trigger('dragend', ev) + } + + // fill in the implementations... + + setIgnoreMove(bool: boolean) { + this.pointer.shouldIgnoreMove = bool + } + + setMirrorIsVisible(bool: boolean) { + this.mirror.setIsVisible(bool) + } + + setMirrorNeedsRevert(bool: boolean) { + this.mirrorNeedsRevert = bool + } + + setAutoScrollEnabled(bool: boolean) { + this.autoScroller.isEnabled = bool + } +} diff --git a/fullcalendar-main/packages/interaction/src/dnd/PointerDragging.ts b/fullcalendar-main/packages/interaction/src/dnd/PointerDragging.ts new file mode 100644 index 0000000..64ae5af --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/dnd/PointerDragging.ts @@ -0,0 +1,344 @@ +import { config, elementClosest, Emitter, PointerDragEvent } from '@fullcalendar/core/internal' + +config.touchMouseIgnoreWait = 500 + +let ignoreMouseDepth = 0 +let listenerCnt = 0 +let isWindowTouchMoveCancelled = false + +/* +Uses a "pointer" abstraction, which monitors UI events for both mouse and touch. +Tracks when the pointer "drags" on a certain element, meaning down+move+up. + +Also, tracks if there was touch-scrolling. +Also, can prevent touch-scrolling from happening. +Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement. + +emits: +- pointerdown +- pointermove +- pointerup +*/ +export class PointerDragging { + containerEl: EventTarget + subjectEl: HTMLElement | null = null + emitter: Emitter<any> + + // options that can be directly assigned by caller + selector: string = '' // will cause subjectEl in all emitted events to be this element + handleSelector: string = '' + shouldIgnoreMove: boolean = false + shouldWatchScroll: boolean = true // for simulating pointermove on scroll + + // internal states + isDragging: boolean = false + isTouchDragging: boolean = false + wasTouchScroll: boolean = false + origPageX: number + origPageY: number + prevPageX: number + prevPageY: number + prevScrollX: number // at time of last pointer pageX/pageY capture + prevScrollY: number // " + + constructor(containerEl: EventTarget) { + this.containerEl = containerEl + this.emitter = new Emitter() + containerEl.addEventListener('mousedown', this.handleMouseDown as EventListener) + containerEl.addEventListener('touchstart', this.handleTouchStart as EventListener, { passive: true }) + listenerCreated() + } + + destroy() { + this.containerEl.removeEventListener('mousedown', this.handleMouseDown as EventListener) + this.containerEl.removeEventListener('touchstart', this.handleTouchStart as EventListener, { passive: true } as AddEventListenerOptions) + listenerDestroyed() + } + + tryStart(ev: UIEvent): boolean { + let subjectEl = this.querySubjectEl(ev) + let downEl = ev.target as HTMLElement + + if ( + subjectEl && + (!this.handleSelector || elementClosest(downEl, this.handleSelector)) + ) { + this.subjectEl = subjectEl + this.isDragging = true // do this first so cancelTouchScroll will work + this.wasTouchScroll = false + + return true + } + + return false + } + + cleanup() { + isWindowTouchMoveCancelled = false + this.isDragging = false + this.subjectEl = null + // keep wasTouchScroll around for later access + this.destroyScrollWatch() + } + + querySubjectEl(ev: UIEvent): HTMLElement { + if (this.selector) { + return elementClosest(ev.target as HTMLElement, this.selector) + } + return this.containerEl as HTMLElement + } + + // Mouse + // ---------------------------------------------------------------------------------------------------- + + handleMouseDown = (ev: MouseEvent) => { + if ( + !this.shouldIgnoreMouse() && + isPrimaryMouseButton(ev) && + this.tryStart(ev) + ) { + let pev = this.createEventFromMouse(ev, true) + this.emitter.trigger('pointerdown', pev) + this.initScrollWatch(pev) + + if (!this.shouldIgnoreMove) { + document.addEventListener('mousemove', this.handleMouseMove) + } + + document.addEventListener('mouseup', this.handleMouseUp) + } + } + + handleMouseMove = (ev: MouseEvent) => { + let pev = this.createEventFromMouse(ev) + this.recordCoords(pev) + this.emitter.trigger('pointermove', pev) + } + + handleMouseUp = (ev: MouseEvent) => { + document.removeEventListener('mousemove', this.handleMouseMove) + document.removeEventListener('mouseup', this.handleMouseUp) + + this.emitter.trigger('pointerup', this.createEventFromMouse(ev)) + + this.cleanup() // call last so that pointerup has access to props + } + + shouldIgnoreMouse() { + return ignoreMouseDepth || this.isTouchDragging + } + + // Touch + // ---------------------------------------------------------------------------------------------------- + + handleTouchStart = (ev: TouchEvent) => { + if (this.tryStart(ev)) { + this.isTouchDragging = true + + let pev = this.createEventFromTouch(ev, true) + this.emitter.trigger('pointerdown', pev) + this.initScrollWatch(pev) + + // unlike mouse, need to attach to target, not document + // https://stackoverflow.com/a/45760014 + let targetEl = ev.target as HTMLElement + + if (!this.shouldIgnoreMove) { + targetEl.addEventListener('touchmove', this.handleTouchMove) + } + + targetEl.addEventListener('touchend', this.handleTouchEnd) + targetEl.addEventListener('touchcancel', this.handleTouchEnd) // treat it as a touch end + + // attach a handler to get called when ANY scroll action happens on the page. + // this was impossible to do with normal on/off because 'scroll' doesn't bubble. + // http://stackoverflow.com/a/32954565/96342 + window.addEventListener( + 'scroll', + this.handleTouchScroll, + true, // useCapture + ) + } + } + + handleTouchMove = (ev: TouchEvent) => { + let pev = this.createEventFromTouch(ev) + this.recordCoords(pev) + this.emitter.trigger('pointermove', pev) + } + + handleTouchEnd = (ev: TouchEvent) => { + if (this.isDragging) { // done to guard against touchend followed by touchcancel + let targetEl = ev.target as HTMLElement + + targetEl.removeEventListener('touchmove', this.handleTouchMove) + targetEl.removeEventListener('touchend', this.handleTouchEnd) + targetEl.removeEventListener('touchcancel', this.handleTouchEnd) + window.removeEventListener('scroll', this.handleTouchScroll, true) // useCaptured=true + + this.emitter.trigger('pointerup', this.createEventFromTouch(ev)) + + this.cleanup() // call last so that pointerup has access to props + this.isTouchDragging = false + startIgnoringMouse() + } + } + + handleTouchScroll = () => { + this.wasTouchScroll = true + } + + // can be called by user of this class, to cancel touch-based scrolling for the current drag + cancelTouchScroll() { + if (this.isDragging) { + isWindowTouchMoveCancelled = true + } + } + + // Scrolling that simulates pointermoves + // ---------------------------------------------------------------------------------------------------- + + initScrollWatch(ev: PointerDragEvent) { + if (this.shouldWatchScroll) { + this.recordCoords(ev) + window.addEventListener('scroll', this.handleScroll, true) // useCapture=true + } + } + + recordCoords(ev: PointerDragEvent) { + if (this.shouldWatchScroll) { + this.prevPageX = (ev as any).pageX + this.prevPageY = (ev as any).pageY + this.prevScrollX = window.pageXOffset + this.prevScrollY = window.pageYOffset + } + } + + handleScroll = (ev: UIEvent) => { + if (!this.shouldIgnoreMove) { + let pageX = (window.pageXOffset - this.prevScrollX) + this.prevPageX + let pageY = (window.pageYOffset - this.prevScrollY) + this.prevPageY + + this.emitter.trigger('pointermove', { + origEvent: ev, + isTouch: this.isTouchDragging, + subjectEl: this.subjectEl, + pageX, + pageY, + deltaX: pageX - this.origPageX, + deltaY: pageY - this.origPageY, + } as PointerDragEvent) + } + } + + destroyScrollWatch() { + if (this.shouldWatchScroll) { + window.removeEventListener('scroll', this.handleScroll, true) // useCaptured=true + } + } + + // Event Normalization + // ---------------------------------------------------------------------------------------------------- + + createEventFromMouse(ev: MouseEvent, isFirst?: boolean): PointerDragEvent { + let deltaX = 0 + let deltaY = 0 + + // TODO: repeat code + if (isFirst) { + this.origPageX = ev.pageX + this.origPageY = ev.pageY + } else { + deltaX = ev.pageX - this.origPageX + deltaY = ev.pageY - this.origPageY + } + + return { + origEvent: ev, + isTouch: false, + subjectEl: this.subjectEl, + pageX: ev.pageX, + pageY: ev.pageY, + deltaX, + deltaY, + } + } + + createEventFromTouch(ev: TouchEvent, isFirst?: boolean): PointerDragEvent { + let touches = ev.touches + let pageX + let pageY + let deltaX = 0 + let deltaY = 0 + + // if touch coords available, prefer, + // because FF would give bad ev.pageX ev.pageY + if (touches && touches.length) { + pageX = touches[0].pageX + pageY = touches[0].pageY + } else { + pageX = (ev as any).pageX + pageY = (ev as any).pageY + } + + // TODO: repeat code + if (isFirst) { + this.origPageX = pageX + this.origPageY = pageY + } else { + deltaX = pageX - this.origPageX + deltaY = pageY - this.origPageY + } + + return { + origEvent: ev, + isTouch: true, + subjectEl: this.subjectEl, + pageX, + pageY, + deltaX, + deltaY, + } + } +} + +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) +function isPrimaryMouseButton(ev: MouseEvent) { + return ev.button === 0 && !ev.ctrlKey +} + +// Ignoring fake mouse events generated by touch +// ---------------------------------------------------------------------------------------------------- + +function startIgnoringMouse() { // can be made non-class function + ignoreMouseDepth += 1 + + setTimeout(() => { + ignoreMouseDepth -= 1 + }, config.touchMouseIgnoreWait) +} + +// We want to attach touchmove as early as possible for Safari +// ---------------------------------------------------------------------------------------------------- + +function listenerCreated() { + listenerCnt += 1 + + if (listenerCnt === 1) { + window.addEventListener('touchmove', onWindowTouchMove, { passive: false }) + } +} + +function listenerDestroyed() { + listenerCnt -= 1 + + if (!listenerCnt) { + window.removeEventListener('touchmove', onWindowTouchMove, { passive: false } as AddEventListenerOptions) + } +} + +function onWindowTouchMove(ev: UIEvent) { + if (isWindowTouchMoveCancelled) { + ev.preventDefault() + } +} diff --git a/fullcalendar-main/packages/interaction/src/index.global.ts b/fullcalendar-main/packages/interaction/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/interaction/src/index.ts b/fullcalendar-main/packages/interaction/src/index.ts new file mode 100644 index 0000000..5443004 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/index.ts @@ -0,0 +1,22 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { DateClicking } from './interactions/DateClicking.js' +import { DateSelecting } from './interactions/DateSelecting.js' +import { EventDragging } from './interactions/EventDragging.js' +import { EventResizing } from './interactions/EventResizing.js' +import { UnselectAuto } from './interactions/UnselectAuto.js' +import { FeaturefulElementDragging } from './dnd/FeaturefulElementDragging.js' +import { OPTION_REFINERS, LISTENER_REFINERS } from './option-refiners.js' +import './ambient.js' + +export default createPlugin({ + name: '<%= pkgName %>', + componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing], + calendarInteractions: [UnselectAuto], + elementDraggingImpl: FeaturefulElementDragging, + optionRefiners: OPTION_REFINERS, + listenerRefiners: LISTENER_REFINERS, +}) as PluginDef + +export * from './public-types.js' +export { ExternalDraggable as Draggable } from './interactions-external/ExternalDraggable.js' +export { ThirdPartyDraggable } from './interactions-external/ThirdPartyDraggable.js' diff --git a/fullcalendar-main/packages/interaction/src/interactions-external/ExternalDraggable.ts b/fullcalendar-main/packages/interaction/src/interactions-external/ExternalDraggable.ts new file mode 100644 index 0000000..79b9538 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions-external/ExternalDraggable.ts @@ -0,0 +1,70 @@ +import { BASE_OPTION_DEFAULTS, PointerDragEvent } from '@fullcalendar/core/internal' +import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging.js' +import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging.js' + +export interface ExternalDraggableSettings { + eventData?: DragMetaGenerator + itemSelector?: string + minDistance?: number + longPressDelay?: number + appendTo?: HTMLElement +} + +/* +Makes an element (that is *external* to any calendar) draggable. +Can pass in data that determines how an event will be created when dropped onto a calendar. +Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. +*/ +export class ExternalDraggable { + dragging: FeaturefulElementDragging + settings: ExternalDraggableSettings + + constructor(el: HTMLElement, settings: ExternalDraggableSettings = {}) { + this.settings = settings + + let dragging = this.dragging = new FeaturefulElementDragging(el) + dragging.touchScrollAllowed = false + + if (settings.itemSelector != null) { + dragging.pointer.selector = settings.itemSelector + } + + if (settings.appendTo != null) { + dragging.mirror.parentNode = settings.appendTo // TODO: write tests + } + + dragging.emitter.on('pointerdown', this.handlePointerDown) + dragging.emitter.on('dragstart', this.handleDragStart) + + new ExternalElementDragging(dragging, settings.eventData) // eslint-disable-line no-new + } + + handlePointerDown = (ev: PointerDragEvent) => { + let { dragging } = this + let { minDistance, longPressDelay } = this.settings + + dragging.minDistance = + minDistance != null ? + minDistance : + (ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance) + + dragging.delay = + ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv + (longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) : + 0 + } + + handleDragStart = (ev: PointerDragEvent) => { + if ( + ev.isTouch && + this.dragging.delay && + (ev.subjectEl as HTMLElement).classList.contains('fc-event') + ) { + this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected') + } + } + + destroy() { + this.dragging.destroy() + } +} diff --git a/fullcalendar-main/packages/interaction/src/interactions-external/ExternalElementDragging.ts b/fullcalendar-main/packages/interaction/src/interactions-external/ExternalElementDragging.ts new file mode 100644 index 0000000..66adf99 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions-external/ExternalElementDragging.ts @@ -0,0 +1,267 @@ +import { ViewApi, DatePointApi } from '@fullcalendar/core' +import { + Hit, + interactionSettingsStore, + PointerDragEvent, + parseEventDef, createEventInstance, EventTuple, + createEmptyEventStore, eventTupleToStore, + config, + DateSpan, + EventInteractionState, + DragMetaInput, DragMeta, parseDragMeta, + elementMatches, + enableCursor, disableCursor, + isInteractionValid, + ElementDragging, + CalendarContext, + getDefaultEventEnd, + refineEventDef, + EventImpl, +} from '@fullcalendar/core/internal' +import { HitDragging } from '../interactions/HitDragging.js' +import { buildDatePointApiWithContext } from '../utils.js' + +export type DragMetaGenerator = DragMetaInput | ((el: HTMLElement) => DragMetaInput) + +export interface ExternalDropApi extends DatePointApi { + draggedEl: HTMLElement + jsEvent: UIEvent + view: ViewApi +} + +/* +Given an already instantiated draggable object for one-or-more elements, +Interprets any dragging as an attempt to drag an events that lives outside +of a calendar onto a calendar. +*/ +export class ExternalElementDragging { + hitDragging: HitDragging + receivingContext: CalendarContext | null = null + droppableEvent: EventTuple | null = null // will exist for all drags, even if create:false + suppliedDragMeta: DragMetaGenerator | null = null + dragMeta: DragMeta | null = null + + constructor(dragging: ElementDragging, suppliedDragMeta?: DragMetaGenerator) { + let hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore) + hitDragging.requireInitial = false // will start outside of a component + hitDragging.emitter.on('dragstart', this.handleDragStart) + hitDragging.emitter.on('hitupdate', this.handleHitUpdate) + hitDragging.emitter.on('dragend', this.handleDragEnd) + + this.suppliedDragMeta = suppliedDragMeta + } + + handleDragStart = (ev: PointerDragEvent) => { + this.dragMeta = this.buildDragMeta(ev.subjectEl as HTMLElement) + } + + buildDragMeta(subjectEl: HTMLElement) { + if (typeof this.suppliedDragMeta === 'object') { + return parseDragMeta(this.suppliedDragMeta) + } + if (typeof this.suppliedDragMeta === 'function') { + return parseDragMeta(this.suppliedDragMeta(subjectEl)) + } + return getDragMetaFromEl(subjectEl) + } + + handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => { + let { dragging } = this.hitDragging + let receivingContext: CalendarContext | null = null + let droppableEvent: EventTuple | null = null + let isInvalid = false + let interaction: EventInteractionState = { + affectedEvents: createEmptyEventStore(), + mutatedEvents: createEmptyEventStore(), + isEvent: this.dragMeta!.create, + } + + if (hit) { + receivingContext = hit.context + + if (this.canDropElOnCalendar(ev.subjectEl as HTMLElement, receivingContext)) { + droppableEvent = computeEventForDateSpan( + hit.dateSpan, + this.dragMeta!, + receivingContext, + ) + + interaction.mutatedEvents = eventTupleToStore(droppableEvent) + isInvalid = !isInteractionValid(interaction, hit.dateProfile, receivingContext) + + if (isInvalid) { + interaction.mutatedEvents = createEmptyEventStore() + droppableEvent = null + } + } + } + + this.displayDrag(receivingContext, interaction) + + // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) + // TODO: wish we could somehow wait for dispatch to guarantee render + dragging.setMirrorIsVisible( + isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror'), // TODO: turn className into constant + // TODO: somehow query FullCalendars WITHIN shadow-roots for existing event-mirror els + ) + + if (!isInvalid) { + enableCursor() + } else { + disableCursor() + } + + if (!isFinal) { + dragging.setMirrorNeedsRevert(!droppableEvent) + + this.receivingContext = receivingContext + this.droppableEvent = droppableEvent + } + } + + handleDragEnd = (pev: PointerDragEvent) => { + let { receivingContext, droppableEvent } = this + + this.clearDrag() + + if (receivingContext && droppableEvent) { + let finalHit = this.hitDragging.finalHit! + let finalView = finalHit.context.viewApi + let dragMeta = this.dragMeta! + + receivingContext.emitter.trigger('drop', { + ...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext), + draggedEl: pev.subjectEl as HTMLElement, + jsEvent: pev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: finalView, + }) + + if (dragMeta.create) { + let addingEvents = eventTupleToStore(droppableEvent) + + receivingContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: addingEvents, + }) + + if (pev.isTouch) { + receivingContext.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: droppableEvent.instance.instanceId, + }) + } + + // signal that an external event landed + receivingContext.emitter.trigger('eventReceive', { + event: new EventImpl( + receivingContext, + droppableEvent.def, + droppableEvent.instance, + ), + relatedEvents: [], + revert() { + receivingContext.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: addingEvents, + }) + }, + draggedEl: pev.subjectEl as HTMLElement, + view: finalView, + }) + } + } + + this.receivingContext = null + this.droppableEvent = null + } + + displayDrag(nextContext: CalendarContext | null, state: EventInteractionState) { + let prevContext = this.receivingContext + + if (prevContext && prevContext !== nextContext) { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }) + } + + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state }) + } + } + + clearDrag() { + if (this.receivingContext) { + this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }) + } + } + + canDropElOnCalendar(el: HTMLElement, receivingContext: CalendarContext): boolean { + let dropAccept = receivingContext.options.dropAccept + + if (typeof dropAccept === 'function') { + return dropAccept.call(receivingContext.calendarApi, el) + } + + if (typeof dropAccept === 'string' && dropAccept) { + return Boolean(elementMatches(el, dropAccept)) + } + + return true + } +} + +// Utils for computing event store from the DragMeta +// ---------------------------------------------------------------------------------------------------- + +function computeEventForDateSpan(dateSpan: DateSpan, dragMeta: DragMeta, context: CalendarContext): EventTuple { + let defProps = { ...dragMeta.leftoverProps } + + for (let transform of context.pluginHooks.externalDefTransforms) { + Object.assign(defProps, transform(dateSpan, dragMeta)) + } + + let { refined, extra } = refineEventDef(defProps, context) + let def = parseEventDef( + refined, + extra, + dragMeta.sourceId, + dateSpan.allDay, + context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd + context, + ) + + let start = dateSpan.range.start + + // only rely on time info if drop zone is all-day, + // otherwise, we already know the time + if (dateSpan.allDay && dragMeta.startTime) { + start = context.dateEnv.add(start, dragMeta.startTime) + } + + let end = dragMeta.duration ? + context.dateEnv.add(start, dragMeta.duration) : + getDefaultEventEnd(dateSpan.allDay, start, context) + + let instance = createEventInstance(def.defId, { start, end }) + + return { def, instance } +} + +// Utils for extracting data from element +// ---------------------------------------------------------------------------------------------------- + +function getDragMetaFromEl(el: HTMLElement): DragMeta { + let str = getEmbeddedElData(el, 'event') + let obj = str ? + JSON.parse(str) : + { create: false } // if no embedded data, assume no event creation + + return parseDragMeta(obj) +} + +config.dataAttrPrefix = '' + +function getEmbeddedElData(el: HTMLElement, name: string): string { + let prefix = config.dataAttrPrefix + let prefixedName = (prefix ? prefix + '-' : '') + name + + return el.getAttribute('data-' + prefixedName) || '' +} diff --git a/fullcalendar-main/packages/interaction/src/interactions-external/InferredElementDragging.ts b/fullcalendar-main/packages/interaction/src/interactions-external/InferredElementDragging.ts new file mode 100644 index 0000000..31b4095 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions-external/InferredElementDragging.ts @@ -0,0 +1,77 @@ +import { PointerDragEvent, ElementDragging } from '@fullcalendar/core/internal' +import { PointerDragging } from '../dnd/PointerDragging.js' + +/* +Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. +The third-party system is responsible for drawing the visuals effects of the drag. +This class simply monitors for pointer movements and fires events. +It also has the ability to hide the moving element (the "mirror") during the drag. +*/ +export class InferredElementDragging extends ElementDragging { + pointer: PointerDragging + shouldIgnoreMove: boolean = false + mirrorSelector: string = '' + currentMirrorEl: HTMLElement | null = null + + constructor(containerEl: HTMLElement) { + super(containerEl) + + let pointer = this.pointer = new PointerDragging(containerEl) + pointer.emitter.on('pointerdown', this.handlePointerDown) + pointer.emitter.on('pointermove', this.handlePointerMove) + pointer.emitter.on('pointerup', this.handlePointerUp) + } + + destroy() { + this.pointer.destroy() + } + + handlePointerDown = (ev: PointerDragEvent) => { + this.emitter.trigger('pointerdown', ev) + + if (!this.shouldIgnoreMove) { + // fire dragstart right away. does not support delay or min-distance + this.emitter.trigger('dragstart', ev) + } + } + + handlePointerMove = (ev: PointerDragEvent) => { + if (!this.shouldIgnoreMove) { + this.emitter.trigger('dragmove', ev) + } + } + + handlePointerUp = (ev: PointerDragEvent) => { + this.emitter.trigger('pointerup', ev) + + if (!this.shouldIgnoreMove) { + // fire dragend right away. does not support a revert animation + this.emitter.trigger('dragend', ev) + } + } + + setIgnoreMove(bool: boolean) { + this.shouldIgnoreMove = bool + } + + setMirrorIsVisible(bool: boolean) { + if (bool) { + // restore a previously hidden element. + // use the reference in case the selector class has already been removed. + if (this.currentMirrorEl) { + this.currentMirrorEl.style.visibility = '' + this.currentMirrorEl = null + } + } else { + let mirrorEl = this.mirrorSelector + // TODO: somehow query FullCalendars WITHIN shadow-roots + ? document.querySelector(this.mirrorSelector) as HTMLElement + : null + + if (mirrorEl) { + this.currentMirrorEl = mirrorEl + mirrorEl.style.visibility = 'hidden' + } + } + } +} diff --git a/fullcalendar-main/packages/interaction/src/interactions-external/ThirdPartyDraggable.ts b/fullcalendar-main/packages/interaction/src/interactions-external/ThirdPartyDraggable.ts new file mode 100644 index 0000000..5cfce03 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions-external/ThirdPartyDraggable.ts @@ -0,0 +1,52 @@ +import { ExternalElementDragging, DragMetaGenerator } from './ExternalElementDragging.js' +import { InferredElementDragging } from './InferredElementDragging.js' + +export interface ThirdPartyDraggableSettings { + eventData?: DragMetaGenerator + itemSelector?: string + mirrorSelector?: string +} + +/* +Bridges third-party drag-n-drop systems with FullCalendar. +Must be instantiated and destroyed by caller. +*/ +export class ThirdPartyDraggable { + dragging: InferredElementDragging + + constructor( + containerOrSettings?: EventTarget | ThirdPartyDraggableSettings, + settings?: ThirdPartyDraggableSettings, + ) { + let containerEl: EventTarget = document + + if ( + // wish we could just test instanceof EventTarget, but doesn't work in IE11 + containerOrSettings === document || + containerOrSettings instanceof Element + ) { + containerEl = containerOrSettings as EventTarget + settings = settings || {} + } else { + settings = (containerOrSettings || {}) as ThirdPartyDraggableSettings + } + + let dragging = this.dragging = new InferredElementDragging(containerEl as HTMLElement) + + if (typeof settings.itemSelector === 'string') { + dragging.pointer.selector = settings.itemSelector + } else if (containerEl === document) { + dragging.pointer.selector = '[data-event]' + } + + if (typeof settings.mirrorSelector === 'string') { + dragging.mirrorSelector = settings.mirrorSelector + } + + new ExternalElementDragging(dragging, settings.eventData) // eslint-disable-line no-new + } + + destroy() { + this.dragging.destroy() + } +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/DateClicking.ts b/fullcalendar-main/packages/interaction/src/interactions/DateClicking.ts new file mode 100644 index 0000000..1829447 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/DateClicking.ts @@ -0,0 +1,70 @@ +import { ViewApi, DatePointApi } from '@fullcalendar/core' +import { + PointerDragEvent, Interaction, InteractionSettings, interactionSettingsToStore, +} from '@fullcalendar/core/internal' +import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging.js' +import { HitDragging, isHitsEqual } from './HitDragging.js' +import { buildDatePointApiWithContext } from '../utils.js' + +export interface DateClickArg extends DatePointApi { + dayEl: HTMLElement + jsEvent: MouseEvent + view: ViewApi +} + +/* +Monitors when the user clicks on a specific date/time of a component. +A pointerdown+pointerup on the same "hit" constitutes a click. +*/ +export class DateClicking extends Interaction { + dragging: FeaturefulElementDragging + hitDragging: HitDragging + + constructor(settings: InteractionSettings) { + super(settings) + + // we DO want to watch pointer moves because otherwise finalHit won't get populated + this.dragging = new FeaturefulElementDragging(settings.el) + this.dragging.autoScroller.isEnabled = false + + let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings)) + hitDragging.emitter.on('pointerdown', this.handlePointerDown) + hitDragging.emitter.on('dragend', this.handleDragEnd) + } + + destroy() { + this.dragging.destroy() + } + + handlePointerDown = (pev: PointerDragEvent) => { + let { dragging } = this + let downEl = pev.origEvent.target as HTMLElement + + // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired + dragging.setIgnoreMove( + !this.component.isValidDateDownEl(downEl), + ) + } + + // won't even fire if moving was ignored + handleDragEnd = (ev: PointerDragEvent) => { + let { component } = this + let { pointer } = this.dragging + + if (!pointer.wasTouchScroll) { + let { initialHit, finalHit } = this.hitDragging + + if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { + let { context } = component + let arg: DateClickArg = { + ...buildDatePointApiWithContext(initialHit.dateSpan, context), + dayEl: initialHit.dayEl, + jsEvent: ev.origEvent as MouseEvent, + view: context.viewApi || context.calendarApi.view, + } + + context.emitter.trigger('dateClick', arg) + } + } + } +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/DateSelecting.ts b/fullcalendar-main/packages/interaction/src/interactions/DateSelecting.ts new file mode 100644 index 0000000..ec15dab --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/DateSelecting.ts @@ -0,0 +1,151 @@ +import { + compareNumbers, enableCursor, disableCursor, DateComponent, Hit, + DateSpan, PointerDragEvent, dateSelectionJoinTransformer, + Interaction, InteractionSettings, interactionSettingsToStore, + triggerDateSelect, isDateSelectionValid, +} from '@fullcalendar/core/internal' +import { HitDragging } from './HitDragging.js' +import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging.js' + +/* +Tracks when the user selects a portion of time of a component, +constituted by a drag over date cells, with a possible delay at the beginning of the drag. +*/ +export class DateSelecting extends Interaction { + dragging: FeaturefulElementDragging + hitDragging: HitDragging + dragSelection: DateSpan | null = null + + constructor(settings: InteractionSettings) { + super(settings) + let { component } = settings + let { options } = component.context + + let dragging = this.dragging = new FeaturefulElementDragging(settings.el) + dragging.touchScrollAllowed = false + dragging.minDistance = options.selectMinDistance || 0 + dragging.autoScroller.isEnabled = options.dragScroll + + let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings)) + hitDragging.emitter.on('pointerdown', this.handlePointerDown) + hitDragging.emitter.on('dragstart', this.handleDragStart) + hitDragging.emitter.on('hitupdate', this.handleHitUpdate) + hitDragging.emitter.on('pointerup', this.handlePointerUp) + } + + destroy() { + this.dragging.destroy() + } + + handlePointerDown = (ev: PointerDragEvent) => { + let { component, dragging } = this + let { options } = component.context + + let canSelect = options.selectable && + component.isValidDateDownEl(ev.origEvent.target as HTMLElement) + + // don't bother to watch expensive moves if component won't do selection + dragging.setIgnoreMove(!canSelect) + + // if touch, require user to hold down + dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null + } + + handleDragStart = (ev: PointerDragEvent) => { + this.component.context.calendarApi.unselect(ev) // unselect previous selections + } + + handleHitUpdate = (hit: Hit | null, isFinal: boolean) => { + let { context } = this.component + let dragSelection: DateSpan | null = null + let isInvalid = false + + if (hit) { + let initialHit = this.hitDragging.initialHit! + let disallowed = hit.componentId === initialHit.componentId + && this.isHitComboAllowed + && !this.isHitComboAllowed(initialHit, hit) + + if (!disallowed) { + dragSelection = joinHitsIntoSelection( + initialHit, + hit, + context.pluginHooks.dateSelectionTransformers, + ) + } + + if (!dragSelection || !isDateSelectionValid(dragSelection, hit.dateProfile, context)) { + isInvalid = true + dragSelection = null + } + } + + if (dragSelection) { + context.dispatch({ type: 'SELECT_DATES', selection: dragSelection }) + } else if (!isFinal) { // only unselect if moved away while dragging + context.dispatch({ type: 'UNSELECT_DATES' }) + } + + if (!isInvalid) { + enableCursor() + } else { + disableCursor() + } + + if (!isFinal) { + this.dragSelection = dragSelection // only clear if moved away from all hits while dragging + } + } + + handlePointerUp = (pev: PointerDragEvent) => { + if (this.dragSelection) { + // selection is already rendered, so just need to report selection + triggerDateSelect(this.dragSelection, pev, this.component.context) + + this.dragSelection = null + } + } +} + +function getComponentTouchDelay(component: DateComponent<any>): number { + let { options } = component.context + let delay = options.selectLongPressDelay + + if (delay == null) { + delay = options.longPressDelay + } + + return delay +} + +function joinHitsIntoSelection(hit0: Hit, hit1: Hit, dateSelectionTransformers: dateSelectionJoinTransformer[]): DateSpan { + let dateSpan0 = hit0.dateSpan + let dateSpan1 = hit1.dateSpan + let ms = [ + dateSpan0.range.start, + dateSpan0.range.end, + dateSpan1.range.start, + dateSpan1.range.end, + ] + + ms.sort(compareNumbers) + + let props = {} as DateSpan + + for (let transformer of dateSelectionTransformers) { + let res = transformer(hit0, hit1) + + if (res === false) { + return null + } + + if (res) { + Object.assign(props, res) + } + } + + props.range = { start: ms[0], end: ms[3] } + props.allDay = dateSpan0.allDay + + return props +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/EventDragging.ts b/fullcalendar-main/packages/interaction/src/interactions/EventDragging.ts new file mode 100644 index 0000000..dc079bd --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/EventDragging.ts @@ -0,0 +1,492 @@ +import { + EventApi, + ViewApi, + EventChangeArg, + EventAddArg, + EventRemoveArg, + EventRenderRange, +} from '@fullcalendar/core' +import { + DateComponent, Seg, + PointerDragEvent, Hit, + EventMutation, applyMutationToEventStore, + startOfDay, + elementClosest, + EventStore, getRelevantEvents, createEmptyEventStore, + EventInteractionState, + diffDates, enableCursor, disableCursor, + getElSeg, + eventDragMutationMassager, + Interaction, InteractionSettings, interactionSettingsStore, + EventDropTransformers, + CalendarContext, + buildEventApis, + isInteractionValid, + EventImpl, +} from '@fullcalendar/core/internal' +import { HitDragging, isHitsEqual } from './HitDragging.js' +import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging.js' +import { buildDatePointApiWithContext } from '../utils.js' + +export type EventDragStopArg = EventDragArg +export type EventDragStartArg = EventDragArg + +export interface EventDragArg { + el: HTMLElement + event: EventApi + jsEvent: MouseEvent + view: ViewApi +} + +export class EventDragging extends Interaction { // TODO: rename to EventSelectingAndDragging + // TODO: test this in IE11 + // QUESTION: why do we need it on the resizable??? + static SELECTOR = '.fc-event-draggable, .fc-event-resizable' + + dragging: FeaturefulElementDragging + hitDragging: HitDragging + + // internal state + subjectEl: HTMLElement | null = null + subjectSeg: Seg | null = null // the seg being selected/dragged + isDragging: boolean = false + eventRange: EventRenderRange | null = null + relevantEvents: EventStore | null = null // the events being dragged + receivingContext: CalendarContext | null = null + validMutation: EventMutation | null = null + mutatedRelevantEvents: EventStore | null = null + + constructor(settings: InteractionSettings) { + super(settings) + let { component } = this + let { options } = component.context + + let dragging = this.dragging = new FeaturefulElementDragging(settings.el) + dragging.pointer.selector = EventDragging.SELECTOR + dragging.touchScrollAllowed = false + dragging.autoScroller.isEnabled = options.dragScroll + + let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsStore) + hitDragging.useSubjectCenter = settings.useEventCenter + hitDragging.emitter.on('pointerdown', this.handlePointerDown) + hitDragging.emitter.on('dragstart', this.handleDragStart) + hitDragging.emitter.on('hitupdate', this.handleHitUpdate) + hitDragging.emitter.on('pointerup', this.handlePointerUp) + hitDragging.emitter.on('dragend', this.handleDragEnd) + } + + destroy() { + this.dragging.destroy() + } + + handlePointerDown = (ev: PointerDragEvent) => { + let origTarget = ev.origEvent.target as HTMLElement + let { component, dragging } = this + let { mirror } = dragging + let { options } = component.context + let initialContext = component.context + this.subjectEl = ev.subjectEl as HTMLElement + let subjectSeg = this.subjectSeg = getElSeg(ev.subjectEl as HTMLElement)! + let eventRange = this.eventRange = subjectSeg.eventRange! + let eventInstanceId = eventRange.instance!.instanceId + + this.relevantEvents = getRelevantEvents( + initialContext.getCurrentData().eventStore, + eventInstanceId, + ) + + dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance + dragging.delay = + // only do a touch delay if touch and this event hasn't been selected yet + (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? + getComponentTouchDelay(component) : + null + + if (options.fixedMirrorParent) { + mirror.parentNode = options.fixedMirrorParent + } else { + mirror.parentNode = elementClosest(origTarget, '.fc') + } + + mirror.revertDuration = options.dragRevertDuration + + let isValid = + component.isValidSegDownEl(origTarget) && + !elementClosest(origTarget, '.fc-event-resizer') // NOT on a resizer + + dragging.setIgnoreMove(!isValid) + + // disable dragging for elements that are resizable (ie, selectable) + // but are not draggable + this.isDragging = isValid && + (ev.subjectEl as HTMLElement).classList.contains('fc-event-draggable') + } + + handleDragStart = (ev: PointerDragEvent) => { + let initialContext = this.component.context + let eventRange = this.eventRange! + let eventInstanceId = eventRange.instance.instanceId + + if (ev.isTouch) { + // need to select a different event? + if (eventInstanceId !== this.component.props.eventSelection) { + initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId }) + } + } else { + // if now using mouse, but was previous touch interaction, clear selected event + initialContext.dispatch({ type: 'UNSELECT_EVENT' }) + } + + if (this.isDragging) { + initialContext.calendarApi.unselect(ev) // unselect *date* selection + initialContext.emitter.trigger('eventDragStart', { + el: this.subjectEl, + event: new EventImpl(initialContext, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: initialContext.viewApi, + } as EventDragStartArg) + } + } + + handleHitUpdate = (hit: Hit | null, isFinal: boolean) => { + if (!this.isDragging) { + return + } + + let relevantEvents = this.relevantEvents! + let initialHit = this.hitDragging.initialHit! + let initialContext = this.component.context + + // states based on new hit + let receivingContext: CalendarContext | null = null + let mutation: EventMutation | null = null + let mutatedRelevantEvents: EventStore | null = null + let isInvalid = false + let interaction: EventInteractionState = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + } + + if (hit) { + receivingContext = hit.context + let receivingOptions = receivingContext.options + + if ( + initialContext === receivingContext || + (receivingOptions.editable && receivingOptions.droppable) + ) { + mutation = computeEventMutation( + initialHit, + hit, + receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers, + ) + + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore( + relevantEvents, + receivingContext.getCurrentData().eventUiBases, + mutation, + receivingContext, + ) + interaction.mutatedEvents = mutatedRelevantEvents + + if (!isInteractionValid(interaction, hit.dateProfile, receivingContext)) { + isInvalid = true + mutation = null + mutatedRelevantEvents = null + interaction.mutatedEvents = createEmptyEventStore() + } + } + } else { + receivingContext = null + } + } + + this.displayDrag(receivingContext, interaction) + + if (!isInvalid) { + enableCursor() + } else { + disableCursor() + } + + if (!isFinal) { + if ( + initialContext === receivingContext && // TODO: write test for this + isHitsEqual(initialHit, hit) + ) { + mutation = null + } + + this.dragging.setMirrorNeedsRevert(!mutation) + + // render the mirror if no already-rendered mirror + // TODO: wish we could somehow wait for dispatch to guarantee render + this.dragging.setMirrorIsVisible( + !hit || !(this.subjectEl.getRootNode() as ParentNode).querySelector('.fc-event-mirror'), // TODO: turn className into constant + ) + + // assign states based on new hit + this.receivingContext = receivingContext + this.validMutation = mutation + this.mutatedRelevantEvents = mutatedRelevantEvents + } + } + + handlePointerUp = () => { + if (!this.isDragging) { + this.cleanup() // because handleDragEnd won't fire + } + } + + handleDragEnd = (ev: PointerDragEvent) => { + if (this.isDragging) { + let initialContext = this.component.context + let initialView = initialContext.viewApi + let { receivingContext, validMutation } = this + let eventDef = this.eventRange!.def + let eventInstance = this.eventRange!.instance + let eventApi = new EventImpl(initialContext, eventDef, eventInstance) + let relevantEvents = this.relevantEvents! + let mutatedRelevantEvents = this.mutatedRelevantEvents! + let { finalHit } = this.hitDragging + + this.clearDrag() // must happen after revert animation + + initialContext.emitter.trigger('eventDragStop', { + el: this.subjectEl, + event: eventApi, + jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: initialView, + } as EventDragStopArg) + + if (validMutation) { + // dropped within same calendar + if (receivingContext === initialContext) { + let updatedEventApi = new EventImpl( + initialContext, + mutatedRelevantEvents.defs[eventDef.defId], + eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null, + ) + + initialContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents, + }) + + let eventChangeArg: EventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents, initialContext, eventInstance), + revert() { + initialContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, // the pre-change data + }) + }, + } + + let transformed: ReturnType<EventDropTransformers> = {} + for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) { + Object.assign(transformed, transformer(validMutation, initialContext)) + } + + initialContext.emitter.trigger('eventDrop', { + ...eventChangeArg, + ...transformed, + el: ev.subjectEl as HTMLElement, + delta: validMutation.datesDelta!, + jsEvent: ev.origEvent as MouseEvent, // bad + view: initialView, + }) + + initialContext.emitter.trigger('eventChange', eventChangeArg) + + // dropped in different calendar + } else if (receivingContext) { + let eventRemoveArg: EventRemoveArg = { + event: eventApi, + relatedEvents: buildEventApis(relevantEvents, initialContext, eventInstance), + revert() { + initialContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, + }) + }, + } + + initialContext.emitter.trigger('eventLeave', { + ...eventRemoveArg, + draggedEl: ev.subjectEl as HTMLElement, + view: initialView, + }) + + initialContext.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: relevantEvents, + }) + + initialContext.emitter.trigger('eventRemove', eventRemoveArg) + + let addedEventDef = mutatedRelevantEvents.defs[eventDef.defId] + let addedEventInstance = mutatedRelevantEvents.instances[eventInstance.instanceId] + let addedEventApi = new EventImpl(receivingContext, addedEventDef, addedEventInstance) + + receivingContext.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents, + }) + + let eventAddArg: EventAddArg = { + event: addedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents, receivingContext, addedEventInstance), + revert() { + receivingContext.dispatch({ + type: 'REMOVE_EVENTS', + eventStore: mutatedRelevantEvents, + }) + }, + } + + receivingContext.emitter.trigger('eventAdd', eventAddArg) + + if (ev.isTouch) { + receivingContext.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: eventInstance.instanceId, + }) + } + + receivingContext.emitter.trigger('drop', { + ...buildDatePointApiWithContext(finalHit.dateSpan, receivingContext), + draggedEl: ev.subjectEl as HTMLElement, + jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: finalHit.context.viewApi, + }) + + receivingContext.emitter.trigger('eventReceive', { + ...eventAddArg, + draggedEl: ev.subjectEl as HTMLElement, + view: finalHit.context.viewApi, + }) + } + } else { + initialContext.emitter.trigger('_noEventDrop') + } + } + + this.cleanup() + } + + // render a drag state on the next receivingCalendar + displayDrag(nextContext: CalendarContext | null, state: EventInteractionState) { + let initialContext = this.component.context + let prevContext = this.receivingContext + + // does the previous calendar need to be cleared? + if (prevContext && prevContext !== nextContext) { + // does the initial calendar need to be cleared? + // if so, don't clear all the way. we still need to to hide the affectedEvents + if (prevContext === initialContext) { + prevContext.dispatch({ + type: 'SET_EVENT_DRAG', + state: { + affectedEvents: state.affectedEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + }, + }) + + // completely clear the old calendar if it wasn't the initial + } else { + prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' }) + } + } + + if (nextContext) { + nextContext.dispatch({ type: 'SET_EVENT_DRAG', state }) + } + } + + clearDrag() { + let initialCalendar = this.component.context + let { receivingContext } = this + + if (receivingContext) { + receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' }) + } + + // the initial calendar might have an dummy drag state from displayDrag + if (initialCalendar !== receivingContext) { + initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }) + } + } + + cleanup() { // reset all internal state + this.subjectSeg = null + this.isDragging = false + this.eventRange = null + this.relevantEvents = null + this.receivingContext = null + this.validMutation = null + this.mutatedRelevantEvents = null + } +} + +function computeEventMutation( + hit0: Hit, + hit1: Hit, + massagers: eventDragMutationMassager[], +): EventMutation { + let dateSpan0 = hit0.dateSpan + let dateSpan1 = hit1.dateSpan + let date0 = dateSpan0.range.start + let date1 = dateSpan1.range.start + let standardProps = {} as any + + if (dateSpan0.allDay !== dateSpan1.allDay) { + standardProps.allDay = dateSpan1.allDay + standardProps.hasEnd = hit1.context.options.allDayMaintainDuration + + if (dateSpan1.allDay) { + // means date1 is already start-of-day, + // but date0 needs to be converted + date0 = startOfDay(date0) + } + } + + let delta = diffDates( + date0, date1, + hit0.context.dateEnv, + hit0.componentId === hit1.componentId ? + hit0.largeUnit : + null, + ) + + if (delta.milliseconds) { // has hours/minutes/seconds + standardProps.allDay = false + } + + let mutation: EventMutation = { + datesDelta: delta, + standardProps, + } + + for (let massager of massagers) { + massager(mutation, hit0, hit1) + } + + return mutation +} + +function getComponentTouchDelay(component: DateComponent<any>): number | null { + let { options } = component.context + let delay = options.eventLongPressDelay + + if (delay == null) { + delay = options.longPressDelay + } + + return delay +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/EventResizing.ts b/fullcalendar-main/packages/interaction/src/interactions/EventResizing.ts new file mode 100644 index 0000000..3682b63 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/EventResizing.ts @@ -0,0 +1,269 @@ +import { + ViewApi, + EventApi, + EventChangeArg, + EventRenderRange, + Duration, +} from '@fullcalendar/core' +import { + Seg, Hit, + EventMutation, applyMutationToEventStore, + elementClosest, + PointerDragEvent, + EventStore, getRelevantEvents, createEmptyEventStore, + diffDates, enableCursor, disableCursor, + DateRange, + getElSeg, + createDuration, + EventInteractionState, + Interaction, InteractionSettings, interactionSettingsToStore, buildEventApis, isInteractionValid, + EventImpl, +} from '@fullcalendar/core/internal' +import { HitDragging, isHitsEqual } from './HitDragging.js' +import { FeaturefulElementDragging } from '../dnd/FeaturefulElementDragging.js' + +export type EventResizeStartArg = EventResizeStartStopArg +export type EventResizeStopArg = EventResizeStartStopArg + +export interface EventResizeStartStopArg { + el: HTMLElement + event: EventApi + jsEvent: MouseEvent + view: ViewApi +} + +export interface EventResizeDoneArg extends EventChangeArg { + el: HTMLElement + startDelta: Duration + endDelta: Duration + jsEvent: MouseEvent + view: ViewApi +} + +export class EventResizing extends Interaction { + dragging: FeaturefulElementDragging + hitDragging: HitDragging + + // internal state + draggingSegEl: HTMLElement | null = null + draggingSeg: Seg | null = null // TODO: rename to resizingSeg? subjectSeg? + eventRange: EventRenderRange | null = null + relevantEvents: EventStore | null = null + validMutation: EventMutation | null = null + mutatedRelevantEvents: EventStore | null = null + + constructor(settings: InteractionSettings) { + super(settings) + let { component } = settings + + let dragging = this.dragging = new FeaturefulElementDragging(settings.el) + dragging.pointer.selector = '.fc-event-resizer' + dragging.touchScrollAllowed = false + dragging.autoScroller.isEnabled = component.context.options.dragScroll + + let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings)) + hitDragging.emitter.on('pointerdown', this.handlePointerDown) + hitDragging.emitter.on('dragstart', this.handleDragStart) + hitDragging.emitter.on('hitupdate', this.handleHitUpdate) + hitDragging.emitter.on('dragend', this.handleDragEnd) + } + + destroy() { + this.dragging.destroy() + } + + handlePointerDown = (ev: PointerDragEvent) => { + let { component } = this + let segEl = this.querySegEl(ev) + let seg = getElSeg(segEl) + let eventRange = this.eventRange = seg.eventRange! + + this.dragging.minDistance = component.context.options.eventDragMinDistance + + // if touch, need to be working with a selected event + this.dragging.setIgnoreMove( + !this.component.isValidSegDownEl(ev.origEvent.target as HTMLElement) || + (ev.isTouch && this.component.props.eventSelection !== eventRange.instance!.instanceId), + ) + } + + handleDragStart = (ev: PointerDragEvent) => { + let { context } = this.component + let eventRange = this.eventRange! + + this.relevantEvents = getRelevantEvents( + context.getCurrentData().eventStore, + this.eventRange.instance!.instanceId, + ) + + let segEl = this.querySegEl(ev) + this.draggingSegEl = segEl + this.draggingSeg = getElSeg(segEl) + + context.calendarApi.unselect() + context.emitter.trigger('eventResizeStart', { + el: segEl, + event: new EventImpl(context, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: context.viewApi, + } as EventResizeStartArg) + } + + handleHitUpdate = (hit: Hit | null, isFinal: boolean, ev: PointerDragEvent) => { + let { context } = this.component + let relevantEvents = this.relevantEvents! + let initialHit = this.hitDragging.initialHit! + let eventInstance = this.eventRange.instance! + let mutation: EventMutation | null = null + let mutatedRelevantEvents: EventStore | null = null + let isInvalid = false + let interaction: EventInteractionState = { + affectedEvents: relevantEvents, + mutatedEvents: createEmptyEventStore(), + isEvent: true, + } + + if (hit) { + let disallowed = hit.componentId === initialHit.componentId + && this.isHitComboAllowed + && !this.isHitComboAllowed(initialHit, hit) + + if (!disallowed) { + mutation = computeMutation( + initialHit, + hit, + (ev.subjectEl as HTMLElement).classList.contains('fc-event-resizer-start'), + eventInstance.range, + ) + } + } + + if (mutation) { + mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context) + interaction.mutatedEvents = mutatedRelevantEvents + + if (!isInteractionValid(interaction, hit.dateProfile, context)) { + isInvalid = true + mutation = null + mutatedRelevantEvents = null + interaction.mutatedEvents = null + } + } + + if (mutatedRelevantEvents) { + context.dispatch({ + type: 'SET_EVENT_RESIZE', + state: interaction, + }) + } else { + context.dispatch({ type: 'UNSET_EVENT_RESIZE' }) + } + + if (!isInvalid) { + enableCursor() + } else { + disableCursor() + } + + if (!isFinal) { + if (mutation && isHitsEqual(initialHit, hit)) { + mutation = null + } + + this.validMutation = mutation + this.mutatedRelevantEvents = mutatedRelevantEvents + } + } + + handleDragEnd = (ev: PointerDragEvent) => { + let { context } = this.component + let eventDef = this.eventRange!.def + let eventInstance = this.eventRange!.instance + let eventApi = new EventImpl(context, eventDef, eventInstance) + let relevantEvents = this.relevantEvents! + let mutatedRelevantEvents = this.mutatedRelevantEvents! + + context.emitter.trigger('eventResizeStop', { + el: this.draggingSegEl, + event: eventApi, + jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655 + view: context.viewApi, + } as EventResizeStopArg) + + if (this.validMutation) { + let updatedEventApi = new EventImpl( + context, + mutatedRelevantEvents.defs[eventDef.defId], + eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null, + ) + + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents, + }) + + let eventChangeArg: EventChangeArg = { + oldEvent: eventApi, + event: updatedEventApi, + relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance), + revert() { + context.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents, // the pre-change events + }) + }, + } + + context.emitter.trigger('eventResize', { + ...eventChangeArg, + el: this.draggingSegEl, + startDelta: this.validMutation.startDelta || createDuration(0), + endDelta: this.validMutation.endDelta || createDuration(0), + jsEvent: ev.origEvent as MouseEvent, + view: context.viewApi, + }) + + context.emitter.trigger('eventChange', eventChangeArg) + } else { + context.emitter.trigger('_noEventResize') + } + + // reset all internal state + this.draggingSeg = null + this.relevantEvents = null + this.validMutation = null + + // okay to keep eventInstance around. useful to set it in handlePointerDown + } + + querySegEl(ev: PointerDragEvent) { + return elementClosest(ev.subjectEl as HTMLElement, '.fc-event') + } +} + +function computeMutation( + hit0: Hit, + hit1: Hit, + isFromStart: boolean, + instanceRange: DateRange, +): EventMutation | null { + let dateEnv = hit0.context.dateEnv + let date0 = hit0.dateSpan.range.start + let date1 = hit1.dateSpan.range.start + + let delta = diffDates( + date0, date1, + dateEnv, + hit0.largeUnit, + ) + + if (isFromStart) { + if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { + return { startDelta: delta } + } + } else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { + return { endDelta: delta } + } + + return null +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/HitDragging.ts b/fullcalendar-main/packages/interaction/src/interactions/HitDragging.ts new file mode 100644 index 0000000..6a7660a --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/HitDragging.ts @@ -0,0 +1,220 @@ +import { + Emitter, PointerDragEvent, + isDateSpansEqual, + computeRect, + constrainPoint, intersectRects, getRectCenter, diffPoints, Point, + rangeContainsRange, + Hit, + InteractionSettingsStore, + mapHash, + ElementDragging, +} from '@fullcalendar/core/internal' +import { OffsetTracker } from '../OffsetTracker.js' + +/* +Tracks movement over multiple droppable areas (aka "hits") +that exist in one or more DateComponents. +Relies on an existing draggable. + +emits: +- pointerdown +- dragstart +- hitchange - fires initially, even if not over a hit +- pointerup +- (hitchange - again, to null, if ended over a hit) +- dragend +*/ +export class HitDragging { + droppableStore: InteractionSettingsStore + dragging: ElementDragging + emitter: Emitter<any> + + // options that can be set by caller + useSubjectCenter: boolean = false + requireInitial: boolean = true // if doesn't start out on a hit, won't emit any events + + // internal state + offsetTrackers: { [componentUid: string]: OffsetTracker } + initialHit: Hit | null = null + movingHit: Hit | null = null + finalHit: Hit | null = null // won't ever be populated if shouldIgnoreMove + coordAdjust?: Point + + constructor(dragging: ElementDragging, droppableStore: InteractionSettingsStore) { + this.droppableStore = droppableStore + + dragging.emitter.on('pointerdown', this.handlePointerDown) + dragging.emitter.on('dragstart', this.handleDragStart) + dragging.emitter.on('dragmove', this.handleDragMove) + dragging.emitter.on('pointerup', this.handlePointerUp) + dragging.emitter.on('dragend', this.handleDragEnd) + + this.dragging = dragging + this.emitter = new Emitter() + } + + handlePointerDown = (ev: PointerDragEvent) => { + let { dragging } = this + + this.initialHit = null + this.movingHit = null + this.finalHit = null + + this.prepareHits() + this.processFirstCoord(ev) + + if (this.initialHit || !this.requireInitial) { + dragging.setIgnoreMove(false) + + // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( + this.emitter.trigger('pointerdown', ev) + } else { + dragging.setIgnoreMove(true) + } + } + + // sets initialHit + // sets coordAdjust + processFirstCoord(ev: PointerDragEvent) { + let origPoint = { left: ev.pageX, top: ev.pageY } + let adjustedPoint = origPoint + let subjectEl = ev.subjectEl + let subjectRect + + if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot + subjectRect = computeRect(subjectEl) + adjustedPoint = constrainPoint(adjustedPoint, subjectRect) + } + + let initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top) + if (initialHit) { + if (this.useSubjectCenter && subjectRect) { + let slicedSubjectRect = intersectRects(subjectRect, initialHit.rect) + if (slicedSubjectRect) { + adjustedPoint = getRectCenter(slicedSubjectRect) + } + } + + this.coordAdjust = diffPoints(adjustedPoint, origPoint) + } else { + this.coordAdjust = { left: 0, top: 0 } + } + } + + handleDragStart = (ev: PointerDragEvent) => { + this.emitter.trigger('dragstart', ev) + this.handleMove(ev, true) // force = fire even if initially null + } + + handleDragMove = (ev: PointerDragEvent) => { + this.emitter.trigger('dragmove', ev) + this.handleMove(ev) + } + + handlePointerUp = (ev: PointerDragEvent) => { + this.releaseHits() + this.emitter.trigger('pointerup', ev) + } + + handleDragEnd = (ev: PointerDragEvent) => { + if (this.movingHit) { + this.emitter.trigger('hitupdate', null, true, ev) + } + + this.finalHit = this.movingHit + this.movingHit = null + this.emitter.trigger('dragend', ev) + } + + handleMove(ev: PointerDragEvent, forceHandle?: boolean) { + let hit = this.queryHitForOffset( + ev.pageX + this.coordAdjust!.left, + ev.pageY + this.coordAdjust!.top, + ) + + if (forceHandle || !isHitsEqual(this.movingHit, hit)) { + this.movingHit = hit + this.emitter.trigger('hitupdate', hit, false, ev) + } + } + + prepareHits() { + this.offsetTrackers = mapHash(this.droppableStore, (interactionSettings) => { + interactionSettings.component.prepareHits() + return new OffsetTracker(interactionSettings.el) + }) + } + + releaseHits() { + let { offsetTrackers } = this + + for (let id in offsetTrackers) { + offsetTrackers[id].destroy() + } + + this.offsetTrackers = {} + } + + queryHitForOffset(offsetLeft: number, offsetTop: number): Hit | null { + let { droppableStore, offsetTrackers } = this + let bestHit: Hit | null = null + + for (let id in droppableStore) { + let component = droppableStore[id].component + let offsetTracker = offsetTrackers[id] + + if ( + offsetTracker && // wasn't destroyed mid-drag + offsetTracker.isWithinClipping(offsetLeft, offsetTop) + ) { + let originLeft = offsetTracker.computeLeft() + let originTop = offsetTracker.computeTop() + let positionLeft = offsetLeft - originLeft + let positionTop = offsetTop - originTop + let { origRect } = offsetTracker + let width = origRect.right - origRect.left + let height = origRect.bottom - origRect.top + + if ( + // must be within the element's bounds + positionLeft >= 0 && positionLeft < width && + positionTop >= 0 && positionTop < height + ) { + let hit = component.queryHit(positionLeft, positionTop, width, height) + if ( + hit && ( + // make sure the hit is within activeRange, meaning it's not a dead cell + rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range) + ) && + (!bestHit || hit.layer > bestHit.layer) + ) { + hit.componentId = id + hit.context = component.context + + // TODO: better way to re-orient rectangle + hit.rect.left += originLeft + hit.rect.right += originLeft + hit.rect.top += originTop + hit.rect.bottom += originTop + + bestHit = hit + } + } + } + } + + return bestHit + } +} + +export function isHitsEqual(hit0: Hit | null, hit1: Hit | null): boolean { + if (!hit0 && !hit1) { + return true + } + + if (Boolean(hit0) !== Boolean(hit1)) { + return false + } + + return isDateSpansEqual(hit0!.dateSpan, hit1!.dateSpan) +} diff --git a/fullcalendar-main/packages/interaction/src/interactions/UnselectAuto.ts b/fullcalendar-main/packages/interaction/src/interactions/UnselectAuto.ts new file mode 100644 index 0000000..1313200 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/interactions/UnselectAuto.ts @@ -0,0 +1,77 @@ +import { DateSelectionApi } from '@fullcalendar/core' +import { + PointerDragEvent, + elementClosest, + CalendarContext, + getEventTargetViaRoot, +} from '@fullcalendar/core/internal' +import { PointerDragging } from '../dnd/PointerDragging.js' +import { EventDragging } from './EventDragging.js' + +export class UnselectAuto { + documentPointer: PointerDragging // for unfocusing + isRecentPointerDateSelect = false // wish we could use a selector to detect date selection, but uses hit system + matchesCancel = false + matchesEvent = false + + constructor(private context: CalendarContext) { + let documentPointer = this.documentPointer = new PointerDragging(document) + documentPointer.shouldIgnoreMove = true + documentPointer.shouldWatchScroll = false + documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown) + documentPointer.emitter.on('pointerup', this.onDocumentPointerUp) + + /* + TODO: better way to know about whether there was a selection with the pointer + */ + context.emitter.on('select', this.onSelect) + } + + destroy() { + this.context.emitter.off('select', this.onSelect) + this.documentPointer.destroy() + } + + onSelect = (selectInfo: DateSelectionApi) => { + if (selectInfo.jsEvent) { + this.isRecentPointerDateSelect = true + } + } + + onDocumentPointerDown = (pev: PointerDragEvent) => { + let unselectCancel = this.context.options.unselectCancel + let downEl = getEventTargetViaRoot(pev.origEvent) as HTMLElement + + this.matchesCancel = !!elementClosest(downEl, unselectCancel) + this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR) // interaction started on an event? + } + + onDocumentPointerUp = (pev: PointerDragEvent) => { + let { context } = this + let { documentPointer } = this + let calendarState = context.getCurrentData() + + // touch-scrolling should never unfocus any type of selection + if (!documentPointer.wasTouchScroll) { + if ( + calendarState.dateSelection && // an existing date selection? + !this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? + ) { + let unselectAuto = context.options.unselectAuto + + if (unselectAuto && (!unselectAuto || !this.matchesCancel)) { + context.calendarApi.unselect(pev) + } + } + + if ( + calendarState.eventSelection && // an existing event selected? + !this.matchesEvent // interaction DIDN'T start on an event + ) { + context.dispatch({ type: 'UNSELECT_EVENT' }) + } + } + + this.isRecentPointerDateSelect = false + } +} diff --git a/fullcalendar-main/packages/interaction/src/option-refiners.ts b/fullcalendar-main/packages/interaction/src/option-refiners.ts new file mode 100644 index 0000000..b21602d --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/option-refiners.ts @@ -0,0 +1,25 @@ +import { EventDropArg } from '@fullcalendar/core' +import { identity, Identity } from '@fullcalendar/core/internal' +import { + DateClickArg, + EventDragStartArg, EventDragStopArg, + EventResizeStartArg, EventResizeStopArg, EventResizeDoneArg, + DropArg, EventReceiveArg, EventLeaveArg, +} from './public-types.js' + +export const OPTION_REFINERS = { + fixedMirrorParent: identity as Identity<HTMLElement>, +} + +export const LISTENER_REFINERS = { + dateClick: identity as Identity<(arg: DateClickArg) => void>, + eventDragStart: identity as Identity<(arg: EventDragStartArg) => void>, + eventDragStop: identity as Identity<(arg: EventDragStopArg) => void>, + eventDrop: identity as Identity<(arg: EventDropArg) => void>, + eventResizeStart: identity as Identity<(arg: EventResizeStartArg) => void>, + eventResizeStop: identity as Identity<(arg: EventResizeStopArg) => void>, + eventResize: identity as Identity<(arg: EventResizeDoneArg) => void>, + drop: identity as Identity<(arg: DropArg) => void>, + eventReceive: identity as Identity<(arg: EventReceiveArg) => void>, + eventLeave: identity as Identity<(arg: EventLeaveArg) => void>, +} diff --git a/fullcalendar-main/packages/interaction/src/public-types.ts b/fullcalendar-main/packages/interaction/src/public-types.ts new file mode 100644 index 0000000..3f910c8 --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/public-types.ts @@ -0,0 +1,5 @@ + +export { DateClickArg } from './interactions/DateClicking.js' +export { EventDragStartArg, EventDragStopArg } from './interactions/EventDragging.js' +export { EventResizeStartArg, EventResizeStopArg, EventResizeDoneArg } from './interactions/EventResizing.js' +export { DropArg, EventReceiveArg, EventLeaveArg } from './utils.js' diff --git a/fullcalendar-main/packages/interaction/src/utils.ts b/fullcalendar-main/packages/interaction/src/utils.ts new file mode 100644 index 0000000..fead3ec --- /dev/null +++ b/fullcalendar-main/packages/interaction/src/utils.ts @@ -0,0 +1,38 @@ +import { ViewApi, EventApi, DatePointApi } from '@fullcalendar/core' +import { DateSpan, CalendarContext, DateEnv } from '@fullcalendar/core/internal' + +export interface DropArg extends DatePointApi { + draggedEl: HTMLElement + jsEvent: MouseEvent + view: ViewApi +} + +export type EventReceiveArg = EventReceiveLeaveArg +export type EventLeaveArg = EventReceiveLeaveArg +export interface EventReceiveLeaveArg { // will this become public? + draggedEl: HTMLElement + event: EventApi + relatedEvents: EventApi[] + revert: () => void + view: ViewApi +} + +export function buildDatePointApiWithContext(dateSpan: DateSpan, context: CalendarContext) { + let props = {} as DatePointApi + + for (let transform of context.pluginHooks.datePointTransforms) { + Object.assign(props, transform(dateSpan, context)) + } + + Object.assign(props, buildDatePointApi(dateSpan, context.dateEnv)) + + return props +} + +export function buildDatePointApi(span: DateSpan, dateEnv: DateEnv): DatePointApi { + return { + date: dateEnv.toDate(span.range.start), + dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }), + allDay: span.allDay, + } +} diff --git a/fullcalendar-main/packages/list/.eslintrc.cjs b/fullcalendar-main/packages/list/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/list/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/list/README.md b/fullcalendar-main/packages/list/README.md new file mode 100644 index 0000000..8fc2d9d --- /dev/null +++ b/fullcalendar-main/packages/list/README.md @@ -0,0 +1,32 @@ + +# FullCalendar List View Plugin + +Display events on a calendar view that looks like a bulleted list + +## Installation + +Install the necessary packages: + +```sh +npm install @fullcalendar/core @fullcalendar/list +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import listPlugin from '@fullcalendar/list' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [listPlugin], + initialView: 'listWeek', + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/list/package.json b/fullcalendar-main/packages/list/package.json new file mode 100644 index 0000000..442ec55 --- /dev/null +++ b/fullcalendar-main/packages/list/package.json @@ -0,0 +1,49 @@ +{ + "name": "@fullcalendar/list", + "version": "6.1.11", + "title": "FullCalendar List View Plugin", + "description": "Display events on a calendar view that looks like a bulleted list", + "keywords": [ + "list-view" + ], + "homepage": "https://fullcalendar.io/docs/list-view", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./internal": {} + }, + "iifeGlobals": { + ".": "FullCalendar.List", + "./internal": "FullCalendar.List.Internal" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/list/src/ListView.tsx b/fullcalendar-main/packages/list/src/ListView.tsx new file mode 100644 index 0000000..e488e1e --- /dev/null +++ b/fullcalendar-main/packages/list/src/ListView.tsx @@ -0,0 +1,294 @@ +import { ViewApi, EventRenderRange } from '@fullcalendar/core' +import { + ViewProps, + Scroller, + DateMarker, + addDays, + startOfDay, + DateRange, + intersectRanges, + DateProfile, + EventUiHash, + sliceEventStore, + EventStore, + memoize, + Seg, + sortEventSegs, + getSegMeta, + NowTimer, + ViewContainer, + DateComponent, + MountArg, + getUniqueDomId, + formatDayString, + ContentContainer, +} from '@fullcalendar/core/internal' +import { + ComponentChild, + createElement, + VNode, +} from '@fullcalendar/core/preact' +import { ListViewHeaderRow } from './ListViewHeaderRow.js' +import { ListViewEventRow } from './ListViewEventRow.js' + +export interface NoEventsContentArg { + text: string + view: ViewApi +} + +export type NoEventsMountArg = MountArg<NoEventsContentArg> + +/* +Responsible for the scroller, and forwarding event-related actions into the "grid". +*/ +export class ListView extends DateComponent<ViewProps> { + private computeDateVars = memoize(computeDateVars) + private eventStoreToSegs = memoize(this._eventStoreToSegs) + state = { + timeHeaderId: getUniqueDomId(), + eventHeaderId: getUniqueDomId(), + dateHeaderIdRoot: getUniqueDomId(), + } + + render() { + let { props, context } = this + let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile) + let eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges) + + return ( + <ViewContainer + elRef={this.setRootEl} + elClasses={[ + 'fc-list', + context.theme.getClass('table'), // just for the outer border. will be on div + context.options.stickyHeaderDates !== false ? + 'fc-list-sticky' : + '', + ]} + viewSpec={context.viewSpec} + > + <Scroller + liquid={!props.isHeightAuto} + overflowX={props.isHeightAuto ? 'visible' : 'hidden'} + overflowY={props.isHeightAuto ? 'visible' : 'auto'} + > + {eventSegs.length > 0 ? + this.renderSegList(eventSegs, dayDates) : + this.renderEmptyMessage()} + </Scroller> + </ViewContainer> + ) + } + + setRootEl = (rootEl: HTMLElement | null) => { + if (rootEl) { + this.context.registerInteractiveComponent(this, { // TODO: make aware that it doesn't do Hits + el: rootEl, + }) + } else { + this.context.unregisterInteractiveComponent(this) + } + } + + renderEmptyMessage() { + let { options, viewApi } = this.context + let renderProps: NoEventsContentArg = { + text: options.noEventsText, + view: viewApi, + } + + return ( + <ContentContainer + elTag="div" + elClasses={['fc-list-empty']} + renderProps={renderProps} + generatorName="noEventsContent" + customGenerator={options.noEventsContent} + defaultGenerator={renderNoEventsInner} + classNameGenerator={options.noEventsClassNames} + didMount={options.noEventsDidMount} + willUnmount={options.noEventsWillUnmount} + > + {(InnerContent) => ( + <InnerContent + elTag="div" + elClasses={['fc-list-empty-cushion']} + /> + )} + </ContentContainer> + ) + } + + renderSegList(allSegs: Seg[], dayDates: DateMarker[]) { + let { theme, options } = this.context + let { timeHeaderId, eventHeaderId, dateHeaderIdRoot } = this.state + let segsByDay = groupSegsByDay(allSegs) // sparse array + + return ( + <NowTimer unit="day"> + {(nowDate: DateMarker, todayRange: DateRange) => { + let innerNodes: VNode[] = [] + + for (let dayIndex = 0; dayIndex < segsByDay.length; dayIndex += 1) { + let daySegs = segsByDay[dayIndex] + + if (daySegs) { // sparse array, so might be undefined + let dayStr = formatDayString(dayDates[dayIndex]) + let dateHeaderId = dateHeaderIdRoot + '-' + dayStr + + // append a day header + innerNodes.push( + <ListViewHeaderRow + key={dayStr} + cellId={dateHeaderId} + dayDate={dayDates[dayIndex]} + todayRange={todayRange} + />, + ) + + daySegs = sortEventSegs(daySegs, options.eventOrder) + + for (let seg of daySegs) { + innerNodes.push( + <ListViewEventRow + key={dayStr + ':' + seg.eventRange.instance.instanceId /* are multiple segs for an instanceId */} + seg={seg} + isDragging={false} + isResizing={false} + isDateSelecting={false} + isSelected={false} + timeHeaderId={timeHeaderId} + eventHeaderId={eventHeaderId} + dateHeaderId={dateHeaderId} + {...getSegMeta(seg, todayRange, nowDate)} + />, + ) + } + } + } + + return ( + <table className={'fc-list-table ' + theme.getClass('table')}> + <thead> + <tr> + <th scope="col" id={timeHeaderId}>{options.timeHint}</th> + <th scope="col" aria-hidden /> + <th scope="col" id={eventHeaderId}>{options.eventHint}</th> + </tr> + </thead> + <tbody>{innerNodes}</tbody> + </table> + ) + }} + </NowTimer> + ) + } + + _eventStoreToSegs(eventStore: EventStore, eventUiBases: EventUiHash, dayRanges: DateRange[]): Seg[] { + return this.eventRangesToSegs( + sliceEventStore( + eventStore, + eventUiBases, + this.props.dateProfile.activeRange, + this.context.options.nextDayThreshold, + ).fg, + dayRanges, + ) + } + + eventRangesToSegs(eventRanges: EventRenderRange[], dayRanges: DateRange[]) { + let segs = [] + + for (let eventRange of eventRanges) { + segs.push(...this.eventRangeToSegs(eventRange, dayRanges)) + } + + return segs + } + + eventRangeToSegs(eventRange: EventRenderRange, dayRanges: DateRange[]) { + let { dateEnv } = this.context + let { nextDayThreshold } = this.context.options + let range = eventRange.range + let allDay = eventRange.def.allDay + let dayIndex + let segRange + let seg + let segs = [] + + for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex += 1) { + segRange = intersectRanges(range, dayRanges[dayIndex]) + + if (segRange) { + seg = { + component: this, + eventRange, + start: segRange.start, + end: segRange.end, + isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(), + isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(), + dayIndex, + } + + segs.push(seg) + + // detect when range won't go fully into the next day, + // and mutate the latest seg to the be the end. + if ( + !seg.isEnd && !allDay && + dayIndex + 1 < dayRanges.length && + range.end < + dateEnv.add( + dayRanges[dayIndex + 1].start, + nextDayThreshold, + ) + ) { + seg.end = range.end + seg.isEnd = true + break + } + } + } + + return segs + } +} + +function renderNoEventsInner(renderProps: NoEventsContentArg): ComponentChild { + return renderProps.text +} + +function computeDateVars(dateProfile: DateProfile) { + let dayStart = startOfDay(dateProfile.renderRange.start) + let viewEnd = dateProfile.renderRange.end + let dayDates: DateMarker[] = [] + let dayRanges: DateRange[] = [] + + while (dayStart < viewEnd) { + dayDates.push(dayStart) + + dayRanges.push({ + start: dayStart, + end: addDays(dayStart, 1), + }) + + dayStart = addDays(dayStart, 1) + } + + return { dayDates, dayRanges } +} + +// Returns a sparse array of arrays, segs grouped by their dayIndex +function groupSegsByDay(segs): Seg[][] { + let segsByDay = [] // sparse array + let i + let seg + + for (i = 0; i < segs.length; i += 1) { + seg = segs[i]; + (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) + .push(seg) + } + + return segsByDay +} diff --git a/fullcalendar-main/packages/list/src/ListViewEventRow.tsx b/fullcalendar-main/packages/list/src/ListViewEventRow.tsx new file mode 100644 index 0000000..e00ff5a --- /dev/null +++ b/fullcalendar-main/packages/list/src/ListViewEventRow.tsx @@ -0,0 +1,165 @@ +import { AllDayContentArg } from '@fullcalendar/core' +import { + MinimalEventProps, BaseComponent, ViewContext, + Seg, isMultiDayRange, DateFormatter, buildSegTimeText, createFormatter, + getSegAnchorAttrs, EventContainer, ContentContainer, +} from '@fullcalendar/core/internal' +import { + createElement, + ComponentChildren, + Fragment, + ComponentChild, +} from '@fullcalendar/core/preact' + +const DEFAULT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: 'short', +}) + +export interface ListViewEventRowProps extends MinimalEventProps { + timeHeaderId: string + eventHeaderId: string + dateHeaderId: string +} + +export class ListViewEventRow extends BaseComponent<ListViewEventRowProps> { + render() { + let { props, context } = this + let { options } = context + let { seg, timeHeaderId, eventHeaderId, dateHeaderId } = props + let timeFormat = options.eventTimeFormat || DEFAULT_TIME_FORMAT + + return ( + <EventContainer + {...props} + elTag="tr" + elClasses={[ + 'fc-list-event', + seg.eventRange.def.url && 'fc-event-forced-url', + ]} + defaultGenerator={() => renderEventInnerContent(seg, context) /* weird */} + seg={seg} + timeText="" + disableDragging={true} + disableResizing={true} + > + {(InnerContent, eventContentArg) => ( + <Fragment> + {buildTimeContent(seg, timeFormat, context, timeHeaderId, dateHeaderId)} + <td aria-hidden className="fc-list-event-graphic"> + <span + className="fc-list-event-dot" + style={{ + borderColor: eventContentArg.borderColor || eventContentArg.backgroundColor, + }} + /> + </td> + <InnerContent + elTag="td" + elClasses={['fc-list-event-title']} + elAttrs={{ headers: `${eventHeaderId} ${dateHeaderId}` }} + /> + </Fragment> + )} + </EventContainer> + ) + } +} + +function renderEventInnerContent(seg: Seg, context: ViewContext) { + let interactiveAttrs = getSegAnchorAttrs(seg, context) + return ( + <a {...interactiveAttrs}> + {/* TODO: document how whole row become clickable */} + {seg.eventRange.def.title} + </a> + ) +} + +function buildTimeContent( + seg: Seg, + timeFormat: DateFormatter, + context: ViewContext, + timeHeaderId: string, + dateHeaderId: string, +): ComponentChildren { + let { options } = context + + if (options.displayEventTime !== false) { + let eventDef = seg.eventRange.def + let eventInstance = seg.eventRange.instance + let doAllDay = false + let timeText: string + + if (eventDef.allDay) { + doAllDay = true + } else if (isMultiDayRange(seg.eventRange.range)) { // TODO: use (!isStart || !isEnd) instead? + if (seg.isStart) { + timeText = buildSegTimeText( + seg, + timeFormat, + context, + null, + null, + eventInstance.range.start, + seg.end, + ) + } else if (seg.isEnd) { + timeText = buildSegTimeText( + seg, + timeFormat, + context, + null, + null, + seg.start, + eventInstance.range.end, + ) + } else { + doAllDay = true + } + } else { + timeText = buildSegTimeText( + seg, + timeFormat, + context, + ) + } + + if (doAllDay) { + let renderProps: AllDayContentArg = { + text: context.options.allDayText, + view: context.viewApi, + } + + return ( + <ContentContainer + elTag="td" + elClasses={['fc-list-event-time']} + elAttrs={{ + headers: `${timeHeaderId} ${dateHeaderId}`, + }} + renderProps={renderProps} + generatorName="allDayContent" + customGenerator={options.allDayContent} + defaultGenerator={renderAllDayInner} + classNameGenerator={options.allDayClassNames} + didMount={options.allDayDidMount} + willUnmount={options.allDayWillUnmount} + /> + ) + } + + return ( + <td className="fc-list-event-time"> + {timeText} + </td> + ) + } + + return null +} + +function renderAllDayInner(renderProps: AllDayContentArg): ComponentChild { + return renderProps.text +} diff --git a/fullcalendar-main/packages/list/src/ListViewHeaderRow.tsx b/fullcalendar-main/packages/list/src/ListViewHeaderRow.tsx new file mode 100644 index 0000000..a842a24 --- /dev/null +++ b/fullcalendar-main/packages/list/src/ListViewHeaderRow.tsx @@ -0,0 +1,99 @@ +import { DayHeaderContentArg } from '@fullcalendar/core' +import { + BaseComponent, DateMarker, DateRange, getDateMeta, + getDayClassNames, formatDayString, buildNavLinkAttrs, getUniqueDomId, ContentContainer, +} from '@fullcalendar/core/internal' +import { createElement, Fragment } from '@fullcalendar/core/preact' + +export interface ListViewHeaderRowProps { + cellId: string + dayDate: DateMarker + todayRange: DateRange +} + +export class ListViewHeaderRow extends BaseComponent<ListViewHeaderRowProps> { + state = { + textId: getUniqueDomId(), + } + + render() { + let { theme, dateEnv, options, viewApi } = this.context + let { cellId, dayDate, todayRange } = this.props + let { textId } = this.state + let dayMeta = getDateMeta(dayDate, todayRange) + + // will ever be falsy? + let text = options.listDayFormat ? dateEnv.format(dayDate, options.listDayFormat) : '' + + // will ever be falsy? also, BAD NAME "alt" + let sideText = options.listDaySideFormat ? dateEnv.format(dayDate, options.listDaySideFormat) : '' + + let renderProps: RenderProps = { + date: dateEnv.toDate(dayDate), + view: viewApi, + textId, + text, + sideText, + navLinkAttrs: buildNavLinkAttrs(this.context, dayDate), + sideNavLinkAttrs: buildNavLinkAttrs(this.context, dayDate, 'day', false), + ...dayMeta, + } + + // TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too) + return ( + <ContentContainer + elTag="tr" + elClasses={[ + 'fc-list-day', + ...getDayClassNames(dayMeta, theme), + ]} + elAttrs={{ + 'data-date': formatDayString(dayDate), + }} + renderProps={renderProps} + generatorName="dayHeaderContent" + customGenerator={options.dayHeaderContent} + defaultGenerator={renderInnerContent} + classNameGenerator={options.dayHeaderClassNames} + didMount={options.dayHeaderDidMount} + willUnmount={options.dayHeaderWillUnmount} + > + {(InnerContent) => ( // TODO: force-hide top border based on :first-child + <th scope="colgroup" colSpan={3} id={cellId} aria-labelledby={textId}> + <InnerContent + elTag="div" + elClasses={[ + 'fc-list-day-cushion', + theme.getClass('tableCellShaded'), + ]} + /> + </th> + )} + </ContentContainer> + ) + } +} + +// doesn't enforce much since DayCellContentArg allow extra props +interface RenderProps extends DayHeaderContentArg { + textId: string // for aria-labelledby + text: string + sideText: string +} + +function renderInnerContent(props: RenderProps) { + return ( + <Fragment> + {props.text && ( + <a id={props.textId} className="fc-list-day-text" {...props.navLinkAttrs}> + {props.text} + </a> + )} + {props.sideText && (/* not keyboard tabbable */ + <a aria-hidden className="fc-list-day-side-text" {...props.sideNavLinkAttrs}> + {props.sideText} + </a> + )} + </Fragment> + ) +} diff --git a/fullcalendar-main/packages/list/src/ambient.ts b/fullcalendar-main/packages/list/src/ambient.ts new file mode 100644 index 0000000..f24b443 --- /dev/null +++ b/fullcalendar-main/packages/list/src/ambient.ts @@ -0,0 +1,7 @@ +import { OPTION_REFINERS } from './option-refiners.js' + +type ExtraOptionRefiners = typeof OPTION_REFINERS + +declare module '@fullcalendar/core/internal' { + interface BaseOptionRefiners extends ExtraOptionRefiners {} +} diff --git a/fullcalendar-main/packages/list/src/index.css b/fullcalendar-main/packages/list/src/index.css new file mode 100644 index 0000000..bc3b040 --- /dev/null +++ b/fullcalendar-main/packages/list/src/index.css @@ -0,0 +1,7 @@ + +@import '../../core/src/styles/mixins'; +@import './styles/vars'; + +@import './styles/list'; +@import './styles/list-table'; +@import './styles/list-event'; diff --git a/fullcalendar-main/packages/list/src/index.global.ts b/fullcalendar-main/packages/list/src/index.global.ts new file mode 100644 index 0000000..c70b8e6 --- /dev/null +++ b/fullcalendar-main/packages/list/src/index.global.ts @@ -0,0 +1,8 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' +import * as Internal from './internal.js' + +globalPlugins.push(plugin) + +export { plugin as default, Internal } +export * from './index.js' diff --git a/fullcalendar-main/packages/list/src/index.ts b/fullcalendar-main/packages/list/src/index.ts new file mode 100644 index 0000000..5fd76b7 --- /dev/null +++ b/fullcalendar-main/packages/list/src/index.ts @@ -0,0 +1,40 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { ListView } from './ListView.js' +import { OPTION_REFINERS } from './option-refiners.js' +import './ambient.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + optionRefiners: OPTION_REFINERS, + views: { + list: { + component: ListView, + buttonTextKey: 'list', // what to lookup in locale files + listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' }, // like "January 1, 2016" + }, + listDay: { + type: 'list', + duration: { days: 1 }, + listDayFormat: { weekday: 'long' }, // day-of-week is all we need. full date is probably in headerToolbar + }, + listWeek: { + type: 'list', + duration: { weeks: 1 }, + listDayFormat: { weekday: 'long' }, // day-of-week is more important + listDaySideFormat: { month: 'long', day: 'numeric', year: 'numeric' }, + }, + listMonth: { + type: 'list', + duration: { month: 1 }, + listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have + }, + listYear: { + type: 'list', + duration: { year: 1 }, + listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have + }, + }, +}) as PluginDef + +export * from './public-types.js' diff --git a/fullcalendar-main/packages/list/src/internal.ts b/fullcalendar-main/packages/list/src/internal.ts new file mode 100644 index 0000000..a7f83dc --- /dev/null +++ b/fullcalendar-main/packages/list/src/internal.ts @@ -0,0 +1,3 @@ +import './index.css' + +export { ListView } from './ListView.js' diff --git a/fullcalendar-main/packages/list/src/option-refiners.ts b/fullcalendar-main/packages/list/src/option-refiners.ts new file mode 100644 index 0000000..afe5053 --- /dev/null +++ b/fullcalendar-main/packages/list/src/option-refiners.ts @@ -0,0 +1,30 @@ +import { ClassNamesGenerator, FormatterInput } from '@fullcalendar/core' +import { + identity, + Identity, + CustomContentGenerator, + DidMountHandler, + WillUnmountHandler, + createFormatter, + DateFormatter, +} from '@fullcalendar/core/internal' +import { + NoEventsContentArg, + NoEventsMountArg, +} from './public-types.js' + +export const OPTION_REFINERS = { + listDayFormat: createFalsableFormatter, // defaults specified in list plugins + listDaySideFormat: createFalsableFormatter, // " + + noEventsClassNames: identity as Identity<ClassNamesGenerator<NoEventsContentArg>>, + noEventsContent: identity as Identity<CustomContentGenerator<NoEventsContentArg>>, + noEventsDidMount: identity as Identity<DidMountHandler<NoEventsMountArg>>, + noEventsWillUnmount: identity as Identity<WillUnmountHandler<NoEventsMountArg>>, + + // noEventsText is defined in base options +} + +function createFalsableFormatter(input: FormatterInput | false): DateFormatter { + return input === false ? null : createFormatter(input) +} diff --git a/fullcalendar-main/packages/list/src/public-types.ts b/fullcalendar-main/packages/list/src/public-types.ts new file mode 100644 index 0000000..a70e71f --- /dev/null +++ b/fullcalendar-main/packages/list/src/public-types.ts @@ -0,0 +1 @@ +export { NoEventsContentArg, NoEventsMountArg } from './ListView.js' diff --git a/fullcalendar-main/packages/list/src/styles/list-event.css b/fullcalendar-main/packages/list/src/styles/list-event.css new file mode 100644 index 0000000..b1049e7 --- /dev/null +++ b/fullcalendar-main/packages/list/src/styles/list-event.css @@ -0,0 +1,39 @@ + +.fc { + + & .fc-list-event.fc-event-forced-url { + cursor: pointer; // whole row will seem clickable + } + + & .fc-list-event:hover td { + background-color: var(--fc-list-event-hover-bg-color); + } + + // shrink certain cols + & .fc-list-event-graphic, + & .fc-list-event-time { + white-space: nowrap; + width: 1px; + } + + & .fc-list-event-dot { + display: inline-block; + box-sizing: content-box; + width: 0; + height: 0; + border: calc(var(--fc-list-event-dot-width) / 2) solid var(--fc-event-border-color); + border-radius: calc(var(--fc-list-event-dot-width) / 2); + } + + // reset <a> styling + & .fc-list-event-title a { + color: inherit; + text-decoration: none; + } + + // underline link when hovering over any part of row + & .fc-list-event.fc-event-forced-url:hover a { + text-decoration: underline; + } + +} diff --git a/fullcalendar-main/packages/list/src/styles/list-table.css b/fullcalendar-main/packages/list/src/styles/list-table.css new file mode 100644 index 0000000..5f68d79 --- /dev/null +++ b/fullcalendar-main/packages/list/src/styles/list-table.css @@ -0,0 +1,77 @@ + +.fc { + + // table within the scroller + // ---------------------------------------------------------------------------------------------------- + + & .fc-list-table { + width: 100%; + border-style: hidden; // kill outer border on theme + } + + & .fc-list-table tr > * { + border-left: 0; + border-right: 0; + } + + & .fc-list-sticky { + & .fc-list-day > * { // the cells + position: sticky; + top: 0; + background: var(--fc-page-bg-color); // for when headers are styled to be transparent and sticky + } + } + + // only exists for aria reasons, hide for non-screen-readers + & .fc-list-table thead { + position: absolute; + left: -10000px; + } + + // the table's border-style:hidden gets confused by hidden thead. force-hide top border of first cell + & .fc-list-table tbody > tr:first-child th { + border-top: 0; + } + + & .fc-list-table th { + padding: 0; // uses an inner-wrapper instead... + } + + & .fc-list-table td, + & .fc-list-day-cushion { + padding: 8px 14px; + } + + + // date heading rows + // ---------------------------------------------------------------------------------------------------- + + & .fc-list-day-cushion { + &:after { + @include clearfix; // clear floating + } + } + +} + +.fc-theme-standard { + + & .fc-list-day-cushion { + background-color: var(--fc-neutral-bg-color); + } + +} + +.fc-direction-ltr .fc-list-day-text, +.fc-direction-rtl .fc-list-day-side-text { + float: left; +} + +.fc-direction-ltr .fc-list-day-side-text, +.fc-direction-rtl .fc-list-day-text { + float: right; +} + +// make the dot closer to the event title +.fc-direction-ltr .fc-list-table .fc-list-event-graphic { padding-right: 0 } +.fc-direction-rtl .fc-list-table .fc-list-event-graphic { padding-left: 0 } diff --git a/fullcalendar-main/packages/list/src/styles/list.css b/fullcalendar-main/packages/list/src/styles/list.css new file mode 100644 index 0000000..394f6d2 --- /dev/null +++ b/fullcalendar-main/packages/list/src/styles/list.css @@ -0,0 +1,25 @@ + +.fc-theme-standard { + + & .fc-list { + border: 1px solid var(--fc-border-color); + } + +} + +.fc { + + // message when no events + & .fc-list-empty { + background-color: var(--fc-neutral-bg-color); + height: 100%; + display: flex; + justify-content: center; + align-items: center; // vertically aligns fc-list-empty-inner + } + + & .fc-list-empty-cushion { + margin: 5em 0; + } + +} diff --git a/fullcalendar-main/packages/list/src/styles/vars.css b/fullcalendar-main/packages/list/src/styles/vars.css new file mode 100644 index 0000000..4edc036 --- /dev/null +++ b/fullcalendar-main/packages/list/src/styles/vars.css @@ -0,0 +1,5 @@ + +:root { + --fc-list-event-dot-width: 10px; + --fc-list-event-hover-bg-color: #f5f5f5; +} diff --git a/fullcalendar-main/packages/luxon1/.eslintrc.cjs b/fullcalendar-main/packages/luxon1/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/luxon1/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/luxon1/README.md b/fullcalendar-main/packages/luxon1/README.md new file mode 100644 index 0000000..fc3594d --- /dev/null +++ b/fullcalendar-main/packages/luxon1/README.md @@ -0,0 +1,41 @@ + +# FullCalendar Luxon 1 Plugin + +Enhanced date formatting, conversion, and [named time zone](https://fullcalendar.io/docs/timeZone#named-time-zones) functionality with [Luxon](https://moment.github.io/luxon/) 1 + +## Installation + +First, ensure Luxon is installed: + +```sh +npm install luxon@1 +``` + +Then, install the FullCalendar core package, the Luxon plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/luxon @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import luxonPlugin from '@fullcalendar/luxon' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + luxonPlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + titleFormat: 'LLLL d, yyyy', // use Luxon format strings + timeZone: 'America/New_York' // enhance named time zones +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/luxon1/package.json b/fullcalendar-main/packages/luxon1/package.json new file mode 100644 index 0000000..cb3c0de --- /dev/null +++ b/fullcalendar-main/packages/luxon1/package.json @@ -0,0 +1,52 @@ +{ + "name": "@fullcalendar/luxon", + "version": "6.1.11", + "title": "FullCalendar Luxon 1 Plugin", + "description": "Enhanced date formatting, conversion, and named time zone functionality with Luxon 1", + "keywords": [ + "luxon", + "luxon1", + "timezone" + ], + "homepage": "https://fullcalendar.io/docs/luxon1", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "luxon": "^1.12.1" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "luxon": "^1.12.1" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.Luxon", + "luxon": "luxon" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/luxon1/src/LuxonNamedTimeZone.ts b/fullcalendar-main/packages/luxon1/src/LuxonNamedTimeZone.ts new file mode 100644 index 0000000..71b5024 --- /dev/null +++ b/fullcalendar-main/packages/luxon1/src/LuxonNamedTimeZone.ts @@ -0,0 +1,17 @@ +import { DateTime as LuxonDateTime } from 'luxon' +import { NamedTimeZoneImpl } from '@fullcalendar/core/internal' +import { arrayToLuxon, luxonToArray } from './convert.js' + +export class LuxonNamedTimeZone extends NamedTimeZoneImpl { + offsetForArray(a: number[]): number { + return arrayToLuxon(a, this.timeZoneName).offset + } + + timestampToArray(ms: number): number[] { + return luxonToArray( + LuxonDateTime.fromMillis(ms, { + zone: this.timeZoneName, + }), + ) + } +} diff --git a/fullcalendar-main/packages/luxon1/src/convert.ts b/fullcalendar-main/packages/luxon1/src/convert.ts new file mode 100644 index 0000000..e203454 --- /dev/null +++ b/fullcalendar-main/packages/luxon1/src/convert.ts @@ -0,0 +1,57 @@ +import { CalendarApi, Duration } from '@fullcalendar/core' +import { DateTime as LuxonDateTime, Duration as LuxonDuration } from 'luxon' +import { CalendarImpl } from '@fullcalendar/core/internal' + +export function toLuxonDateTime(date: Date, calendar: CalendarApi): LuxonDateTime { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDateTime.fromJSDate(date, { + zone: dateEnv.timeZone, + locale: dateEnv.locale.codes[0], + }) +} + +export function toLuxonDuration(duration: Duration, calendar: CalendarApi): LuxonDuration { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDuration.fromObject({ + ...duration, + locale: dateEnv.locale.codes[0], + }) +} + +// Internal Utils + +export function luxonToArray(datetime: LuxonDateTime): number[] { + return [ + datetime.year, + datetime.month - 1, // convert 1-based to 0-based + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.millisecond, + ] +} + +export function arrayToLuxon(arr: number[], timeZone: string, locale?: string): LuxonDateTime { + return LuxonDateTime.fromObject({ + zone: timeZone, + locale, + year: arr[0], + month: arr[1] + 1, // convert 0-based to 1-based + day: arr[2], + hour: arr[3], + minute: arr[4], + second: arr[5], + millisecond: arr[6], + }) +} diff --git a/fullcalendar-main/packages/luxon1/src/format.ts b/fullcalendar-main/packages/luxon1/src/format.ts new file mode 100644 index 0000000..ca2f39a --- /dev/null +++ b/fullcalendar-main/packages/luxon1/src/format.ts @@ -0,0 +1,95 @@ +import { VerboseFormattingArg } from '@fullcalendar/core/internal' +import { arrayToLuxon } from './convert.js' + +export function formatWithCmdStr(cmdStr: string, arg: VerboseFormattingArg): string { + let cmd = parseCmdStr(cmdStr) + + if (arg.end) { + let start = arrayToLuxon( + arg.start.array, + arg.timeZone, + arg.localeCodes[0], + ) + let end = arrayToLuxon( + arg.end.array, + arg.timeZone, + arg.localeCodes[0], + ) + return formatRange( + cmd, + start.toFormat.bind(start), + end.toFormat.bind(end), + arg.defaultSeparator, + ) + } + + return arrayToLuxon( + arg.date.array, + arg.timeZone, + arg.localeCodes[0], + ).toFormat(cmd.whole) +} + +/* Range Formatting (duplicate code as other date plugins) +----------------------------------------------------------------------------------------------------*/ + +interface CmdParts { + head: string | null + middle: CmdParts | null + tail: string | null + whole: string +} + +function parseCmdStr(cmdStr: string): CmdParts { + let parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/) // TODO: lookbehinds for escape characters + + if (parts) { + let middle = parseCmdStr(parts[2]) + + return { + head: parts[1], + middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3], + } + } + + return { + head: null, + middle: null, + tail: null, + whole: cmdStr, + } +} + +function formatRange( + cmd: CmdParts, + formatStart: (cmdStr: string) => string, + formatEnd: (cmdStr: string) => string, + separator: string, +): string { + if (cmd.middle) { + let startHead = formatStart(cmd.head) + let startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let startTail = formatStart(cmd.tail) + + let endHead = formatEnd(cmd.head) + let endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let endTail = formatEnd(cmd.tail) + + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail + } + } + + let startWhole = formatStart(cmd.whole) + let endWhole = formatEnd(cmd.whole) + + if (startWhole === endWhole) { + return startWhole + } + + return startWhole + separator + endWhole +} diff --git a/fullcalendar-main/packages/luxon1/src/index.global.ts b/fullcalendar-main/packages/luxon1/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/luxon1/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/luxon1/src/index.ts b/fullcalendar-main/packages/luxon1/src/index.ts new file mode 100644 index 0000000..bcc14cb --- /dev/null +++ b/fullcalendar-main/packages/luxon1/src/index.ts @@ -0,0 +1,11 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { LuxonNamedTimeZone } from './LuxonNamedTimeZone.js' +import { formatWithCmdStr } from './format.js' + +export default createPlugin({ + name: '<%= pkgName %>', + cmdFormatter: formatWithCmdStr, + namedTimeZonedImpl: LuxonNamedTimeZone, +}) as PluginDef + +export { toLuxonDateTime, toLuxonDuration } from './convert.js' diff --git a/fullcalendar-main/packages/luxon2/.eslintrc.cjs b/fullcalendar-main/packages/luxon2/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/luxon2/README.md b/fullcalendar-main/packages/luxon2/README.md new file mode 100644 index 0000000..a0770e1 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/README.md @@ -0,0 +1,41 @@ + +# FullCalendar Luxon 2 Plugin + +Enhanced date formatting, conversion, and [named time zone](https://fullcalendar.io/docs/timeZone#named-time-zones) functionality with [Luxon](https://moment.github.io/luxon/) 2 + +## Installation + +First, ensure Luxon is installed: + +```sh +npm install luxon@2 +``` + +Then, install the FullCalendar core package, the Luxon plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/luxon2 @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import luxon2Plugin from '@fullcalendar/luxon2' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + luxon2Plugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + titleFormat: 'LLLL d, yyyy', // use Luxon format strings + timeZone: 'America/New_York' // enhance named time zones +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/luxon2/package.json b/fullcalendar-main/packages/luxon2/package.json new file mode 100644 index 0000000..07deea1 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/package.json @@ -0,0 +1,53 @@ +{ + "name": "@fullcalendar/luxon2", + "version": "6.1.11", + "title": "FullCalendar Luxon 2 Plugin", + "description": "Enhanced date formatting, conversion, and named time zone functionality with Luxon 2", + "keywords": [ + "luxon", + "luxon2", + "timezone" + ], + "homepage": "https://fullcalendar.io/docs/luxon2", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "luxon": "^2.0.0" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "@types/luxon": "^2.0.9", + "luxon": "^2.0.0" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.Luxon2", + "luxon": "luxon" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/luxon2/src/LuxonNamedTimeZone.ts b/fullcalendar-main/packages/luxon2/src/LuxonNamedTimeZone.ts new file mode 100644 index 0000000..71b5024 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/src/LuxonNamedTimeZone.ts @@ -0,0 +1,17 @@ +import { DateTime as LuxonDateTime } from 'luxon' +import { NamedTimeZoneImpl } from '@fullcalendar/core/internal' +import { arrayToLuxon, luxonToArray } from './convert.js' + +export class LuxonNamedTimeZone extends NamedTimeZoneImpl { + offsetForArray(a: number[]): number { + return arrayToLuxon(a, this.timeZoneName).offset + } + + timestampToArray(ms: number): number[] { + return luxonToArray( + LuxonDateTime.fromMillis(ms, { + zone: this.timeZoneName, + }), + ) + } +} diff --git a/fullcalendar-main/packages/luxon2/src/convert.ts b/fullcalendar-main/packages/luxon2/src/convert.ts new file mode 100644 index 0000000..943b099 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/src/convert.ts @@ -0,0 +1,59 @@ +import { DateTime as LuxonDateTime, Duration as LuxonDuration } from 'luxon' +import { CalendarApi, Duration } from '@fullcalendar/core' +import { CalendarImpl } from '@fullcalendar/core/internal' + +export function toLuxonDateTime(date: Date, calendar: CalendarApi): LuxonDateTime { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDateTime.fromJSDate(date, { + zone: dateEnv.timeZone, + locale: dateEnv.locale.codes[0], + } as { + zone: string // HACK to allow locale property, which IS supported + }) +} + +export function toLuxonDuration(duration: Duration, calendar: CalendarApi): LuxonDuration { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDuration.fromObject(duration, { + locale: dateEnv.locale.codes[0], + }) +} + +// Internal Utils + +export function luxonToArray(datetime: LuxonDateTime): number[] { + return [ + datetime.year, + datetime.month - 1, // convert 1-based to 0-based + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.millisecond, + ] +} + +export function arrayToLuxon(arr: number[], timeZone: string, locale?: string): LuxonDateTime { + return LuxonDateTime.fromObject({ + year: arr[0], + month: arr[1] + 1, // convert 0-based to 1-based + day: arr[2], + hour: arr[3], + minute: arr[4], + second: arr[5], + millisecond: arr[6], + }, { + locale, + zone: timeZone, + }) +} diff --git a/fullcalendar-main/packages/luxon2/src/format.ts b/fullcalendar-main/packages/luxon2/src/format.ts new file mode 100644 index 0000000..ca2f39a --- /dev/null +++ b/fullcalendar-main/packages/luxon2/src/format.ts @@ -0,0 +1,95 @@ +import { VerboseFormattingArg } from '@fullcalendar/core/internal' +import { arrayToLuxon } from './convert.js' + +export function formatWithCmdStr(cmdStr: string, arg: VerboseFormattingArg): string { + let cmd = parseCmdStr(cmdStr) + + if (arg.end) { + let start = arrayToLuxon( + arg.start.array, + arg.timeZone, + arg.localeCodes[0], + ) + let end = arrayToLuxon( + arg.end.array, + arg.timeZone, + arg.localeCodes[0], + ) + return formatRange( + cmd, + start.toFormat.bind(start), + end.toFormat.bind(end), + arg.defaultSeparator, + ) + } + + return arrayToLuxon( + arg.date.array, + arg.timeZone, + arg.localeCodes[0], + ).toFormat(cmd.whole) +} + +/* Range Formatting (duplicate code as other date plugins) +----------------------------------------------------------------------------------------------------*/ + +interface CmdParts { + head: string | null + middle: CmdParts | null + tail: string | null + whole: string +} + +function parseCmdStr(cmdStr: string): CmdParts { + let parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/) // TODO: lookbehinds for escape characters + + if (parts) { + let middle = parseCmdStr(parts[2]) + + return { + head: parts[1], + middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3], + } + } + + return { + head: null, + middle: null, + tail: null, + whole: cmdStr, + } +} + +function formatRange( + cmd: CmdParts, + formatStart: (cmdStr: string) => string, + formatEnd: (cmdStr: string) => string, + separator: string, +): string { + if (cmd.middle) { + let startHead = formatStart(cmd.head) + let startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let startTail = formatStart(cmd.tail) + + let endHead = formatEnd(cmd.head) + let endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let endTail = formatEnd(cmd.tail) + + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail + } + } + + let startWhole = formatStart(cmd.whole) + let endWhole = formatEnd(cmd.whole) + + if (startWhole === endWhole) { + return startWhole + } + + return startWhole + separator + endWhole +} diff --git a/fullcalendar-main/packages/luxon2/src/index.global.ts b/fullcalendar-main/packages/luxon2/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/luxon2/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/luxon2/src/index.ts b/fullcalendar-main/packages/luxon2/src/index.ts new file mode 100644 index 0000000..bcc14cb --- /dev/null +++ b/fullcalendar-main/packages/luxon2/src/index.ts @@ -0,0 +1,11 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { LuxonNamedTimeZone } from './LuxonNamedTimeZone.js' +import { formatWithCmdStr } from './format.js' + +export default createPlugin({ + name: '<%= pkgName %>', + cmdFormatter: formatWithCmdStr, + namedTimeZonedImpl: LuxonNamedTimeZone, +}) as PluginDef + +export { toLuxonDateTime, toLuxonDuration } from './convert.js' diff --git a/fullcalendar-main/packages/luxon3/.eslintrc.cjs b/fullcalendar-main/packages/luxon3/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/luxon3/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/luxon3/README.md b/fullcalendar-main/packages/luxon3/README.md new file mode 100644 index 0000000..24fdebd --- /dev/null +++ b/fullcalendar-main/packages/luxon3/README.md @@ -0,0 +1,41 @@ + +# FullCalendar Luxon 3 Plugin + +Enhanced date formatting, conversion, and [named time zone](https://fullcalendar.io/docs/timeZone#named-time-zones) functionality with [Luxon](https://moment.github.io/luxon/) 3 + +## Installation + +First, ensure Luxon is installed: + +```sh +npm install luxon@3 +``` + +Then, install the FullCalendar core package, the Luxon plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/luxon3 @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import luxon3Plugin from '@fullcalendar/luxon3' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + luxon3Plugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + titleFormat: 'LLLL d, yyyy', // use Luxon format strings + timeZone: 'America/New_York' // enhance named time zones +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/luxon3/package.json b/fullcalendar-main/packages/luxon3/package.json new file mode 100644 index 0000000..c3290b2 --- /dev/null +++ b/fullcalendar-main/packages/luxon3/package.json @@ -0,0 +1,53 @@ +{ + "name": "@fullcalendar/luxon3", + "version": "6.1.11", + "title": "FullCalendar Luxon 3 Plugin", + "description": "Enhanced date formatting, conversion, and named time zone functionality with Luxon 3", + "keywords": [ + "luxon", + "luxon3", + "timezone" + ], + "homepage": "https://fullcalendar.io/docs/luxon", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "luxon": "^3.0.0" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "@types/luxon": "^3.3.0", + "luxon": "^3.0.0" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.Luxon3", + "luxon": "luxon" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/luxon3/src/LuxonNamedTimeZone.ts b/fullcalendar-main/packages/luxon3/src/LuxonNamedTimeZone.ts new file mode 100644 index 0000000..71b5024 --- /dev/null +++ b/fullcalendar-main/packages/luxon3/src/LuxonNamedTimeZone.ts @@ -0,0 +1,17 @@ +import { DateTime as LuxonDateTime } from 'luxon' +import { NamedTimeZoneImpl } from '@fullcalendar/core/internal' +import { arrayToLuxon, luxonToArray } from './convert.js' + +export class LuxonNamedTimeZone extends NamedTimeZoneImpl { + offsetForArray(a: number[]): number { + return arrayToLuxon(a, this.timeZoneName).offset + } + + timestampToArray(ms: number): number[] { + return luxonToArray( + LuxonDateTime.fromMillis(ms, { + zone: this.timeZoneName, + }), + ) + } +} diff --git a/fullcalendar-main/packages/luxon3/src/convert.ts b/fullcalendar-main/packages/luxon3/src/convert.ts new file mode 100644 index 0000000..943b099 --- /dev/null +++ b/fullcalendar-main/packages/luxon3/src/convert.ts @@ -0,0 +1,59 @@ +import { DateTime as LuxonDateTime, Duration as LuxonDuration } from 'luxon' +import { CalendarApi, Duration } from '@fullcalendar/core' +import { CalendarImpl } from '@fullcalendar/core/internal' + +export function toLuxonDateTime(date: Date, calendar: CalendarApi): LuxonDateTime { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDateTime.fromJSDate(date, { + zone: dateEnv.timeZone, + locale: dateEnv.locale.codes[0], + } as { + zone: string // HACK to allow locale property, which IS supported + }) +} + +export function toLuxonDuration(duration: Duration, calendar: CalendarApi): LuxonDuration { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return LuxonDuration.fromObject(duration, { + locale: dateEnv.locale.codes[0], + }) +} + +// Internal Utils + +export function luxonToArray(datetime: LuxonDateTime): number[] { + return [ + datetime.year, + datetime.month - 1, // convert 1-based to 0-based + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.millisecond, + ] +} + +export function arrayToLuxon(arr: number[], timeZone: string, locale?: string): LuxonDateTime { + return LuxonDateTime.fromObject({ + year: arr[0], + month: arr[1] + 1, // convert 0-based to 1-based + day: arr[2], + hour: arr[3], + minute: arr[4], + second: arr[5], + millisecond: arr[6], + }, { + locale, + zone: timeZone, + }) +} diff --git a/fullcalendar-main/packages/luxon3/src/format.ts b/fullcalendar-main/packages/luxon3/src/format.ts new file mode 100644 index 0000000..ca2f39a --- /dev/null +++ b/fullcalendar-main/packages/luxon3/src/format.ts @@ -0,0 +1,95 @@ +import { VerboseFormattingArg } from '@fullcalendar/core/internal' +import { arrayToLuxon } from './convert.js' + +export function formatWithCmdStr(cmdStr: string, arg: VerboseFormattingArg): string { + let cmd = parseCmdStr(cmdStr) + + if (arg.end) { + let start = arrayToLuxon( + arg.start.array, + arg.timeZone, + arg.localeCodes[0], + ) + let end = arrayToLuxon( + arg.end.array, + arg.timeZone, + arg.localeCodes[0], + ) + return formatRange( + cmd, + start.toFormat.bind(start), + end.toFormat.bind(end), + arg.defaultSeparator, + ) + } + + return arrayToLuxon( + arg.date.array, + arg.timeZone, + arg.localeCodes[0], + ).toFormat(cmd.whole) +} + +/* Range Formatting (duplicate code as other date plugins) +----------------------------------------------------------------------------------------------------*/ + +interface CmdParts { + head: string | null + middle: CmdParts | null + tail: string | null + whole: string +} + +function parseCmdStr(cmdStr: string): CmdParts { + let parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/) // TODO: lookbehinds for escape characters + + if (parts) { + let middle = parseCmdStr(parts[2]) + + return { + head: parts[1], + middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3], + } + } + + return { + head: null, + middle: null, + tail: null, + whole: cmdStr, + } +} + +function formatRange( + cmd: CmdParts, + formatStart: (cmdStr: string) => string, + formatEnd: (cmdStr: string) => string, + separator: string, +): string { + if (cmd.middle) { + let startHead = formatStart(cmd.head) + let startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let startTail = formatStart(cmd.tail) + + let endHead = formatEnd(cmd.head) + let endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let endTail = formatEnd(cmd.tail) + + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail + } + } + + let startWhole = formatStart(cmd.whole) + let endWhole = formatEnd(cmd.whole) + + if (startWhole === endWhole) { + return startWhole + } + + return startWhole + separator + endWhole +} diff --git a/fullcalendar-main/packages/luxon3/src/index.global.ts b/fullcalendar-main/packages/luxon3/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/luxon3/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/luxon3/src/index.ts b/fullcalendar-main/packages/luxon3/src/index.ts new file mode 100644 index 0000000..bcc14cb --- /dev/null +++ b/fullcalendar-main/packages/luxon3/src/index.ts @@ -0,0 +1,11 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { LuxonNamedTimeZone } from './LuxonNamedTimeZone.js' +import { formatWithCmdStr } from './format.js' + +export default createPlugin({ + name: '<%= pkgName %>', + cmdFormatter: formatWithCmdStr, + namedTimeZonedImpl: LuxonNamedTimeZone, +}) as PluginDef + +export { toLuxonDateTime, toLuxonDuration } from './convert.js' diff --git a/fullcalendar-main/packages/moment-timezone/.eslintrc.cjs b/fullcalendar-main/packages/moment-timezone/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/moment-timezone/README.md b/fullcalendar-main/packages/moment-timezone/README.md new file mode 100644 index 0000000..f38b3e1 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/README.md @@ -0,0 +1,40 @@ + +# FullCalendar Moment Timezone Plugin + +Enhanced [named time zone](https://fullcalendar.io/docs/timeZone#named-time-zones) functionality with [Moment Timezone](https://momentjs.com/timezone/) + +## Installation + +First, ensure Moment Timezone is installed: + +```sh +npm install moment-timezone +``` + +Then, install the FullCalendar core package, the Moment Timezone plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/moment-timezone @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import momentTimezonePlugin from '@fullcalendar/moment-timezone' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + momentTimezonePlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + timeZone: 'America/New_York' // enhance named time zones +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/moment-timezone/package.json b/fullcalendar-main/packages/moment-timezone/package.json new file mode 100644 index 0000000..ead7d58 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/package.json @@ -0,0 +1,52 @@ +{ + "name": "@fullcalendar/moment-timezone", + "version": "6.1.11", + "title": "FullCalendar Moment Timezone Plugin", + "description": "Enhanced named time zone functionality with Moment Timezone", + "keywords": [ + "moment", + "moment-timezone", + "timezone" + ], + "homepage": "https://fullcalendar.io/docs/moment-timezone-plugin", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "moment-timezone": "^0.5.40" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "moment-timezone": "^0.5.40" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.MomentTimezone", + "moment-timezone": "moment" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/moment-timezone/src/MomentNamedTimeZone.ts b/fullcalendar-main/packages/moment-timezone/src/MomentNamedTimeZone.ts new file mode 100644 index 0000000..938dbb0 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/src/MomentNamedTimeZone.ts @@ -0,0 +1,12 @@ +import moment from 'moment-timezone' +import { NamedTimeZoneImpl } from '@fullcalendar/core/internal' + +export class MomentNamedTimeZone extends NamedTimeZoneImpl { + offsetForArray(a: number[]): number { + return moment.tz(a, this.timeZoneName).utcOffset() + } + + timestampToArray(ms: number): number[] { + return moment.tz(ms, this.timeZoneName).toArray() + } +} diff --git a/fullcalendar-main/packages/moment-timezone/src/index.global.ts b/fullcalendar-main/packages/moment-timezone/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/moment-timezone/src/index.ts b/fullcalendar-main/packages/moment-timezone/src/index.ts new file mode 100644 index 0000000..72996b1 --- /dev/null +++ b/fullcalendar-main/packages/moment-timezone/src/index.ts @@ -0,0 +1,7 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { MomentNamedTimeZone } from './MomentNamedTimeZone.js' + +export default createPlugin({ + name: '<%= pkgName %>', + namedTimeZonedImpl: MomentNamedTimeZone, +}) as PluginDef diff --git a/fullcalendar-main/packages/moment/.eslintrc.cjs b/fullcalendar-main/packages/moment/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/moment/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/moment/README.md b/fullcalendar-main/packages/moment/README.md new file mode 100644 index 0000000..4c1ee27 --- /dev/null +++ b/fullcalendar-main/packages/moment/README.md @@ -0,0 +1,40 @@ + +# FullCalendar Moment Plugin + +Enhanced date formatting and conversion with [Moment](https://momentjs.com/) + +## Installation + +First, ensure Moment is installed: + +```sh +npm install moment +``` + +Then, install the FullCalendar core package, the Moment plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/moment @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import momentPlugin from '@fullcalendar/moment' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + momentPlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + titleFormat: 'MMMM D, YYYY' // use Moment format strings +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/moment/package.json b/fullcalendar-main/packages/moment/package.json new file mode 100644 index 0000000..0c23968 --- /dev/null +++ b/fullcalendar-main/packages/moment/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/moment", + "version": "6.1.11", + "title": "FullCalendar Moment Plugin", + "description": "Enhanced date formatting and conversion with Moment", + "keywords": [ + "moment" + ], + "homepage": "https://fullcalendar.io/docs/moment-plugin", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "moment": "^2.29.1" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "moment": "^2.29.1" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.Moment", + "moment": "moment" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/moment/src/convert.ts b/fullcalendar-main/packages/moment/src/convert.ts new file mode 100644 index 0000000..b1707a4 --- /dev/null +++ b/fullcalendar-main/packages/moment/src/convert.ts @@ -0,0 +1,51 @@ +import moment from 'moment' +import { CalendarApi, Duration } from '@fullcalendar/core' +import { CalendarImpl } from '@fullcalendar/core/internal' + +export function toMoment(date: Date, calendar: CalendarApi): moment.Moment { + if (!(calendar instanceof CalendarImpl)) { + throw new Error('must supply a CalendarApi instance') + } + + let { dateEnv } = calendar.getCurrentData() + + return convertToMoment( + date, + dateEnv.timeZone, + null, + dateEnv.locale.codes[0], + ) +} + +export function toMomentDuration(fcDuration: Duration): moment.Duration { + return moment.duration(fcDuration) // moment accepts all the props that fc.Duration already has! +} + +// Internal Utils + +export function convertToMoment( + input: any, + timeZone: string, + timeZoneOffset: number | null, + locale: string, +): moment.Moment { + let mom: moment.Moment + + if (timeZone === 'local') { + mom = moment(input) + } else if (timeZone === 'UTC') { + mom = moment.utc(input) + } else if ((moment as any).tz) { + mom = (moment as any).tz(input, timeZone) + } else { + mom = moment.utc(input) + + if (timeZoneOffset != null) { + mom.utcOffset(timeZoneOffset) + } + } + + mom.locale(locale) + + return mom +} diff --git a/fullcalendar-main/packages/moment/src/format.ts b/fullcalendar-main/packages/moment/src/format.ts new file mode 100644 index 0000000..afa7b86 --- /dev/null +++ b/fullcalendar-main/packages/moment/src/format.ts @@ -0,0 +1,104 @@ +import { VerboseFormattingArg } from '@fullcalendar/core/internal' +import { convertToMoment } from './convert.js' + +export function formatWithCmdStr(cmdStr: string, arg: VerboseFormattingArg): string { + let cmd = parseCmdStr(cmdStr) + + if (arg.end) { + let startMom = convertToMoment( + arg.start.array, + arg.timeZone, + arg.start.timeZoneOffset, + arg.localeCodes[0], + ) + let endMom = convertToMoment( + arg.end.array, + arg.timeZone, + arg.end.timeZoneOffset, + arg.localeCodes[0], + ) + return formatRange( + cmd, + createMomentFormatFunc(startMom), + createMomentFormatFunc(endMom), + arg.defaultSeparator, + ) + } + + return convertToMoment( + arg.date.array, + arg.timeZone, + arg.date.timeZoneOffset, + arg.localeCodes[0], + ).format(cmd.whole) // TODO: test for this +} + +function createMomentFormatFunc(mom: moment.Moment) { + return (cmdStr) => ( + cmdStr ? mom.format(cmdStr) : '' // because calling with blank string results in ISO8601 :( + ) +} + +/* Range Formatting (duplicate code as other date plugins) +----------------------------------------------------------------------------------------------------*/ + +interface CmdParts { + head: string | null + middle: CmdParts | null + tail: string | null + whole: string +} + +function parseCmdStr(cmdStr: string): CmdParts { + let parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/) // TODO: lookbehinds for escape characters + + if (parts) { + let middle = parseCmdStr(parts[2]) + + return { + head: parts[1], + middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3], + } + } + + return { + head: null, + middle: null, + tail: null, + whole: cmdStr, + } +} + +function formatRange( + cmd: CmdParts, + formatStart: (cmdStr: string) => string, + formatEnd: (cmdStr: string) => string, + separator: string, +): string { + if (cmd.middle) { + let startHead = formatStart(cmd.head) + let startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let startTail = formatStart(cmd.tail) + + let endHead = formatEnd(cmd.head) + let endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator) + let endTail = formatEnd(cmd.tail) + + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail + } + } + + let startWhole = formatStart(cmd.whole) + let endWhole = formatEnd(cmd.whole) + + if (startWhole === endWhole) { + return startWhole + } + + return startWhole + separator + endWhole +} diff --git a/fullcalendar-main/packages/moment/src/index.global.ts b/fullcalendar-main/packages/moment/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/moment/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/moment/src/index.ts b/fullcalendar-main/packages/moment/src/index.ts new file mode 100644 index 0000000..dad436f --- /dev/null +++ b/fullcalendar-main/packages/moment/src/index.ts @@ -0,0 +1,9 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { formatWithCmdStr } from './format.js' + +export default createPlugin({ + name: '<%= pkgName %>', + cmdFormatter: formatWithCmdStr, +}) as PluginDef + +export { toMoment, toMomentDuration } from './convert.js' diff --git a/fullcalendar-main/packages/multimonth/.eslintrc.cjs b/fullcalendar-main/packages/multimonth/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/multimonth/README.md b/fullcalendar-main/packages/multimonth/README.md new file mode 100644 index 0000000..20c6600 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/README.md @@ -0,0 +1,32 @@ + +# FullCalendar Multi-Month Plugin + +Display multiple months, in a grid or vertical stack + +## Installation + +Install the necessary packages: + +```sh +npm install @fullcalendar/core @fullcalendar/multimonth +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import multiMonthPlugin from '@fullcalendar/multimonth' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [multiMonthPlugin], + initialView: 'multiMonthYear', + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/multimonth/package.json b/fullcalendar-main/packages/multimonth/package.json new file mode 100644 index 0000000..ce535b6 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/multimonth", + "version": "6.1.11", + "title": "FullCalendar Multi-Month Plugin", + "description": "Display a sequence or grid of multiple months", + "keywords": [ + "month" + ], + "homepage": "https://fullcalendar.io/docs/multimonth-grid", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.11" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.MultiMonth" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/multimonth/src/MultiMonthView.tsx b/fullcalendar-main/packages/multimonth/src/MultiMonthView.tsx new file mode 100644 index 0000000..8381c20 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/MultiMonthView.tsx @@ -0,0 +1,253 @@ +import { + DateComponent, + ViewProps, + ViewContainer, + DateProfile, + intersectRanges, + DateMarker, + DateEnv, + createDuration, + memoize, + DateFormatter, + createFormatter, + isPropsEqual, + DateProfileGenerator, + formatIsoMonthStr, +} from '@fullcalendar/core/internal' +import { buildDayTableRenderRange } from '@fullcalendar/daygrid/internal' +import { createElement, createRef } from '@fullcalendar/core/preact' +import { SingleMonth } from './SingleMonth.js' + +interface MultiMonthViewState { + clientWidth?: number + clientHeight?: number + monthHPadding?: number +} + +export class MultiMonthView extends DateComponent<ViewProps, MultiMonthViewState> { + private splitDateProfileByMonth = memoize(splitDateProfileByMonth) + private buildMonthFormat = memoize(buildMonthFormat) + private scrollElRef = createRef<HTMLDivElement>() + private firstMonthElRef = createRef<HTMLDivElement>() + private needsScrollReset = false + + render() { + const { context, props, state } = this + const { options } = context + const { clientWidth, clientHeight } = state + const monthHPadding = state.monthHPadding || 0 + + const colCount = Math.min( + clientWidth != null ? + Math.floor(clientWidth / (options.multiMonthMinWidth + monthHPadding)) : + 1, + options.multiMonthMaxColumns, + ) || 1 + + const monthWidthPct = (100 / colCount) + '%' + const monthTableWidth = clientWidth == null ? null : + (clientWidth / colCount) - monthHPadding + + const isLegitSingleCol = clientWidth != null && colCount === 1 + const monthDateProfiles = this.splitDateProfileByMonth( + context.dateProfileGenerator, + props.dateProfile, + context.dateEnv, + isLegitSingleCol ? false : options.fixedWeekCount, + options.showNonCurrentDates, + ) + + const monthTitleFormat = this.buildMonthFormat(options.multiMonthTitleFormat, monthDateProfiles) + const rootClassNames = [ + 'fc-multimonth', + isLegitSingleCol ? + 'fc-multimonth-singlecol' : + 'fc-multimonth-multicol', + (monthTableWidth != null && monthTableWidth < 400) ? + 'fc-multimonth-compact' : + '', + ] + + return ( + <ViewContainer + elRef={this.scrollElRef} + elClasses={rootClassNames} + viewSpec={context.viewSpec} + > + {monthDateProfiles.map((monthDateProfile, i) => { + const monthStr = formatIsoMonthStr(monthDateProfile.currentRange.start) + + return ( + <SingleMonth + {...props} + key={monthStr} + isoDateStr={monthStr} + elRef={i === 0 ? this.firstMonthElRef : undefined} + titleFormat={monthTitleFormat} + dateProfile={monthDateProfile} + width={monthWidthPct} + tableWidth={monthTableWidth} + clientWidth={clientWidth} + clientHeight={clientHeight} + /> + ) + })} + </ViewContainer> + ) + } + + componentDidMount(): void { + this.updateSize() + this.context.addResizeHandler(this.handleSizing) + this.requestScrollReset() + } + + componentDidUpdate(prevProps: ViewProps) { + if (!isPropsEqual(prevProps, this.props)) { // an external change? + this.handleSizing(false) + } + + if (prevProps.dateProfile !== this.props.dateProfile) { + this.requestScrollReset() + } else { + this.flushScrollReset() + } + } + + componentWillUnmount() { + this.context.removeResizeHandler(this.handleSizing) + } + + handleSizing = (isForced: boolean) => { + if (isForced) { + this.updateSize() + } + } + + updateSize() { + const scrollEl = this.scrollElRef.current + const firstMonthEl = this.firstMonthElRef.current + + if (scrollEl) { + this.setState({ + clientWidth: scrollEl.clientWidth, + clientHeight: scrollEl.clientHeight, + }) + } + + if (firstMonthEl && scrollEl) { + if (this.state.monthHPadding == null) { // always remember initial non-zero value + this.setState({ + monthHPadding: + scrollEl.clientWidth - // go within padding + (firstMonthEl.firstChild as HTMLElement).offsetWidth, + }) + } + } + } + + requestScrollReset() { + this.needsScrollReset = true + this.flushScrollReset() + } + + flushScrollReset() { + if ( + this.needsScrollReset && + this.state.monthHPadding != null // indicates sizing already happened + ) { + const { currentDate } = this.props.dateProfile + const scrollEl = this.scrollElRef.current + const monthEl = scrollEl.querySelector(`[data-date="${formatIsoMonthStr(currentDate)}"]`) + + scrollEl.scrollTop = monthEl.getBoundingClientRect().top - + this.firstMonthElRef.current.getBoundingClientRect().top + + this.needsScrollReset = false + } + } + + // workaround for when queued setState render (w/ clientWidth) gets cancelled because + // subsequent update and shouldComponentUpdate says not to render :( + shouldComponentUpdate() { + return true + } +} + +// date profile +// ------------------------------------------------------------------------------------------------- + +const oneMonthDuration = createDuration(1, 'month') + +function splitDateProfileByMonth( + dateProfileGenerator: DateProfileGenerator, + dateProfile: DateProfile, + dateEnv: DateEnv, + fixedWeekCount?: boolean, + showNonCurrentDates?: boolean, +): DateProfile[] { + const { start, end } = dateProfile.currentRange + let monthStart: DateMarker = start + const monthDateProfiles: DateProfile[] = [] + + while (monthStart.valueOf() < end.valueOf()) { + const monthEnd = dateEnv.add(monthStart, oneMonthDuration) + const currentRange = { + // yuck + start: dateProfileGenerator.skipHiddenDays(monthStart), + end: dateProfileGenerator.skipHiddenDays(monthEnd, -1, true), + } + let renderRange = buildDayTableRenderRange({ + currentRange, + snapToWeek: true, + fixedWeekCount, + dateEnv, + }) + renderRange = { + // yuck + start: dateProfileGenerator.skipHiddenDays(renderRange.start), + end: dateProfileGenerator.skipHiddenDays(renderRange.end, -1, true), + } + const activeRange = dateProfile.activeRange ? + intersectRanges( + dateProfile.activeRange, + showNonCurrentDates ? renderRange : currentRange, + ) : + null + + monthDateProfiles.push({ + currentDate: dateProfile.currentDate, + isValid: dateProfile.isValid, + validRange: dateProfile.validRange, + renderRange, + activeRange, + currentRange, + currentRangeUnit: 'month', + isRangeAllDay: true, + dateIncrement: dateProfile.dateIncrement, + slotMinTime: dateProfile.slotMaxTime, + slotMaxTime: dateProfile.slotMinTime, + }) + + monthStart = monthEnd + } + + return monthDateProfiles +} + +// date formatting +// ------------------------------------------------------------------------------------------------- + +const YEAR_MONTH_FORMATTER = createFormatter({ year: 'numeric', month: 'long' }) +const YEAR_FORMATTER = createFormatter({ month: 'long' }) + +function buildMonthFormat( + formatOverride: DateFormatter | undefined, + monthDateProfiles: DateProfile[], +): DateFormatter { + return formatOverride || + ((monthDateProfiles[0].currentRange.start.getUTCFullYear() !== + monthDateProfiles[monthDateProfiles.length - 1].currentRange.start.getUTCFullYear()) + ? YEAR_MONTH_FORMATTER + : YEAR_FORMATTER) +} diff --git a/fullcalendar-main/packages/multimonth/src/SingleMonth.tsx b/fullcalendar-main/packages/multimonth/src/SingleMonth.tsx new file mode 100644 index 0000000..06c46d6 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/SingleMonth.tsx @@ -0,0 +1,114 @@ +import { CssDimValue } from '@fullcalendar/core' +import { DateComponent, DayHeader, ViewProps, memoize, DateFormatter, getUniqueDomId } from '@fullcalendar/core/internal' +import { TableRows, buildDayTableModel, DayTableSlicer } from '@fullcalendar/daygrid/internal' +import { createElement, Ref } from '@fullcalendar/core/preact' + +export interface SingleMonthProps extends ViewProps { + elRef?: Ref<HTMLDivElement> + isoDateStr?: string + titleFormat: DateFormatter + width: CssDimValue + tableWidth: number | null // solely for computation purposes + clientWidth: number | null + clientHeight: number | null +} + +interface SingleMonthState { + labelId: string +} + +export class SingleMonth extends DateComponent<SingleMonthProps, SingleMonthState> { + private buildDayTableModel = memoize(buildDayTableModel) + private slicer = new DayTableSlicer() + + state: SingleMonthState = { + labelId: getUniqueDomId(), + } + + render() { + const { props, state, context } = this + const { dateProfile, forPrint } = props + const { options } = context + const dayTableModel = this.buildDayTableModel(dateProfile, context.dateProfileGenerator) + const slicedProps = this.slicer.sliceProps(props, dateProfile, options.nextDayThreshold, context, dayTableModel) + + // ensure single-month has aspect ratio + const tableHeight = props.tableWidth != null ? props.tableWidth / options.aspectRatio : null + const rowCnt = dayTableModel.cells.length + const rowHeight = tableHeight != null ? tableHeight / rowCnt : null + + return ( + <div + ref={props.elRef} + data-date={props.isoDateStr} + className="fc-multimonth-month" + style={{ width: props.width }} + role="grid" + aria-labelledby={state.labelId} + > + <div + className="fc-multimonth-header" + style={{ marginBottom: rowHeight }} // for stickyness + role="presentation" + > + <div className="fc-multimonth-title" id={state.labelId}> + {context.dateEnv.format( + props.dateProfile.currentRange.start, + props.titleFormat, + )} + </div> + <table + className={[ + 'fc-multimonth-header-table', + context.theme.getClass('table'), + ].join(' ')} + role="presentation" + > + <thead role="rowgroup"> + <DayHeader + dateProfile={props.dateProfile} + dates={dayTableModel.headerDates} + datesRepDistinctDays={false} + /> + </thead> + </table> + </div> + <div + className={[ + 'fc-multimonth-daygrid', + 'fc-daygrid', + 'fc-daygrid-body', // necessary for TableRows DnD parent + !forPrint && 'fc-daygrid-body-balanced', + forPrint && 'fc-daygrid-body-unbalanced', + forPrint && 'fc-daygrid-body-natural', + ].join(' ')} + style={{ marginTop: -rowHeight }} // for stickyness + > + <table + className={[ + 'fc-multimonth-daygrid-table', + context.theme.getClass('table'), + ].join(' ')} + style={{ height: forPrint ? '' : tableHeight }} + role="presentation" + > + <tbody role="rowgroup"> + <TableRows + {...slicedProps} + dateProfile={dateProfile} + cells={dayTableModel.cells} + eventSelection={props.eventSelection} + dayMaxEvents={!forPrint} + dayMaxEventRows={!forPrint} + showWeekNumbers={options.weekNumbers} + clientWidth={props.clientWidth} + clientHeight={props.clientHeight} + forPrint={forPrint} + /> + </tbody> + </table> + </div> + </div> + ) + } +} diff --git a/fullcalendar-main/packages/multimonth/src/ambient.ts b/fullcalendar-main/packages/multimonth/src/ambient.ts new file mode 100644 index 0000000..cf3bf49 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/ambient.ts @@ -0,0 +1,7 @@ +import { OPTION_REFINERS } from './options-refiners.js' + +type ExtraOptionRefiners = typeof OPTION_REFINERS + +declare module '@fullcalendar/core/internal' { + interface BaseOptionRefiners extends ExtraOptionRefiners {} +} diff --git a/fullcalendar-main/packages/multimonth/src/index.css b/fullcalendar-main/packages/multimonth/src/index.css new file mode 100644 index 0000000..25a0fed --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/index.css @@ -0,0 +1,103 @@ + +.fc { + + & .fc-multimonth { + display: flex; + flex-wrap: wrap; + border: 1px solid var(--fc-border-color); + overflow-y: auto; + overflow-x: hidden; + } + + // a single month + + & .fc-multimonth-title { + padding: 1em 0; + text-align: center; + font-weight: bold; + font-size: 1.2em; + } + + & .fc-multimonth-daygrid { + background: var(--fc-page-bg-color); + } + + & .fc-multimonth-header-table, + & .fc-multimonth-daygrid-table { + width: 100%; + table-layout: fixed; + } + + & .fc-multimonth-daygrid-table { + border-top-style: hidden !important; + } + + // variants + + & .fc-multimonth-singlecol { + & .fc-multimonth { + position: relative; + } + + & .fc-multimonth-header { + position: relative; // ultimately for stick + top: 0; + z-index: 2; + background: var(--fc-page-bg-color); + } + + & .fc-multimonth-daygrid { + position: relative; + z-index: 1; + } + + & .fc-multimonth-header-table, + & .fc-multimonth-daygrid-table { + border-left-style: hidden; + border-right-style: hidden; + } + + & .fc-multimonth-month:last-child .fc-multimonth-daygrid-table { + border-bottom-style: hidden; + } + } + + & .fc-multimonth-multicol { + line-height: 1; // undo themes that have thick line-height + + & .fc-multimonth-month { + padding: 0 1.2em 1.2em; + } + + // more-link look like an event + & .fc-daygrid-more-link { + border: 1px solid var(--fc-event-border-color); + padding: 1px; + float: none; + display: block; + } + } + + & .fc-multimonth-compact { + line-height: 1; // undo themes that have thick line-height + + & .fc-multimonth-header-table, + & .fc-multimonth-daygrid-table { + font-size: 0.9em; + } + } +} + +.fc-media-screen { + & .fc-multimonth-singlecol { + & .fc-multimonth-header { + position: sticky; + } + } +} + +.fc-media-print { + & .fc-multimonth { + overflow: visible; + } +} diff --git a/fullcalendar-main/packages/multimonth/src/index.global.ts b/fullcalendar-main/packages/multimonth/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/multimonth/src/index.ts b/fullcalendar-main/packages/multimonth/src/index.ts new file mode 100644 index 0000000..7e6a6e9 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/index.ts @@ -0,0 +1,26 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { TableDateProfileGenerator } from '@fullcalendar/daygrid/internal' +import { MultiMonthView } from './MultiMonthView.js' +import { OPTION_REFINERS } from './options-refiners.js' +import './ambient.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + initialView: 'multiMonthYear', + optionRefiners: OPTION_REFINERS, + views: { + multiMonth: { + component: MultiMonthView, + dateProfileGeneratorClass: TableDateProfileGenerator, + multiMonthMinWidth: 350, + multiMonthMaxColumns: 3, + }, + multiMonthYear: { + type: 'multiMonth', + duration: { years: 1 }, + fixedWeekCount: true, + showNonCurrentDates: false, + }, + }, +}) as PluginDef diff --git a/fullcalendar-main/packages/multimonth/src/options-refiners.ts b/fullcalendar-main/packages/multimonth/src/options-refiners.ts new file mode 100644 index 0000000..a5f33a3 --- /dev/null +++ b/fullcalendar-main/packages/multimonth/src/options-refiners.ts @@ -0,0 +1,7 @@ +import { createFormatter } from '@fullcalendar/core/internal' + +export const OPTION_REFINERS = { + multiMonthTitleFormat: createFormatter, + multiMonthMaxColumns: Number, + multiMonthMinWidth: Number, +} diff --git a/fullcalendar-main/packages/rrule/.eslintrc.cjs b/fullcalendar-main/packages/rrule/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/rrule/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/rrule/README.md b/fullcalendar-main/packages/rrule/README.md new file mode 100644 index 0000000..131d8c1 --- /dev/null +++ b/fullcalendar-main/packages/rrule/README.md @@ -0,0 +1,48 @@ + +# FullCalendar RRule Plugin + +Recurring events with [RRule](https://github.com/jakubroztocil/rrule) + +## Installation + +First, ensure the RRule lib is installed: + +```sh +npm install rrule +``` + +Then, install the FullCalendar core package, the RRule plugin, and any other plugins (like [daygrid](https://fullcalendar.io/docs/month-view)): + +```sh +npm install @fullcalendar/core @fullcalendar/rrule @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import rrulePlugin from '@fullcalendar/rrule' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + rrulePlugin, + dayGridPlugin + ], + initialView: 'dayGridMonth', + events: [ + { + title: 'Meeting', + rrule: { + freq: 'weekly', + byweekday: ['mo', 'fr'] + } + } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/rrule/package.json b/fullcalendar-main/packages/rrule/package.json new file mode 100644 index 0000000..a1f958e --- /dev/null +++ b/fullcalendar-main/packages/rrule/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/rrule", + "version": "6.1.11", + "title": "FullCalendar RRule Plugin", + "description": "Recurring events with RRule", + "keywords": [ + "rrule" + ], + "homepage": "https://fullcalendar.io/docs/rrule-plugin", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11", + "rrule": "^2.6.0" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*", + "rrule": "^2.6.0" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + } + }, + "iifeGlobals": { + ".": "FullCalendar.RRule", + "rrule": "rrule" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/rrule/src/ambient.ts b/fullcalendar-main/packages/rrule/src/ambient.ts new file mode 100644 index 0000000..63d6f40 --- /dev/null +++ b/fullcalendar-main/packages/rrule/src/ambient.ts @@ -0,0 +1,7 @@ +import { RRULE_EVENT_REFINERS } from './event-refiners.js' + +type ExtraRefiners = typeof RRULE_EVENT_REFINERS + +declare module '@fullcalendar/core/internal' { + interface EventRefiners extends ExtraRefiners {} +} diff --git a/fullcalendar-main/packages/rrule/src/event-refiners.ts b/fullcalendar-main/packages/rrule/src/event-refiners.ts new file mode 100644 index 0000000..7971f9e --- /dev/null +++ b/fullcalendar-main/packages/rrule/src/event-refiners.ts @@ -0,0 +1,21 @@ +import { Options as RRuleOptions } from 'rrule' +import { DateInput } from '@fullcalendar/core' +import { createDuration, identity, Identity } from '@fullcalendar/core/internal' + +export type RRuleInputObjectFull = Omit<RRuleOptions, 'dtstart' | 'until' | 'freq' | 'wkst' | 'byweekday'> & { + dtstart: RRuleOptions['dtstart'] | DateInput + until: RRuleOptions['until'] | DateInput + freq: RRuleOptions['freq'] | string + wkst: RRuleOptions['wkst'] | string + byweekday: RRuleOptions['byweekday'] | string | string[] +} + +export type RRuleInputObject = Partial<RRuleInputObjectFull> +export type RRuleInput = RRuleInputObject | string + +export const RRULE_EVENT_REFINERS = { + rrule: identity as Identity<RRuleInput>, + exrule: identity as Identity<RRuleInputObject | RRuleInputObject[]>, + exdate: identity as Identity<DateInput | DateInput[]>, + duration: createDuration, +} diff --git a/fullcalendar-main/packages/rrule/src/index.global.ts b/fullcalendar-main/packages/rrule/src/index.global.ts new file mode 100644 index 0000000..7a091f6 --- /dev/null +++ b/fullcalendar-main/packages/rrule/src/index.global.ts @@ -0,0 +1,7 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' + +globalPlugins.push(plugin) + +export { plugin as default } +export * from './index.js' diff --git a/fullcalendar-main/packages/rrule/src/index.ts b/fullcalendar-main/packages/rrule/src/index.ts new file mode 100644 index 0000000..3da48f7 --- /dev/null +++ b/fullcalendar-main/packages/rrule/src/index.ts @@ -0,0 +1,10 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { recurringType } from './recurring-type.js' +import { RRULE_EVENT_REFINERS } from './event-refiners.js' +import './ambient.js' + +export default createPlugin({ + name: '<%= pkgName %>', + recurringTypes: [recurringType], + eventRefiners: RRULE_EVENT_REFINERS, +}) as PluginDef diff --git a/fullcalendar-main/packages/rrule/src/recurring-type.ts b/fullcalendar-main/packages/rrule/src/recurring-type.ts new file mode 100644 index 0000000..1e6dd64 --- /dev/null +++ b/fullcalendar-main/packages/rrule/src/recurring-type.ts @@ -0,0 +1,167 @@ +import * as rruleLib from 'rrule' // see https://github.com/jakubroztocil/rrule/issues/548 +import { DateInput } from '@fullcalendar/core' +import { + RecurringType, + EventRefined, + DateEnv, + DateRange, + DateMarker, + parseMarker, +} from '@fullcalendar/core/internal' +import { RRuleInputObject } from './event-refiners.js' + +interface EventRRuleData { + rruleSet: rruleLib.RRuleSet + isTimeZoneSpecified: boolean +} + +export const recurringType: RecurringType<EventRRuleData> = { + parse(eventProps: EventRefined, dateEnv: DateEnv) { + if (eventProps.rrule != null) { + let eventRRuleData = parseEventRRule(eventProps, dateEnv) + + if (eventRRuleData) { + return { + typeData: { rruleSet: eventRRuleData.rruleSet, isTimeZoneSpecified: eventRRuleData.isTimeZoneSpecified }, + allDayGuess: !eventRRuleData.isTimeSpecified, + duration: eventProps.duration, + } + } + } + + return null + }, + expand(eventRRuleData: EventRRuleData, framingRange: DateRange, dateEnv: DateEnv): DateMarker[] { + let dates: DateMarker[] + + if (eventRRuleData.isTimeZoneSpecified) { + dates = eventRRuleData.rruleSet.between( + dateEnv.toDate(framingRange.start), // rrule lib will treat as UTC-zoned + dateEnv.toDate(framingRange.end), // (same) + true, // inclusive (will give extra events at start, see https://github.com/jakubroztocil/rrule/issues/84) + ).map((date) => dateEnv.createMarker(date)) // convert UTC-zoned-date to locale datemarker + } else { + // when no timezone in given start/end, the rrule lib will assume UTC, + // which is same as our DateMarkers. no need to manipulate + dates = eventRRuleData.rruleSet.between( + framingRange.start, + framingRange.end, + true, // inclusive (will give extra events at start, see https://github.com/jakubroztocil/rrule/issues/84) + ) + } + return dates + }, +} + +function parseEventRRule(eventProps: EventRefined, dateEnv: DateEnv) { + let rruleSet: rruleLib.RRuleSet + let isTimeSpecified = false + let isTimeZoneSpecified = false + + if (typeof eventProps.rrule === 'string') { + let res = parseRRuleString(eventProps.rrule) + rruleSet = res.rruleSet + isTimeSpecified = res.isTimeSpecified + isTimeZoneSpecified = res.isTimeZoneSpecified + } + + if (typeof eventProps.rrule === 'object' && eventProps.rrule) { // non-null object + let res = parseRRuleObject(eventProps.rrule, dateEnv) + rruleSet = new rruleLib.RRuleSet() + rruleSet.rrule(res.rrule) + isTimeSpecified = res.isTimeSpecified + isTimeZoneSpecified = res.isTimeZoneSpecified + } + + // convery to arrays. TODO: general util? + let exdateInputs: DateInput[] = [].concat(eventProps.exdate || []) + let exruleInputs: RRuleInputObject[] = [].concat(eventProps.exrule || []) + + for (let exdateInput of exdateInputs) { + let res = parseMarker(exdateInput) + isTimeSpecified = isTimeSpecified || !res.isTimeUnspecified + isTimeZoneSpecified = isTimeZoneSpecified || res.timeZoneOffset !== null + rruleSet.exdate( + new Date(res.marker.valueOf() - (res.timeZoneOffset || 0) * 60 * 1000), // NOT DRY + ) + } + + // TODO: exrule is deprecated. what to do? (https://icalendar.org/iCalendar-RFC-5545/a-3-deprecated-features.html) + for (let exruleInput of exruleInputs) { + let res = parseRRuleObject(exruleInput, dateEnv) + isTimeSpecified = isTimeSpecified || res.isTimeSpecified + isTimeZoneSpecified = isTimeZoneSpecified || res.isTimeZoneSpecified + rruleSet.exrule(res.rrule) + } + + return { rruleSet, isTimeSpecified, isTimeZoneSpecified } +} + +function parseRRuleObject(rruleInput: RRuleInputObject, dateEnv: DateEnv) { + let isTimeSpecified = false + let isTimeZoneSpecified = false + + function processDateInput(dateInput: DateInput) { + if (typeof dateInput === 'string') { + let markerData = parseMarker(dateInput) + if (markerData) { + isTimeSpecified = isTimeSpecified || !markerData.isTimeUnspecified + isTimeZoneSpecified = isTimeZoneSpecified || markerData.timeZoneOffset !== null + return new Date(markerData.marker.valueOf() - (markerData.timeZoneOffset || 0) * 60 * 1000) // NOT DRY + } + return null + } + return dateInput as Date // TODO: what about number timestamps? + } + + let rruleOptions: Partial<rruleLib.Options> = { + ...rruleInput, + dtstart: processDateInput(rruleInput.dtstart), + until: processDateInput(rruleInput.until), + freq: convertConstant(rruleInput.freq), + wkst: rruleInput.wkst == null + ? (dateEnv.weekDow - 1 + 7) % 7 // convert Sunday-first to Monday-first + : convertConstant(rruleInput.wkst), + byweekday: convertConstants(rruleInput.byweekday), + } + + return { rrule: new rruleLib.RRule(rruleOptions), isTimeSpecified, isTimeZoneSpecified } +} + +function parseRRuleString(str) { + let rruleSet = rruleLib.rrulestr(str, { forceset: true }) as rruleLib.RRuleSet + let analysis = analyzeRRuleString(str) + + return { rruleSet, ...analysis } +} + +function analyzeRRuleString(str) { + let isTimeSpecified = false + let isTimeZoneSpecified = false + + function processMatch(whole: string, introPart: string, datePart: string) { + let result = parseMarker(datePart) + isTimeSpecified = isTimeSpecified || !result.isTimeUnspecified + isTimeZoneSpecified = isTimeZoneSpecified || result.timeZoneOffset !== null + } + + str.replace(/\b(DTSTART:)([^\n]*)/, processMatch) + str.replace(/\b(EXDATE:)([^\n]*)/, processMatch) + str.replace(/\b(UNTIL=)([^;\n]*)/, processMatch) + + return { isTimeSpecified, isTimeZoneSpecified } +} + +function convertConstants(input): number | null | number[] | null[] { + if (Array.isArray(input)) { + return input.map(convertConstant) + } + return convertConstant(input) +} + +function convertConstant(input): number | null { + if (typeof input === 'string') { + return rruleLib.RRule[input.toUpperCase()] + } + return input +} diff --git a/fullcalendar-main/packages/timegrid/.eslintrc.cjs b/fullcalendar-main/packages/timegrid/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/timegrid/README.md b/fullcalendar-main/packages/timegrid/README.md new file mode 100644 index 0000000..cfce878 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/README.md @@ -0,0 +1,32 @@ + +# FullCalendar Time Grid Plugin + +Display events on time slots + +## Installation + +Install the necessary packages: + +```sh +npm install @fullcalendar/core @fullcalendar/timegrid +``` + +## Usage + +Instantiate a Calendar with the necessary plugin: + +```js +import { Calendar } from '@fullcalendar/core' +import timeGridPlugin from '@fullcalendar/timegrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [timeGridPlugin], + initialView: 'timeGridWeek', + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` diff --git a/fullcalendar-main/packages/timegrid/package.json b/fullcalendar-main/packages/timegrid/package.json new file mode 100644 index 0000000..a5fa04f --- /dev/null +++ b/fullcalendar-main/packages/timegrid/package.json @@ -0,0 +1,53 @@ +{ + "name": "@fullcalendar/timegrid", + "version": "6.1.11", + "title": "FullCalendar Time Grid Plugin", + "description": "Display events on time slots", + "keywords": [ + "time", + "slots" + ], + "homepage": "https://fullcalendar.io/docs/timegrid-view", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.11" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./internal": {} + }, + "iifeGlobals": { + ".": "FullCalendar.TimeGrid", + "./internal": "FullCalendar.TimeGrid.Internal" + } + }, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/timegrid/src/AllDaySplitter.ts b/fullcalendar-main/packages/timegrid/src/AllDaySplitter.ts new file mode 100644 index 0000000..3b71e45 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/AllDaySplitter.ts @@ -0,0 +1,35 @@ +import { + Splitter, + hasBgRendering, + EventDef, + DateSpan, +} from '@fullcalendar/core/internal' + +export class AllDaySplitter extends Splitter { + getKeyInfo() { + return { + allDay: {}, + timed: {}, + } + } + + getKeysForDateSpan(dateSpan: DateSpan): string[] { + if (dateSpan.allDay) { + return ['allDay'] + } + + return ['timed'] + } + + getKeysForEventDef(eventDef: EventDef): string[] { + if (!eventDef.allDay) { + return ['timed'] + } + + if (hasBgRendering(eventDef)) { + return ['timed', 'allDay'] + } + + return ['allDay'] + } +} diff --git a/fullcalendar-main/packages/timegrid/src/DayTimeCols.tsx b/fullcalendar-main/packages/timegrid/src/DayTimeCols.tsx new file mode 100644 index 0000000..673b430 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/DayTimeCols.tsx @@ -0,0 +1,102 @@ +import { Duration, CssDimValue } from '@fullcalendar/core' +import { + DateComponent, + DateProfile, + EventStore, + EventUiHash, + EventInteractionState, + DateSpan, + memoize, + DateRange, + DayTableModel, + DateEnv, + DateMarker, + NowTimer, +} from '@fullcalendar/core/internal' +import { + createElement, + createRef, + VNode, +} from '@fullcalendar/core/preact' +import { TimeCols } from './TimeCols.js' +import { TimeSlatMeta } from './time-slat-meta.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { DayTimeColsSlicer } from './DayTimeColsSlicer.js' + +export interface DayTimeColsProps { + dateProfile: DateProfile + dayTableModel: DayTableModel + axis: boolean + slotDuration: Duration + slatMetas: TimeSlatMeta[] + businessHours: EventStore + eventStore: EventStore + eventUiBases: EventUiHash + dateSelection: DateSpan | null + eventSelection: string + eventDrag: EventInteractionState | null + eventResize: EventInteractionState | null + tableColGroupNode: VNode + tableMinWidth: CssDimValue + clientWidth: number | null + clientHeight: number | null + expandRows: boolean + onScrollTopRequest?: (scrollTop: number) => void + forPrint: boolean + onSlatCoords?: (slatCoords: TimeColsSlatsCoords) => void +} + +export class DayTimeCols extends DateComponent<DayTimeColsProps> { + private buildDayRanges = memoize(buildDayRanges) + private slicer = new DayTimeColsSlicer() + private timeColsRef = createRef<TimeCols>() + + render() { + let { props, context } = this + let { dateProfile, dayTableModel } = props + let { nowIndicator, nextDayThreshold } = context.options + let dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv) + + // give it the first row of cells + // TODO: would move this further down hierarchy, but sliceNowDate needs it + return ( + <NowTimer unit={nowIndicator ? 'minute' : 'day'}> + {(nowDate: DateMarker, todayRange: DateRange) => ( + <TimeCols + ref={this.timeColsRef} + {...this.slicer.sliceProps(props, dateProfile, null, context, dayRanges)} + forPrint={props.forPrint} + axis={props.axis} + dateProfile={dateProfile} + slatMetas={props.slatMetas} + slotDuration={props.slotDuration} + cells={dayTableModel.cells[0]} + tableColGroupNode={props.tableColGroupNode} + tableMinWidth={props.tableMinWidth} + clientWidth={props.clientWidth} + clientHeight={props.clientHeight} + expandRows={props.expandRows} + nowDate={nowDate} + nowIndicatorSegs={nowIndicator && this.slicer.sliceNowDate(nowDate, dateProfile, nextDayThreshold, context, dayRanges)} + todayRange={todayRange} + onScrollTopRequest={props.onScrollTopRequest} + onSlatCoords={props.onSlatCoords} + /> + )} + </NowTimer> + ) + } +} + +export function buildDayRanges(dayTableModel: DayTableModel, dateProfile: DateProfile, dateEnv: DateEnv): DateRange[] { + let ranges: DateRange[] = [] + + for (let date of dayTableModel.headerDates) { + ranges.push({ + start: dateEnv.add(date, dateProfile.slotMinTime), + end: dateEnv.add(date, dateProfile.slotMaxTime), + }) + } + + return ranges +} diff --git a/fullcalendar-main/packages/timegrid/src/DayTimeColsSlicer.ts b/fullcalendar-main/packages/timegrid/src/DayTimeColsSlicer.ts new file mode 100644 index 0000000..255f983 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/DayTimeColsSlicer.ts @@ -0,0 +1,24 @@ +import { intersectRanges, DateRange, Slicer } from '@fullcalendar/core/internal' +import { TimeColsSeg } from './TimeColsSeg.js' + +export class DayTimeColsSlicer extends Slicer<TimeColsSeg, [DateRange[]]> { + sliceRange(range: DateRange, dayRanges: DateRange[]): TimeColsSeg[] { + let segs: TimeColsSeg[] = [] + + for (let col = 0; col < dayRanges.length; col += 1) { + let segRange = intersectRanges(range, dayRanges[col]) + + if (segRange) { + segs.push({ + start: segRange.start, + end: segRange.end, + isStart: segRange.start.valueOf() === range.start.valueOf(), + isEnd: segRange.end.valueOf() === range.end.valueOf(), + col, + }) + } + } + + return segs + } +} diff --git a/fullcalendar-main/packages/timegrid/src/DayTimeColsView.tsx b/fullcalendar-main/packages/timegrid/src/DayTimeColsView.tsx new file mode 100644 index 0000000..b0f97b1 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/DayTimeColsView.tsx @@ -0,0 +1,108 @@ +import { + DateProfileGenerator, + DateProfile, + DayHeader, + DaySeriesModel, + DayTableModel, + memoize, + ChunkContentCallbackArgs, +} from '@fullcalendar/core/internal' +import { + createElement, +} from '@fullcalendar/core/preact' +import { DayTable } from '@fullcalendar/daygrid/internal' +import { TimeColsView } from './TimeColsView.js' +import { DayTimeCols } from './DayTimeCols.js' +import { buildSlatMetas } from './time-slat-meta.js' + +export class DayTimeColsView extends TimeColsView { + private buildTimeColsModel = memoize(buildTimeColsModel) + private buildSlatMetas = memoize(buildSlatMetas) + + render() { + let { options, dateEnv, dateProfileGenerator } = this.context + let { props } = this + let { dateProfile } = props + let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator) + let splitProps = this.allDaySplitter.splitProps(props) + let slatMetas = this.buildSlatMetas( + dateProfile.slotMinTime, + dateProfile.slotMaxTime, + options.slotLabelInterval, + options.slotDuration, + dateEnv, + ) + let { dayMinWidth } = options + let hasAttachedAxis = !dayMinWidth + let hasDetachedAxis = dayMinWidth + + let headerContent = options.dayHeaders && ( + <DayHeader + dates={dayTableModel.headerDates} + dateProfile={dateProfile} + datesRepDistinctDays + renderIntro={hasAttachedAxis ? this.renderHeadAxis : null} + /> + ) + + let allDayContent = (options.allDaySlot !== false) && ((contentArg: ChunkContentCallbackArgs) => ( + <DayTable + {...splitProps.allDay} + dateProfile={dateProfile} + dayTableModel={dayTableModel} + nextDayThreshold={options.nextDayThreshold} + tableMinWidth={contentArg.tableMinWidth} + colGroupNode={contentArg.tableColGroupNode} + renderRowIntro={hasAttachedAxis ? this.renderTableRowAxis : null} + showWeekNumbers={false} + expandRows={false} + headerAlignElRef={this.headerElRef} + clientWidth={contentArg.clientWidth} + clientHeight={contentArg.clientHeight} + forPrint={props.forPrint} + {...this.getAllDayMaxEventProps()} + /> + )) + + let timeGridContent = (contentArg: ChunkContentCallbackArgs) => ( + <DayTimeCols + {...splitProps.timed} + dayTableModel={dayTableModel} + dateProfile={dateProfile} + axis={hasAttachedAxis} + slotDuration={options.slotDuration} + slatMetas={slatMetas} + forPrint={props.forPrint} + tableColGroupNode={contentArg.tableColGroupNode} + tableMinWidth={contentArg.tableMinWidth} + clientWidth={contentArg.clientWidth} + clientHeight={contentArg.clientHeight} + onSlatCoords={this.handleSlatCoords} + expandRows={contentArg.expandRows} + onScrollTopRequest={this.handleScrollTopRequest} + /> + ) + + return hasDetachedAxis + ? this.renderHScrollLayout( + headerContent, + allDayContent, + timeGridContent, + dayTableModel.colCnt, + dayMinWidth, + slatMetas, + this.state.slatCoords, + ) + : this.renderSimpleLayout( + headerContent, + allDayContent, + timeGridContent, + ) + } +} + +export function buildTimeColsModel(dateProfile: DateProfile, dateProfileGenerator: DateProfileGenerator) { + let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator) + + return new DayTableModel(daySeries, false) +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeBodyAxis.tsx b/fullcalendar-main/packages/timegrid/src/TimeBodyAxis.tsx new file mode 100644 index 0000000..aa57230 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeBodyAxis.tsx @@ -0,0 +1,21 @@ +import { BaseComponent } from '@fullcalendar/core/internal' +import { createElement } from '@fullcalendar/core/preact' +import { TimeColsAxisCell } from './TimeColsAxisCell.js' +import { TimeSlatMeta } from './time-slat-meta.js' + +/* Thin Axis +------------------------------------------------------------------------------------------------------------------*/ + +interface TimeBodyAxisProps { + slatMetas: TimeSlatMeta[] +} + +export class TimeBodyAxis extends BaseComponent<TimeBodyAxisProps> { // just <tr> content + render() { + return this.props.slatMetas.map((slatMeta: TimeSlatMeta) => ( + <tr key={slatMeta.key}> + <TimeColsAxisCell {...slatMeta} /> + </tr> + )) + } +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeCol.tsx b/fullcalendar-main/packages/timegrid/src/TimeCol.tsx new file mode 100644 index 0000000..80d89d0 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeCol.tsx @@ -0,0 +1,357 @@ +import { CssDimValue } from '@fullcalendar/core' +import { + DateMarker, BaseComponent, EventSegUiInteractionState, Seg, getSegMeta, + DateRange, DayCellContainer, NowIndicatorContainer, BgEvent, renderFill, buildIsoString, computeEarliestSegStart, + DateProfile, buildEventRangeKey, sortEventSegs, memoize, SegEntryGroup, SegEntry, Dictionary, SegSpan, hasCustomDayCellContent, +} from '@fullcalendar/core/internal' +import { + createElement, + Fragment, + Ref, +} from '@fullcalendar/core/preact' +import { TimeColMoreLink } from './TimeColMoreLink.js' +import { TimeColsSeg } from './TimeColsSeg.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { SegWebRect } from './seg-web.js' +import { computeFgSegPlacements, computeSegVCoords } from './event-placement.js' +import { TimeColEvent } from './TimeColEvent.js' + +export interface TimeColProps { + elRef?: Ref<HTMLTableCellElement> + dateProfile: DateProfile + date: DateMarker + nowDate: DateMarker + todayRange: DateRange + extraDataAttrs?: any + extraRenderProps?: any + extraClassNames?: string[] + extraDateSpan?: Dictionary + fgEventSegs: TimeColsSeg[] + bgEventSegs: TimeColsSeg[] + businessHourSegs: TimeColsSeg[] + nowIndicatorSegs: TimeColsSeg[] + dateSelectionSegs: TimeColsSeg[] + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + slatCoords: TimeColsSlatsCoords + forPrint: boolean +} + +export class TimeCol extends BaseComponent<TimeColProps> { + sortEventSegs = memoize(sortEventSegs) as (typeof sortEventSegs) + // TODO: memoize event-placement? + + render() { + let { props, context } = this + let { options } = context + let isSelectMirror = options.selectMirror + + let mirrorSegs: Seg[] = // yuck + (props.eventDrag && props.eventDrag.segs) || + (props.eventResize && props.eventResize.segs) || + (isSelectMirror && props.dateSelectionSegs) || + [] + + let interactionAffectedInstances = // TODO: messy way to compute this + (props.eventDrag && props.eventDrag.affectedInstances) || + (props.eventResize && props.eventResize.affectedInstances) || + {} + + let sortedFgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder) as TimeColsSeg[] + + return ( + <DayCellContainer + elTag="td" + elRef={props.elRef} + elClasses={[ + 'fc-timegrid-col', + ...(props.extraClassNames || []), + ]} + elAttrs={{ + role: 'gridcell', + ...props.extraDataAttrs, + }} + date={props.date} + dateProfile={props.dateProfile} + todayRange={props.todayRange} + extraRenderProps={props.extraRenderProps} + > + {(InnerContent) => ( + <div className="fc-timegrid-col-frame"> + <div className="fc-timegrid-col-bg"> + {this.renderFillSegs(props.businessHourSegs, 'non-business')} + {this.renderFillSegs(props.bgEventSegs, 'bg-event')} + {this.renderFillSegs(props.dateSelectionSegs, 'highlight')} + </div> + <div className="fc-timegrid-col-events"> + {this.renderFgSegs( + sortedFgSegs, + interactionAffectedInstances, + false, + false, + false, + )} + </div> + <div className="fc-timegrid-col-events"> + {this.renderFgSegs( + mirrorSegs as TimeColsSeg[], + {}, + Boolean(props.eventDrag), + Boolean(props.eventResize), + Boolean(isSelectMirror), + 'mirror', + )} + </div> + <div className="fc-timegrid-now-indicator-container"> + {this.renderNowIndicator(props.nowIndicatorSegs)} + </div> + {hasCustomDayCellContent(options) && ( + <InnerContent + elTag="div" + elClasses={['fc-timegrid-col-misc']} + /> + )} + </div> + )} + </DayCellContainer> + ) + } + + renderFgSegs( + sortedFgSegs: TimeColsSeg[], + segIsInvisible: { [instanceId: string]: any }, + isDragging: boolean, + isResizing: boolean, + isDateSelecting: boolean, + forcedKey?: string, + ) { + let { props } = this + if (props.forPrint) { + return renderPlainFgSegs(sortedFgSegs, props) + } + return this.renderPositionedFgSegs( + sortedFgSegs, + segIsInvisible, + isDragging, + isResizing, + isDateSelecting, + forcedKey, + ) + } + + renderPositionedFgSegs( + segs: TimeColsSeg[], // if not mirror, needs to be sorted + segIsInvisible: { [instanceId: string]: any }, + isDragging: boolean, + isResizing: boolean, + isDateSelecting: boolean, + forcedKey?: string, + ) { + let { eventMaxStack, eventShortHeight, eventOrderStrict, eventMinHeight } = this.context.options + let { date, slatCoords, eventSelection, todayRange, nowDate } = this.props + let isMirror = isDragging || isResizing || isDateSelecting + let segVCoords = computeSegVCoords(segs, date, slatCoords, eventMinHeight) + let { segPlacements, hiddenGroups } = computeFgSegPlacements(segs, segVCoords, eventOrderStrict, eventMaxStack) + + return ( + <Fragment> + {this.renderHiddenGroups(hiddenGroups, segs)} + {segPlacements.map((segPlacement) => { + let { seg, rect } = segPlacement + let instanceId = seg.eventRange.instance.instanceId + let isVisible = isMirror || Boolean(!segIsInvisible[instanceId] && rect) + let vStyle = computeSegVStyle(rect && rect.span) + let hStyle = (!isMirror && rect) ? this.computeSegHStyle(rect) : { left: 0, right: 0 } + let isInset = Boolean(rect) && rect.stackForward > 0 + let isShort = Boolean(rect) && (rect.span.end - rect.span.start) < eventShortHeight // look at other places for this problem + + return ( + <div + className={ + 'fc-timegrid-event-harness' + + (isInset ? ' fc-timegrid-event-harness-inset' : '') + } + key={forcedKey || instanceId} + style={{ + visibility: isVisible ? ('' as any) : 'hidden', + ...vStyle, + ...hStyle, + }} + > + <TimeColEvent + seg={seg} + isDragging={isDragging} + isResizing={isResizing} + isDateSelecting={isDateSelecting} + isSelected={instanceId === eventSelection} + isShort={isShort} + {...getSegMeta(seg, todayRange, nowDate)} + /> + </div> + ) + })} + </Fragment> + ) + } + + // will already have eventMinHeight applied because segInputs already had it + renderHiddenGroups(hiddenGroups: SegEntryGroup[], segs: TimeColsSeg[]) { + let { extraDateSpan, dateProfile, todayRange, nowDate, eventSelection, eventDrag, eventResize } = this.props + return ( + <Fragment> + {hiddenGroups.map((hiddenGroup) => { + let positionCss = computeSegVStyle(hiddenGroup.span) + let hiddenSegs = compileSegsFromEntries(hiddenGroup.entries, segs) + return ( + <TimeColMoreLink + key={buildIsoString(computeEarliestSegStart(hiddenSegs))} + hiddenSegs={hiddenSegs} + top={positionCss.top} + bottom={positionCss.bottom} + extraDateSpan={extraDateSpan} + dateProfile={dateProfile} + todayRange={todayRange} + nowDate={nowDate} + eventSelection={eventSelection} + eventDrag={eventDrag} + eventResize={eventResize} + /> + ) + })} + </Fragment> + ) + } + + renderFillSegs(segs: TimeColsSeg[], fillType: string) { + let { props, context } = this + let segVCoords = computeSegVCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight) // don't assume all populated + + let children = segVCoords.map((vcoords, i) => { + let seg = segs[i] + return ( + <div + key={buildEventRangeKey(seg.eventRange)} + className="fc-timegrid-bg-harness" + style={computeSegVStyle(vcoords)} + > + {fillType === 'bg-event' ? + <BgEvent seg={seg} {...getSegMeta(seg, props.todayRange, props.nowDate)} /> : + renderFill(fillType)} + </div> + ) + }) + + return <Fragment>{children}</Fragment> + } + + renderNowIndicator(segs: TimeColsSeg[]) { + let { slatCoords, date } = this.props + + if (!slatCoords) { return null } + + return segs.map((seg, i) => ( + <NowIndicatorContainer + // key doesn't matter. will only ever be one + key={i} // eslint-disable-line react/no-array-index-key + elClasses={['fc-timegrid-now-indicator-line']} + elStyle={{ + top: slatCoords.computeDateTop(seg.start, date), + }} + isAxis={false} + date={date} + /> + )) + } + + computeSegHStyle(segHCoords: SegWebRect) { + let { isRtl, options } = this.context + let shouldOverlap = options.slotEventOverlap + let nearCoord = segHCoords.levelCoord // the left side if LTR. the right side if RTL. floating-point + let farCoord = segHCoords.levelCoord + segHCoords.thickness // the right side if LTR. the left side if RTL. floating-point + let left // amount of space from left edge, a fraction of the total width + let right // amount of space from right edge, a fraction of the total width + + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + farCoord = Math.min(1, nearCoord + (farCoord - nearCoord) * 2) + } + + if (isRtl) { + left = 1 - farCoord + right = nearCoord + } else { + left = nearCoord + right = 1 - farCoord + } + + let props = { + zIndex: segHCoords.stackDepth + 1, // convert from 0-base to 1-based + left: left * 100 + '%', + right: right * 100 + '%', + } + + if (shouldOverlap && !segHCoords.stackForward) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2 // 10 is a guesstimate of the icon's width + } + + return props + } +} + +export function renderPlainFgSegs( + sortedFgSegs: TimeColsSeg[], + { todayRange, nowDate, eventSelection, eventDrag, eventResize }: { + todayRange: DateRange + nowDate: DateMarker + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + }, +) { + let hiddenInstances = + (eventDrag ? eventDrag.affectedInstances : null) || + (eventResize ? eventResize.affectedInstances : null) || + {} + return ( + <Fragment> + {sortedFgSegs.map((seg) => { + let instanceId = seg.eventRange.instance.instanceId + return ( + <div + key={instanceId} + style={{ visibility: hiddenInstances[instanceId] ? 'hidden' : ('' as any) }} + > + <TimeColEvent + seg={seg} + isDragging={false} + isResizing={false} + isDateSelecting={false} + isSelected={instanceId === eventSelection} + isShort={false} + {...getSegMeta(seg, todayRange, nowDate)} + /> + </div> + ) + })} + </Fragment> + ) +} + +function computeSegVStyle(segVCoords: SegSpan | null): { top: CssDimValue, bottom: CssDimValue } { + if (!segVCoords) { + return { top: '', bottom: '' } + } + return { + top: segVCoords.start, + bottom: -segVCoords.end, + } +} + +function compileSegsFromEntries( + segEntries: SegEntry[], + allSegs: TimeColsSeg[], +): TimeColsSeg[] { + return segEntries.map((segEntry) => allSegs[segEntry.index]) +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColEvent.tsx b/fullcalendar-main/packages/timegrid/src/TimeColEvent.tsx new file mode 100644 index 0000000..291bdcf --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColEvent.tsx @@ -0,0 +1,28 @@ +import { StandardEvent, BaseComponent, MinimalEventProps, createFormatter } from '@fullcalendar/core/internal' +import { createElement } from '@fullcalendar/core/preact' + +const DEFAULT_TIME_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + meridiem: false, +}) + +export interface TimeColEventProps extends MinimalEventProps { + isShort: boolean +} + +export class TimeColEvent extends BaseComponent<TimeColEventProps> { + render() { + return ( + <StandardEvent + {...this.props} + elClasses={[ + 'fc-timegrid-event', + 'fc-v-event', + this.props.isShort && 'fc-timegrid-event-short', + ]} + defaultTimeFormat={DEFAULT_TIME_FORMAT} + /> + ) + } +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColMoreLink.tsx b/fullcalendar-main/packages/timegrid/src/TimeColMoreLink.tsx new file mode 100644 index 0000000..daf87c4 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColMoreLink.tsx @@ -0,0 +1,58 @@ +import { MoreLinkContentArg, CssDimValue } from '@fullcalendar/core' +import { + MoreLinkContainer, BaseComponent, + Dictionary, DateProfile, DateRange, DateMarker, EventSegUiInteractionState, +} from '@fullcalendar/core/internal' +import { createElement } from '@fullcalendar/core/preact' +import { renderPlainFgSegs } from './TimeCol.js' +import { TimeColsSeg } from './TimeColsSeg.js' + +export interface TimeColMoreLinkProps { + hiddenSegs: TimeColsSeg[] + top: CssDimValue + bottom: CssDimValue + extraDateSpan?: Dictionary + dateProfile: DateProfile + todayRange: DateRange + nowDate: DateMarker + eventSelection: string + eventDrag: EventSegUiInteractionState + eventResize: EventSegUiInteractionState +} + +export class TimeColMoreLink extends BaseComponent<TimeColMoreLinkProps> { + render() { + let { props } = this + + return ( + <MoreLinkContainer + elClasses={['fc-timegrid-more-link']} + elStyle={{ + top: props.top, + bottom: props.bottom, + }} + allDayDate={null} + moreCnt={props.hiddenSegs.length} + allSegs={props.hiddenSegs} + hiddenSegs={props.hiddenSegs} + extraDateSpan={props.extraDateSpan} + dateProfile={props.dateProfile} + todayRange={props.todayRange} + popoverContent={() => renderPlainFgSegs(props.hiddenSegs, props)} + defaultGenerator={renderMoreLinkInner} + forceTimed={true} + > + {(InnerContent) => ( + <InnerContent + elTag="div" + elClasses={['fc-timegrid-more-link-inner', 'fc-sticky']} + /> + )} + </MoreLinkContainer> + ) + } +} + +function renderMoreLinkInner(props: MoreLinkContentArg) { + return props.shortText +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeCols.tsx b/fullcalendar-main/packages/timegrid/src/TimeCols.tsx new file mode 100644 index 0000000..2a1066e --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeCols.tsx @@ -0,0 +1,236 @@ +import { Duration, CssDimValue } from '@fullcalendar/core' +import { + addDurations, + multiplyDuration, + wholeDivideDurations, + DateMarker, + EventSegUiInteractionState, + memoize, + PositionCache, + ScrollResponder, + ScrollRequest, + DateRange, + DateProfile, + DayTableCell, + Hit, + DateComponent, +} from '@fullcalendar/core/internal' +import { + createElement, + VNode, +} from '@fullcalendar/core/preact' +import { TimeColsSlats } from './TimeColsSlats.js' +import { TimeSlatMeta } from './time-slat-meta.js' +import { TimeColsContent } from './TimeColsContent.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { TimeColsSeg } from './TimeColsSeg.js' + +export interface TimeColsProps { + cells: DayTableCell[] + dateProfile: DateProfile + slotDuration: Duration + nowDate: DateMarker + todayRange: DateRange + businessHourSegs: TimeColsSeg[] + bgEventSegs: TimeColsSeg[] + fgEventSegs: TimeColsSeg[] + dateSelectionSegs: TimeColsSeg[] + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + tableColGroupNode: VNode + tableMinWidth: CssDimValue + clientWidth: number | null + clientHeight: number | null + expandRows: boolean + nowIndicatorSegs: TimeColsSeg[] + onScrollTopRequest?: (scrollTop: number) => void + forPrint: boolean + axis: boolean + slatMetas: TimeSlatMeta[] + onSlatCoords?: (slatCoords: TimeColsSlatsCoords) => void + isHitComboAllowed?: (hit0: Hit, hit1: Hit) => boolean +} + +interface TimeColsState { + slatCoords: TimeColsSlatsCoords | null +} + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ + +export class TimeCols extends DateComponent<TimeColsProps, TimeColsState> { + private processSlotOptions = memoize(processSlotOptions) + private scrollResponder: ScrollResponder + private colCoords: PositionCache + + state = { + slatCoords: null, + } + + render() { + let { props, state } = this + + return ( + <div + className="fc-timegrid-body" + ref={this.handleRootEl} + style={{ + // these props are important to give this wrapper correct dimensions for interactions + // TODO: if we set it here, can we avoid giving to inner tables? + width: props.clientWidth, + minWidth: props.tableMinWidth, + }} + > + <TimeColsSlats + axis={props.axis} + dateProfile={props.dateProfile} + slatMetas={props.slatMetas} + clientWidth={props.clientWidth} + minHeight={props.expandRows ? props.clientHeight : ''} + tableMinWidth={props.tableMinWidth} + tableColGroupNode={props.axis ? props.tableColGroupNode : null /* axis depends on the colgroup's shrinking */} + onCoords={this.handleSlatCoords} + /> + <TimeColsContent + cells={props.cells} + axis={props.axis} + dateProfile={props.dateProfile} + businessHourSegs={props.businessHourSegs} + bgEventSegs={props.bgEventSegs} + fgEventSegs={props.fgEventSegs} + dateSelectionSegs={props.dateSelectionSegs} + eventSelection={props.eventSelection} + eventDrag={props.eventDrag} + eventResize={props.eventResize} + todayRange={props.todayRange} + nowDate={props.nowDate} + nowIndicatorSegs={props.nowIndicatorSegs} + clientWidth={props.clientWidth} + tableMinWidth={props.tableMinWidth} + tableColGroupNode={props.tableColGroupNode} + slatCoords={state.slatCoords} + onColCoords={this.handleColCoords} + forPrint={props.forPrint} + /> + </div> + ) + } + + handleRootEl = (el: HTMLElement | null) => { + if (el) { + this.context.registerInteractiveComponent(this, { + el, + isHitComboAllowed: this.props.isHitComboAllowed, + }) + } else { + this.context.unregisterInteractiveComponent(this) + } + } + + componentDidMount() { + this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest) + } + + componentDidUpdate(prevProps: TimeColsProps) { + this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile) + } + + componentWillUnmount() { + this.scrollResponder.detach() + } + + handleScrollRequest = (request: ScrollRequest) => { + let { onScrollTopRequest } = this.props + let { slatCoords } = this.state + + if (onScrollTopRequest && slatCoords) { + if (request.time) { + let top = slatCoords.computeTimeTop(request.time) + top = Math.ceil(top) // zoom can give weird floating-point values. rather scroll a little bit further + if (top) { + top += 1 // to overcome top border that slots beyond the first have. looks better + } + + onScrollTopRequest(top) + } + + return true + } + + return false + } + + handleColCoords = (colCoords: PositionCache | null) => { + this.colCoords = colCoords + } + + handleSlatCoords = (slatCoords: TimeColsSlatsCoords | null) => { + this.setState({ slatCoords }) + + if (this.props.onSlatCoords) { + this.props.onSlatCoords(slatCoords) + } + } + + queryHit(positionLeft: number, positionTop: number): Hit { + let { dateEnv, options } = this.context + let { colCoords } = this + let { dateProfile } = this.props + let { slatCoords } = this.state + let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration) + + let colIndex = colCoords.leftToIndex(positionLeft) + let slatIndex = slatCoords.positions.topToIndex(positionTop) + + if (colIndex != null && slatIndex != null) { + let cell = this.props.cells[colIndex] + let slatTop = slatCoords.positions.tops[slatIndex] + let slatHeight = slatCoords.positions.getHeight(slatIndex) + let partial = (positionTop - slatTop) / slatHeight // floating point number between 0 and 1 + let localSnapIndex = Math.floor(partial * snapsPerSlot) // the snap # relative to start of slat + let snapIndex = slatIndex * snapsPerSlot + localSnapIndex + + let dayDate = this.props.cells[colIndex].date + let time = addDurations( + dateProfile.slotMinTime, + multiplyDuration(snapDuration, snapIndex), + ) + + let start = dateEnv.add(dayDate, time) + let end = dateEnv.add(start, snapDuration) + + return { + dateProfile, + dateSpan: { + range: { start, end }, + allDay: false, + ...cell.extraDateSpan, + }, + dayEl: colCoords.els[colIndex], + rect: { + left: colCoords.lefts[colIndex], + right: colCoords.rights[colIndex], + top: slatTop, + bottom: slatTop + slatHeight, + }, + layer: 0, + } + } + + return null + } +} + +function processSlotOptions(slotDuration: Duration, snapDurationOverride: Duration | null) { + let snapDuration = snapDurationOverride || slotDuration + let snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration) + + if (snapsPerSlot === null) { + snapDuration = slotDuration + snapsPerSlot = 1 + // TODO: say warning? + } + + return { snapDuration, snapsPerSlot } +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsAxisCell.tsx b/fullcalendar-main/packages/timegrid/src/TimeColsAxisCell.tsx new file mode 100644 index 0000000..358f5b5 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsAxisCell.tsx @@ -0,0 +1,87 @@ +import { + SlotLabelContentArg, +} from '@fullcalendar/core' +import { + ViewContext, + createFormatter, + ViewContextType, + ContentContainer, +} from '@fullcalendar/core/internal' +import { + createElement, +} from '@fullcalendar/core/preact' +import { TimeSlatMeta } from './time-slat-meta.js' + +const DEFAULT_SLAT_LABEL_FORMAT = createFormatter({ + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'short', +}) + +export function TimeColsAxisCell(props: TimeSlatMeta) { + let classNames = [ + 'fc-timegrid-slot', + 'fc-timegrid-slot-label', + props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor', + ] + + return ( + <ViewContextType.Consumer> + {(context: ViewContext) => { + if (!props.isLabeled) { + return ( + <td className={classNames.join(' ')} data-time={props.isoTimeStr} /> + ) + } + + let { dateEnv, options, viewApi } = context + let labelFormat = // TODO: fully pre-parse + options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT : + Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) : + createFormatter(options.slotLabelFormat) + + let renderProps: SlotLabelContentArg = { + level: 0, + time: props.time, + date: dateEnv.toDate(props.date), + view: viewApi, + text: dateEnv.format(props.date, labelFormat), + } + + return ( + <ContentContainer + elTag="td" + elClasses={classNames} + elAttrs={{ + 'data-time': props.isoTimeStr, + }} + renderProps={renderProps} + generatorName="slotLabelContent" + customGenerator={options.slotLabelContent} + defaultGenerator={renderInnerContent} + classNameGenerator={options.slotLabelClassNames} + didMount={options.slotLabelDidMount} + willUnmount={options.slotLabelWillUnmount} + > + {(InnerContent) => ( + <div className="fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame"> + <InnerContent + elTag="div" + elClasses={[ + 'fc-timegrid-slot-label-cushion', + 'fc-scrollgrid-shrink-cushion', + ]} + /> + </div> + )} + </ContentContainer> + ) + }} + </ViewContextType.Consumer> + ) +} + +function renderInnerContent(props) { // TODO: add types + return props.text +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsContent.tsx b/fullcalendar-main/packages/timegrid/src/TimeColsContent.tsx new file mode 100644 index 0000000..eeabdb9 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsContent.tsx @@ -0,0 +1,160 @@ +import { CssDimValue } from '@fullcalendar/core' +import { + BaseComponent, + EventSegUiInteractionState, + DateMarker, + RefMap, + PositionCache, + memoize, + DateRange, + NowIndicatorContainer, + DateProfile, + DayTableCell, +} from '@fullcalendar/core/internal' +import { + createElement, + createRef, + VNode, +} from '@fullcalendar/core/preact' +import { TimeColsSeg, splitSegsByCol, splitInteractionByCol } from './TimeColsSeg.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { TimeCol } from './TimeCol.js' + +export interface TimeColsContentProps { + axis: boolean + cells: DayTableCell[] + dateProfile: DateProfile + nowDate: DateMarker + todayRange: DateRange + businessHourSegs: TimeColsSeg[] + bgEventSegs: TimeColsSeg[] + fgEventSegs: TimeColsSeg[] + dateSelectionSegs: TimeColsSeg[] + eventSelection: string + eventDrag: EventSegUiInteractionState | null + eventResize: EventSegUiInteractionState | null + nowIndicatorSegs: TimeColsSeg[] + clientWidth: number | null + tableMinWidth: CssDimValue + tableColGroupNode: VNode + slatCoords: TimeColsSlatsCoords + onColCoords?: (colCoords: PositionCache) => void + forPrint: boolean +} + +export class TimeColsContent extends BaseComponent<TimeColsContentProps> { // TODO: rename + private splitFgEventSegs = memoize(splitSegsByCol) + private splitBgEventSegs = memoize(splitSegsByCol) + private splitBusinessHourSegs = memoize(splitSegsByCol) + private splitNowIndicatorSegs = memoize(splitSegsByCol) + private splitDateSelectionSegs = memoize(splitSegsByCol) + private splitEventDrag = memoize(splitInteractionByCol) + private splitEventResize = memoize(splitInteractionByCol) + private rootElRef = createRef<HTMLDivElement>() + private cellElRefs = new RefMap<HTMLTableCellElement>() + + render() { + let { props, context } = this + let nowIndicatorTop = + context.options.nowIndicator && + props.slatCoords && + props.slatCoords.safeComputeTop(props.nowDate) // might return void + + let colCnt = props.cells.length + let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, colCnt) + let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, colCnt) + let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, colCnt) + let nowIndicatorSegsByRow = this.splitNowIndicatorSegs(props.nowIndicatorSegs, colCnt) + let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, colCnt) + let eventDragByRow = this.splitEventDrag(props.eventDrag, colCnt) + let eventResizeByRow = this.splitEventResize(props.eventResize, colCnt) + + return ( + <div className="fc-timegrid-cols" ref={this.rootElRef}> + <table + role="presentation" + style={{ + minWidth: props.tableMinWidth, + width: props.clientWidth, + }} + > + {props.tableColGroupNode} + <tbody role="presentation"> + <tr role="row"> + {props.axis && ( + <td aria-hidden className="fc-timegrid-col fc-timegrid-axis"> + <div className="fc-timegrid-col-frame"> + <div className="fc-timegrid-now-indicator-container"> + {typeof nowIndicatorTop === 'number' && ( + <NowIndicatorContainer + elClasses={['fc-timegrid-now-indicator-arrow']} + elStyle={{ top: nowIndicatorTop }} + isAxis + date={props.nowDate} + /> + )} + </div> + </div> + </td> + )} + {props.cells.map((cell, i) => ( + <TimeCol + key={cell.key} + elRef={this.cellElRefs.createRef(cell.key)} + dateProfile={props.dateProfile} + date={cell.date} + nowDate={props.nowDate} + todayRange={props.todayRange} + extraRenderProps={cell.extraRenderProps} + extraDataAttrs={cell.extraDataAttrs} + extraClassNames={cell.extraClassNames} + extraDateSpan={cell.extraDateSpan} + fgEventSegs={fgEventSegsByRow[i]} + bgEventSegs={bgEventSegsByRow[i]} + businessHourSegs={businessHourSegsByRow[i]} + nowIndicatorSegs={nowIndicatorSegsByRow[i]} + dateSelectionSegs={dateSelectionSegsByRow[i]} + eventDrag={eventDragByRow[i]} + eventResize={eventResizeByRow[i]} + slatCoords={props.slatCoords} + eventSelection={props.eventSelection} + forPrint={props.forPrint} + /> + ))} + </tr> + </tbody> + </table> + </div> + ) + } + + componentDidMount() { + this.updateCoords() + } + + componentDidUpdate() { + this.updateCoords() + } + + updateCoords() { + let { props } = this + + if ( + props.onColCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + props.onColCoords( + new PositionCache( + this.rootElRef.current, + collectCellEls(this.cellElRefs.currentMap, props.cells), + true, // horizontal + false, + ), + ) + } + } +} + +function collectCellEls(elMap: { [key: string]: HTMLElement }, cells: DayTableCell[]) { + return cells.map((cell) => elMap[cell.key]) +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsSeg.ts b/fullcalendar-main/packages/timegrid/src/TimeColsSeg.ts new file mode 100644 index 0000000..50df28b --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsSeg.ts @@ -0,0 +1,50 @@ +import { DateMarker, Seg, EventSegUiInteractionState } from '@fullcalendar/core/internal' + +// JUST A DATA STRUCTURE, not a component + +export interface TimeColsSeg extends Seg { + col: number + start: DateMarker + end: DateMarker +} + +export function splitSegsByCol(segs: TimeColsSeg[] | null, colCnt: number) { // can be given null/undefined! + let segsByCol: TimeColsSeg[][] = [] + let i + + for (i = 0; i < colCnt; i += 1) { + segsByCol.push([]) + } + + if (segs) { + for (i = 0; i < segs.length; i += 1) { + segsByCol[segs[i].col].push(segs[i]) + } + } + + return segsByCol +} + +export function splitInteractionByCol(ui: EventSegUiInteractionState | null, colCnt: number) { + let byRow: EventSegUiInteractionState[] = [] + + if (!ui) { + for (let i = 0; i < colCnt; i += 1) { + byRow[i] = null + } + } else { + for (let i = 0; i < colCnt; i += 1) { + byRow[i] = { + affectedInstances: ui.affectedInstances, + isEvent: ui.isEvent, + segs: [], + } + } + + for (let seg of ui.segs) { + byRow[seg.col].segs.push(seg) + } + } + + return byRow +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsSlats.tsx b/fullcalendar-main/packages/timegrid/src/TimeColsSlats.tsx new file mode 100644 index 0000000..53b1759 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsSlats.tsx @@ -0,0 +1,107 @@ +import { CssDimValue } from '@fullcalendar/core' +import { + BaseComponent, + RefMap, + PositionCache, + DateProfile, +} from '@fullcalendar/core/internal' +import { + createElement, + VNode, + createRef, +} from '@fullcalendar/core/preact' +import { TimeSlatMeta } from './time-slat-meta.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { TimeColsSlatsBody } from './TimeColsSlatsBody.js' + +export interface TimeColsSlatsProps extends TimeColsSlatsContentProps { + dateProfile: DateProfile + clientWidth: number | null + minHeight: CssDimValue + tableMinWidth: CssDimValue + tableColGroupNode: VNode + onCoords?: (coords: TimeColsSlatsCoords | null) => void +} + +interface TimeColsSlatsContentProps { + axis: boolean + slatMetas: TimeSlatMeta[] +} + +/* +for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. +*/ + +export class TimeColsSlats extends BaseComponent<TimeColsSlatsProps> { + private rootElRef = createRef<HTMLDivElement>() + private slatElRefs = new RefMap<HTMLTableRowElement>() + + render() { + let { props, context } = this + + return ( + <div ref={this.rootElRef} className="fc-timegrid-slots"> + <table + aria-hidden + className={context.theme.getClass('table')} + style={{ + minWidth: props.tableMinWidth, + width: props.clientWidth, + height: props.minHeight, + }} + > + {props.tableColGroupNode /* relies on there only being a single <col> for the axis */} + <TimeColsSlatsBody + slatElRefs={this.slatElRefs} + axis={props.axis} + slatMetas={props.slatMetas} + /> + </table> + </div> + ) + } + + componentDidMount() { + this.updateSizing() + } + + componentDidUpdate() { + this.updateSizing() + } + + componentWillUnmount() { + if (this.props.onCoords) { + this.props.onCoords(null) + } + } + + updateSizing() { + let { context, props } = this + + if ( + props.onCoords && + props.clientWidth !== null // means sizing has stabilized + ) { + let rootEl = this.rootElRef.current + + if (rootEl.offsetHeight) { // not hidden by css + props.onCoords( + new TimeColsSlatsCoords( + new PositionCache( + this.rootElRef.current, + collectSlatEls(this.slatElRefs.currentMap, props.slatMetas), + false, + true, // vertical + ), + this.props.dateProfile, + context.options.slotDuration, + ), + ) + } + } + } +} + +function collectSlatEls(elMap: { [key: string]: HTMLElement }, slatMetas: TimeSlatMeta[]) { + return slatMetas.map((slatMeta) => elMap[slatMeta.key]) +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsSlatsBody.tsx b/fullcalendar-main/packages/timegrid/src/TimeColsSlatsBody.tsx new file mode 100644 index 0000000..0ff4509 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsSlatsBody.tsx @@ -0,0 +1,67 @@ +import { + SlotLaneContentArg, +} from '@fullcalendar/core' +import { + BaseComponent, + ContentContainer, + RefMap, +} from '@fullcalendar/core/internal' +import { + createElement, +} from '@fullcalendar/core/preact' +import { TimeColsAxisCell } from './TimeColsAxisCell.js' +import { TimeSlatMeta } from './time-slat-meta.js' + +export interface TimeColsSlatsBodyProps { + axis: boolean + slatMetas: TimeSlatMeta[] + slatElRefs: RefMap<HTMLTableRowElement> +} + +export class TimeColsSlatsBody extends BaseComponent<TimeColsSlatsBodyProps> { + render() { + let { props, context } = this + let { options } = context + let { slatElRefs } = props + + return ( + <tbody> + {props.slatMetas.map((slatMeta, i) => { + let renderProps: SlotLaneContentArg = { + time: slatMeta.time, + date: context.dateEnv.toDate(slatMeta.date), + view: context.viewApi, + } + + return ( + <tr + key={slatMeta.key} + ref={slatElRefs.createRef(slatMeta.key)} + > + {props.axis && ( + <TimeColsAxisCell {...slatMeta} /> + )} + <ContentContainer + elTag="td" + elClasses={[ + 'fc-timegrid-slot', + 'fc-timegrid-slot-lane', + !slatMeta.isLabeled && 'fc-timegrid-slot-minor', + ]} + elAttrs={{ + 'data-time': slatMeta.isoTimeStr, + }} + renderProps={renderProps} + generatorName="slotLaneContent" + customGenerator={options.slotLaneContent} + classNameGenerator={options.slotLaneClassNames} + didMount={options.slotLaneDidMount} + willUnmount={options.slotLaneWillUnmount} + /> + </tr> + ) + })} + </tbody> + ) + } +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsSlatsCoords.ts b/fullcalendar-main/packages/timegrid/src/TimeColsSlatsCoords.ts new file mode 100644 index 0000000..5d33a07 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsSlatsCoords.ts @@ -0,0 +1,77 @@ +import { Duration } from '@fullcalendar/core' +import { + PositionCache, + DateMarker, + startOfDay, + createDuration, + asRoughMs, + DateProfile, + rangeContainsMarker, +} from '@fullcalendar/core/internal' + +export class TimeColsSlatsCoords { + constructor( + public positions: PositionCache, + private dateProfile: DateProfile, + private slotDuration: Duration, + ) { + } + + safeComputeTop(date: DateMarker) { // TODO: DRY with computeDateTop + let { dateProfile } = this + + if (rangeContainsMarker(dateProfile.currentRange, date)) { + let startOfDayDate = startOfDay(date) + let timeMs = date.valueOf() - startOfDayDate.valueOf() + + if ( + timeMs >= asRoughMs(dateProfile.slotMinTime) && + timeMs < asRoughMs(dateProfile.slotMaxTime) + ) { + return this.computeTimeTop(createDuration(timeMs)) + } + } + + return null + } + + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop(when: DateMarker, startOfDayDate?: DateMarker) { + if (!startOfDayDate) { + startOfDayDate = startOfDay(when) + } + return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf())) + } + + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + // This is a makeshify way to compute the time-top. Assumes all slatMetas dates are uniform. + // Eventually allow computation with arbirary slat dates. + computeTimeTop(duration: Duration): number { + let { positions, dateProfile } = this + let len = positions.els.length + + // floating-point value of # of slots covered + let slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.slotMinTime)) / asRoughMs(this.slotDuration) + let slatIndex + let slatRemainder + + // compute a floating-point number for how many slats should be progressed through. + // from 0 to number of slats (inclusive) + // constrained because slotMinTime/slotMaxTime might be customized. + slatCoverage = Math.max(0, slatCoverage) + slatCoverage = Math.min(len, slatCoverage) + + // an integer index of the furthest whole slat + // from 0 to number slats (*exclusive*, so len-1) + slatIndex = Math.floor(slatCoverage) + slatIndex = Math.min(slatIndex, len - 1) + + // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. + // could be 1.0 if slatCoverage is covering *all* the slots + slatRemainder = slatCoverage - slatIndex + + return positions.tops[slatIndex] + + positions.getHeight(slatIndex) * slatRemainder + } +} diff --git a/fullcalendar-main/packages/timegrid/src/TimeColsView.tsx b/fullcalendar-main/packages/timegrid/src/TimeColsView.tsx new file mode 100644 index 0000000..55a30fc --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/TimeColsView.tsx @@ -0,0 +1,432 @@ +import { CssDimValue, AllDayContentArg } from '@fullcalendar/core' +import { + diffDays, + SimpleScrollGridSection, + SimpleScrollGrid, + ChunkContentCallbackArgs, + ScrollGridSectionConfig, + buildNavLinkAttrs, + ViewContainer, + WeekNumberContainer, + DateComponent, + ViewProps, + renderScrollShim, + getStickyHeaderDates, + getStickyFooterScrollbar, + createFormatter, + NowTimer, + DateMarker, + NowIndicatorContainer, + ContentContainer, +} from '@fullcalendar/core/internal' +import { + createElement, + createRef, + VNode, + RefObject, + ComponentChild, +} from '@fullcalendar/core/preact' +import { AllDaySplitter } from './AllDaySplitter.js' +import { TimeSlatMeta } from './time-slat-meta.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { TimeBodyAxis } from './TimeBodyAxis.js' + +const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' }) +const AUTO_ALL_DAY_MAX_EVENT_ROWS = 5 + +/* An abstract class for all timegrid-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeCols subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +interface TimeColsViewState { + slatCoords: TimeColsSlatsCoords | null +} + +export abstract class TimeColsView extends DateComponent<ViewProps, TimeColsViewState> { + protected allDaySplitter = new AllDaySplitter() // for use by subclasses + + protected headerElRef: RefObject<HTMLTableCellElement> = createRef<HTMLTableCellElement>() + private rootElRef: RefObject<HTMLElement> = createRef<HTMLElement>() + private scrollerElRef: RefObject<HTMLDivElement> = createRef<HTMLDivElement>() + + state = { + slatCoords: null, + } + + // rendering + // ---------------------------------------------------------------------------------------------------- + + renderSimpleLayout( + headerRowContent: VNode | null, + allDayContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null, + timeContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null, + ) { + let { context, props } = this + let sections: SimpleScrollGridSection[] = [] + let stickyHeaderDates = getStickyHeaderDates(context.options) + + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + chunk: { + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + }) + } + + if (allDayContent) { + sections.push({ + type: 'body', + key: 'all-day', + chunk: { content: allDayContent }, + }) + sections.push({ + type: 'body', + key: 'all-day-divider', + outerContent: ( // TODO: rename to cellContent so don't need to define <tr>? + <tr role="presentation" className="fc-scrollgrid-section"> + <td + className={'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded')} + /> + </tr> + ), + }) + } + + sections.push({ + type: 'body', + key: 'body', + liquid: true, + expandRows: Boolean(context.options.expandRows), + chunk: { + scrollerElRef: this.scrollerElRef, + content: timeContent, + }, + }) + + return ( + <ViewContainer elRef={this.rootElRef} elClasses={['fc-timegrid']} viewSpec={context.viewSpec}> + <SimpleScrollGrid + liquid={!props.isHeightAuto && !props.forPrint} + collapsibleWidth={props.forPrint} + cols={[{ width: 'shrink' }]} + sections={sections} + /> + </ViewContainer> + ) + } + + renderHScrollLayout( + headerRowContent: VNode | null, + allDayContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null, + timeContent: ((contentArg: ChunkContentCallbackArgs) => VNode) | null, + colCnt: number, + dayMinWidth: number, + slatMetas: TimeSlatMeta[], + slatCoords: TimeColsSlatsCoords | null, // yuck + ) { + let ScrollGrid = this.context.pluginHooks.scrollGridImpl + + if (!ScrollGrid) { + throw new Error('No ScrollGrid implementation') + } + + let { context, props } = this + let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options) + let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options) + let sections: ScrollGridSectionConfig[] = [] + + if (headerRowContent) { + sections.push({ + type: 'header', + key: 'header', + isSticky: stickyHeaderDates, + syncRowHeights: true, + chunks: [ + { + key: 'axis', + rowContent: (arg: ChunkContentCallbackArgs) => ( + <tr role="presentation"> + {this.renderHeadAxis('day', arg.rowSyncHeights[0])} + </tr> + ), + }, + { + key: 'cols', + elRef: this.headerElRef, + tableClassName: 'fc-col-header', + rowContent: headerRowContent, + }, + ], + }) + } + + if (allDayContent) { + sections.push({ + type: 'body', + key: 'all-day', + syncRowHeights: true, + chunks: [ + { + key: 'axis', + rowContent: (contentArg: ChunkContentCallbackArgs) => ( + <tr role="presentation"> + {this.renderTableRowAxis(contentArg.rowSyncHeights[0])} + </tr> + ), + }, + { + key: 'cols', + content: allDayContent, + }, + ], + }) + sections.push({ + key: 'all-day-divider', + type: 'body', + outerContent: ( // TODO: rename to cellContent so don't need to define <tr>? + <tr role="presentation" className="fc-scrollgrid-section"> + <td + colSpan={2} + className={'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded')} + /> + </tr> + ), + }) + } + + let isNowIndicator = context.options.nowIndicator + + sections.push({ + type: 'body', + key: 'body', + liquid: true, + expandRows: Boolean(context.options.expandRows), + chunks: [ + { + key: 'axis', + content: (arg) => ( + // TODO: make this now-indicator arrow more DRY with TimeColsContent + <div className="fc-timegrid-axis-chunk"> + <table aria-hidden style={{ height: arg.expandRows ? arg.clientHeight : '' }}> + {arg.tableColGroupNode} + <tbody> + <TimeBodyAxis slatMetas={slatMetas} /> + </tbody> + </table> + <div className="fc-timegrid-now-indicator-container"> + <NowTimer unit={isNowIndicator ? 'minute' : 'day' /* hacky */}> + {(nowDate: DateMarker) => { + let nowIndicatorTop = + isNowIndicator && + slatCoords && + slatCoords.safeComputeTop(nowDate) // might return void + + if (typeof nowIndicatorTop === 'number') { + return ( + <NowIndicatorContainer + elClasses={['fc-timegrid-now-indicator-arrow']} + elStyle={{ top: nowIndicatorTop }} + isAxis + date={nowDate} + /> + ) + } + + return null + }} + </NowTimer> + </div> + </div> + ), + }, + { + key: 'cols', + scrollerElRef: this.scrollerElRef, + content: timeContent, + }, + ], + }) + + if (stickyFooterScrollbar) { + sections.push({ + key: 'footer', + type: 'footer', + isSticky: true, + chunks: [ + { + key: 'axis', + content: renderScrollShim, + }, + { + key: 'cols', + content: renderScrollShim, + }, + ], + }) + } + + return ( + <ViewContainer elRef={this.rootElRef} elClasses={['fc-timegrid']} viewSpec={context.viewSpec}> + <ScrollGrid + liquid={!props.isHeightAuto && !props.forPrint} + forPrint={props.forPrint} + collapsibleWidth={false} + colGroups={[ + { width: 'shrink', cols: [{ width: 'shrink' }] }, // TODO: allow no specify cols + { cols: [{ span: colCnt, minWidth: dayMinWidth }] }, + ]} + sections={sections} + /> + </ViewContainer> + ) + } + + handleScrollTopRequest = (scrollTop: number) => { + let scrollerEl = this.scrollerElRef.current + + if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer + scrollerEl.scrollTop = scrollTop + } + } + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + getAllDayMaxEventProps() { + let { dayMaxEvents, dayMaxEventRows } = this.context.options + + if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto? + dayMaxEvents = undefined + dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS // make sure "auto" goes to a real number + } + + return { dayMaxEvents, dayMaxEventRows } + } + + /* Header Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + + renderHeadAxis = (rowKey: 'day' | string, frameHeight: CssDimValue = '') => { + let { options } = this.context + let { dateProfile } = this.props + let range = dateProfile.renderRange + let dayCnt = diffDays(range.start, range.end) + + // only do in day views (to avoid doing in week views that dont need it) + let navLinkAttrs = (dayCnt === 1) + ? buildNavLinkAttrs(this.context, range.start, 'week') + : {} + + if (options.weekNumbers && rowKey === 'day') { + return ( + <WeekNumberContainer + elTag="th" + elClasses={[ + 'fc-timegrid-axis', + 'fc-scrollgrid-shrink', + ]} + elAttrs={{ + 'aria-hidden': true, + }} + date={range.start} + defaultFormat={DEFAULT_WEEK_NUM_FORMAT} + > + {(InnerContent) => ( + <div + className={[ + 'fc-timegrid-axis-frame', + 'fc-scrollgrid-shrink-frame', + 'fc-timegrid-axis-frame-liquid', + ].join(' ')} + style={{ height: frameHeight }} + > + <InnerContent + elTag="a" + elClasses={[ + 'fc-timegrid-axis-cushion', + 'fc-scrollgrid-shrink-cushion', + 'fc-scrollgrid-sync-inner', + ]} + elAttrs={navLinkAttrs} + /> + </div> + )} + </WeekNumberContainer> + ) + } + + return ( + <th aria-hidden className="fc-timegrid-axis"> + <div className="fc-timegrid-axis-frame" style={{ height: frameHeight }} /> + </th> + ) + } + + /* Table Component Render Methods + ------------------------------------------------------------------------------------------------------------------*/ + + // only a one-way height sync. we don't send the axis inner-content height to the DayGrid, + // but DayGrid still needs to have classNames on inner elements in order to measure. + renderTableRowAxis = (rowHeight?: number) => { + let { options, viewApi } = this.context + let renderProps: AllDayContentArg = { + text: options.allDayText, + view: viewApi, + } + + return ( + // TODO: make reusable hook. used in list view too + <ContentContainer + elTag="td" + elClasses={[ + 'fc-timegrid-axis', + 'fc-scrollgrid-shrink', + ]} + elAttrs={{ + 'aria-hidden': true, + }} + renderProps={renderProps} + generatorName="allDayContent" + customGenerator={options.allDayContent} + defaultGenerator={renderAllDayInner} + classNameGenerator={options.allDayClassNames} + didMount={options.allDayDidMount} + willUnmount={options.allDayWillUnmount} + > + {(InnerContent) => ( + <div + className={[ + 'fc-timegrid-axis-frame', + 'fc-scrollgrid-shrink-frame', + rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : '', + ].join(' ')} + style={{ height: rowHeight }} + > + <InnerContent + elTag="span" + elClasses={[ + 'fc-timegrid-axis-cushion', + 'fc-scrollgrid-shrink-cushion', + 'fc-scrollgrid-sync-inner', + ]} + /> + </div> + )} + </ContentContainer> + ) + } + + handleSlatCoords = (slatCoords: TimeColsSlatsCoords) => { + this.setState({ slatCoords }) + } +} + +function renderAllDayInner(renderProps: AllDayContentArg): ComponentChild { + return renderProps.text +} diff --git a/fullcalendar-main/packages/timegrid/src/ambient.ts b/fullcalendar-main/packages/timegrid/src/ambient.ts new file mode 100644 index 0000000..751ca70 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/ambient.ts @@ -0,0 +1,10 @@ +import { OPTION_REFINERS } from './options-refiners.js' + +// all dependencies except core +import '@fullcalendar/daygrid' + +type ExtraOptionRefiners = typeof OPTION_REFINERS + +declare module '@fullcalendar/core/internal' { + interface BaseOptionRefiners extends ExtraOptionRefiners {} +} diff --git a/fullcalendar-main/packages/timegrid/src/event-placement.ts b/fullcalendar-main/packages/timegrid/src/event-placement.ts new file mode 100644 index 0000000..6a55fe7 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/event-placement.ts @@ -0,0 +1,82 @@ +import { + SegSpan, + SegEntry, + SegEntryGroup, + DateMarker, +} from '@fullcalendar/core/internal' +import { TimeColsSeg } from './TimeColsSeg.js' +import { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' +import { SegWebRect, buildPositioning } from './seg-web.js' + +// public interface +// ------------------------------------------------------------------------------------------ + +export interface TimeColFgSegPlacement { + seg: TimeColsSeg + rect: SegWebRect | null +} + +export function computeSegVCoords( + segs: TimeColsSeg[], + colDate: DateMarker, + slatCoords: TimeColsSlatsCoords = null, + eventMinHeight: number = 0, // might be null/undefined :( +): SegSpan[] { + let vcoords: SegSpan[] = [] + + if (slatCoords) { + for (let i = 0; i < segs.length; i += 1) { + let seg = segs[i] + let spanStart = slatCoords.computeDateTop(seg.start, colDate) + let spanEnd = Math.max( + spanStart + (eventMinHeight || 0), // :( + slatCoords.computeDateTop(seg.end, colDate), + ) + vcoords.push({ + start: Math.round(spanStart), // for barely-overlapping collisions + end: Math.round(spanEnd), // + }) + } + } + + return vcoords +} + +export function computeFgSegPlacements( + segs: TimeColsSeg[], + segVCoords: SegSpan[], // might not have for every seg + eventOrderStrict?: boolean, + eventMaxStack?: number, +): { segPlacements: TimeColFgSegPlacement[], hiddenGroups: SegEntryGroup[] } { + let segInputs: SegEntry[] = [] + let dumbSegs: TimeColsSeg[] = [] // segs without coords + + for (let i = 0; i < segs.length; i += 1) { + let vcoords = segVCoords[i] + if (vcoords) { + segInputs.push({ + index: i, + thickness: 1, + span: vcoords, + }) + } else { + dumbSegs.push(segs[i]) + } + } + + let { segRects, hiddenGroups } = buildPositioning(segInputs, eventOrderStrict, eventMaxStack) + let segPlacements: TimeColFgSegPlacement[] = [] + + for (let segRect of segRects) { + segPlacements.push({ + seg: segs[segRect.index], + rect: segRect, + }) + } + + for (let dumbSeg of dumbSegs) { + segPlacements.push({ seg: dumbSeg, rect: null }) + } + + return { segPlacements, hiddenGroups } +} diff --git a/fullcalendar-main/packages/timegrid/src/index.css b/fullcalendar-main/packages/timegrid/src/index.css new file mode 100644 index 0000000..82d691f --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/index.css @@ -0,0 +1,10 @@ + +@import '../../core/src/styles/mixins'; +@import './styles/constants'; + +@import './styles/v-event'; +@import './styles/timegrid'; +@import './styles/timegrid-slots'; +@import './styles/timegrid-cols'; +@import './styles/timegrid-event'; +@import './styles/timegrid-now-indicator'; diff --git a/fullcalendar-main/packages/timegrid/src/index.global.ts b/fullcalendar-main/packages/timegrid/src/index.global.ts new file mode 100644 index 0000000..c70b8e6 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/index.global.ts @@ -0,0 +1,8 @@ +import { globalPlugins } from '@fullcalendar/core' +import plugin from './index.js' +import * as Internal from './internal.js' + +globalPlugins.push(plugin) + +export { plugin as default, Internal } +export * from './index.js' diff --git a/fullcalendar-main/packages/timegrid/src/index.ts b/fullcalendar-main/packages/timegrid/src/index.ts new file mode 100644 index 0000000..7602f49 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/index.ts @@ -0,0 +1,28 @@ +import { createPlugin, PluginDef } from '@fullcalendar/core' +import { DayTimeColsView } from './DayTimeColsView.js' +import { OPTION_REFINERS } from './options-refiners.js' +import './ambient.js' +import './index.css' + +export default createPlugin({ + name: '<%= pkgName %>', + initialView: 'timeGridWeek', + optionRefiners: OPTION_REFINERS, + views: { + timeGrid: { + component: DayTimeColsView, + usesMinMaxTime: true, // indicates that slotMinTime/slotMaxTime affects rendering + allDaySlot: true, + slotDuration: '00:30:00', + slotEventOverlap: true, // a bad name. confused with overlap/constraint system + }, + timeGridDay: { + type: 'timeGrid', + duration: { days: 1 }, + }, + timeGridWeek: { + type: 'timeGrid', + duration: { weeks: 1 }, + }, + }, +}) as PluginDef diff --git a/fullcalendar-main/packages/timegrid/src/internal.ts b/fullcalendar-main/packages/timegrid/src/internal.ts new file mode 100644 index 0000000..9c96466 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/internal.ts @@ -0,0 +1,10 @@ +import './index.css' + +export { TimeColsView } from './TimeColsView.js' +export { DayTimeColsView, buildTimeColsModel } from './DayTimeColsView.js' +export { TimeColsSeg } from './TimeColsSeg.js' +export { DayTimeCols, buildDayRanges } from './DayTimeCols.js' +export { DayTimeColsSlicer } from './DayTimeColsSlicer.js' +export { TimeCols } from './TimeCols.js' +export { TimeSlatMeta, buildSlatMetas } from './time-slat-meta.js' +export { TimeColsSlatsCoords } from './TimeColsSlatsCoords.js' diff --git a/fullcalendar-main/packages/timegrid/src/options-refiners.ts b/fullcalendar-main/packages/timegrid/src/options-refiners.ts new file mode 100644 index 0000000..9aca8b3 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/options-refiners.ts @@ -0,0 +1,3 @@ +export const OPTION_REFINERS = { + allDaySlot: Boolean, +} diff --git a/fullcalendar-main/packages/timegrid/src/seg-web.ts b/fullcalendar-main/packages/timegrid/src/seg-web.ts new file mode 100644 index 0000000..07775d7 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/seg-web.ts @@ -0,0 +1,225 @@ +import { + SegEntry, + SegHierarchy, + SegRect, + buildEntryKey, + getEntrySpanEnd, + binarySearch, + SegEntryGroup, + groupIntersectingEntries, +} from '@fullcalendar/core/internal' + +interface SegNode extends SegEntry { + nextLevelNodes: SegNode[] // with highest-pressure first +} + +type SegNodeAndPressure = [ SegNode, number ] + +interface SegSiblingRange { // will ALWAYS have span of 1 or more items. if not, will be null + level: number + lateralStart: number + lateralEnd: number +} + +export interface SegWebRect extends SegRect { + stackDepth: number + stackForward: number +} + +// segInputs assumed sorted +export function buildPositioning( + segInputs: SegEntry[], + strictOrder?: boolean, + maxStackCnt?: number, +): { segRects: SegWebRect[], hiddenGroups: SegEntryGroup[] } { + let hierarchy = new SegHierarchy() + if (strictOrder != null) { + hierarchy.strictOrder = strictOrder + } + if (maxStackCnt != null) { + hierarchy.maxStackCnt = maxStackCnt + } + + let hiddenEntries = hierarchy.addSegs(segInputs) + let hiddenGroups = groupIntersectingEntries(hiddenEntries) + + let web = buildWeb(hierarchy) + web = stretchWeb(web, 1) // all levelCoords/thickness will have 0.0-1.0 + let segRects = webToRects(web) + + return { segRects, hiddenGroups } +} + +function buildWeb(hierarchy: SegHierarchy): SegNode[] { + const { entriesByLevel } = hierarchy + + const buildNode = cacheable( + (level: number, lateral: number) => level + ':' + lateral, + (level: number, lateral: number): SegNodeAndPressure => { + let siblingRange = findNextLevelSegs(hierarchy, level, lateral) + let nextLevelRes = buildNodes(siblingRange, buildNode) + let entry = entriesByLevel[level][lateral] + + return [ + { ...entry, nextLevelNodes: nextLevelRes[0] }, + entry.thickness + nextLevelRes[1], // the pressure builds + ] + }, + ) + + return buildNodes( + entriesByLevel.length + ? { level: 0, lateralStart: 0, lateralEnd: entriesByLevel[0].length } + : null, + buildNode, + )[0] +} + +function buildNodes( + siblingRange: SegSiblingRange | null, + buildNode: (level: number, lateral: number) => SegNodeAndPressure, +): [SegNode[], number] { // number is maxPressure + if (!siblingRange) { + return [[], 0] + } + + let { level, lateralStart, lateralEnd } = siblingRange + let lateral = lateralStart + let pairs: SegNodeAndPressure[] = [] + + while (lateral < lateralEnd) { + pairs.push(buildNode(level, lateral)) + lateral += 1 + } + + pairs.sort(cmpDescPressures) + + return [ + pairs.map(extractNode), + pairs[0][1], // first item's pressure + ] +} + +function cmpDescPressures(a: SegNodeAndPressure, b: SegNodeAndPressure) { // sort pressure high -> low + return b[1] - a[1] +} + +function extractNode(a: SegNodeAndPressure): SegNode { + return a[0] +} + +function findNextLevelSegs(hierarchy: SegHierarchy, subjectLevel: number, subjectLateral: number): SegSiblingRange | null { + let { levelCoords, entriesByLevel } = hierarchy + let subjectEntry = entriesByLevel[subjectLevel][subjectLateral] + let afterSubject = levelCoords[subjectLevel] + subjectEntry.thickness + let levelCnt = levelCoords.length + let level = subjectLevel + + // skip past levels that are too high up + for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1) ; // do nothing + + for (; level < levelCnt; level += 1) { + let entries = entriesByLevel[level] + let entry: SegEntry + let searchIndex = binarySearch(entries, subjectEntry.span.start, getEntrySpanEnd) + let lateralStart = searchIndex[0] + searchIndex[1] // if exact match (which doesn't collide), go to next one + let lateralEnd = lateralStart + + while ( // loop through entries that horizontally intersect + (entry = entries[lateralEnd]) && // but not past the whole seg list + entry.span.start < subjectEntry.span.end + ) { lateralEnd += 1 } + + if (lateralStart < lateralEnd) { + return { level, lateralStart, lateralEnd } + } + } + + return null +} + +function stretchWeb(topLevelNodes: SegNode[], totalThickness: number): SegNode[] { + const stretchNode = cacheable( + (node: SegNode, startCoord: number, prevThickness: number) => buildEntryKey(node), + (node: SegNode, startCoord: number, prevThickness: number): [number, SegNode] => { // [startCoord, node] + let { nextLevelNodes, thickness } = node + let allThickness = thickness + prevThickness + let thicknessFraction = thickness / allThickness + let endCoord: number + let newChildren: SegNode[] = [] + + if (!nextLevelNodes.length) { + endCoord = totalThickness + } else { + for (let childNode of nextLevelNodes) { + if (endCoord === undefined) { + let res = stretchNode(childNode, startCoord, allThickness) + endCoord = res[0] + newChildren.push(res[1]) + } else { + let res = stretchNode(childNode, endCoord, 0) + newChildren.push(res[1]) + } + } + } + + let newThickness = (endCoord - startCoord) * thicknessFraction + return [endCoord - newThickness, { + ...node, + thickness: newThickness, + nextLevelNodes: newChildren, + }] + }, + ) + + return topLevelNodes.map((node: SegNode) => stretchNode(node, 0, 0)[1]) +} + +// not sorted in any particular order +function webToRects(topLevelNodes: SegNode[]): SegWebRect[] { + let rects: SegWebRect[] = [] + + const processNode = cacheable( + (node: SegNode, levelCoord: number, stackDepth: number) => buildEntryKey(node), + (node: SegNode, levelCoord: number, stackDepth: number) => { // returns forwardPressure + let rect = { + ...node, + levelCoord, + stackDepth, + stackForward: 0, // will assign after recursing + } as SegWebRect + rects.push(rect) + + return ( + rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1) + 1 + ) + }, + ) + + function processNodes(nodes: SegNode[], levelCoord: number, stackDepth: number) { // returns stackForward + let stackForward = 0 + for (let node of nodes) { + stackForward = Math.max(processNode(node, levelCoord, stackDepth), stackForward) + } + return stackForward + } + + processNodes(topLevelNodes, 0, 0) + return rects // TODO: sort rects by levelCoord to be consistent with toRects? +} + +// TODO: move to general util + +function cacheable<Args extends any[], Res>( + keyFunc: (...args: Args) => string, + workFunc: (...args: Args) => Res, +): ((...args: Args) => Res) { + const cache: { [key: string]: Res } = {} + + return (...args: Args) => { + let key = keyFunc(...args) + return (key in cache) + ? cache[key] + : (cache[key] = workFunc(...args)) + } +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/constants.css b/fullcalendar-main/packages/timegrid/src/styles/constants.css new file mode 100644 index 0000000..946cef6 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/constants.css @@ -0,0 +1,5 @@ + +$timegrid-slots-z: 1; +$timegrid-col-bg-z: 2; +$timegrid-col-fg-z: 3; +$timegrid-now-indicator-z: 4; diff --git a/fullcalendar-main/packages/timegrid/src/styles/timegrid-cols.css b/fullcalendar-main/packages/timegrid/src/styles/timegrid-cols.css new file mode 100644 index 0000000..fe7fb5a --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/timegrid-cols.css @@ -0,0 +1,94 @@ + +.fc { + + & .fc-timegrid-col { + &.fc-day-today { + background-color: var(--fc-today-bg-color); + } + } + + & .fc-timegrid-col-frame { + min-height: 100%; // liquid-hack is below + position: relative; + } + +} + +.fc-media-screen { + &.fc-liquid-hack { + & .fc-timegrid-col-frame { + @include liquid-absolute-override; + } + } + + & .fc-timegrid-cols { + position: absolute; // no z-index. children will decide and go above slots + top: 0; + left: 0; + right: 0; + bottom: 0; + + & > table { + height: 100%; + } + } + + & .fc-timegrid-col-bg, + & .fc-timegrid-col-events, + & .fc-timegrid-now-indicator-container { + position: absolute; + top: 0; + left: 0; + right: 0; + } +} + +.fc { + + // bg + + & .fc-timegrid-col-bg { + z-index: $timegrid-col-bg-z; + @include bg-z-indexes; + } + + & .fc-timegrid-bg-harness { + position: absolute; // top/bottom will be set by JS + left: 0; + right: 0; + } + + // fg events + // (the mirror segs are put into a separate container with same classname, + // and they must be after the normal seg container to appear at a higher z-index) + + & .fc-timegrid-col-events { + z-index: $timegrid-col-fg-z; + // child event segs have z-indexes that are scoped within this div + } + + // now indicator + + & .fc-timegrid-now-indicator-container { + bottom: 0; + overflow: hidden; // don't let overflow of lines/arrows cause unnecessary scrolling + // z-index is set on the individual elements + } + +} + +.fc-direction-ltr { + + & .fc-timegrid-col-events { + margin: 0 2.5% 0 2px; + } + +} + +.fc-direction-rtl { + + & .fc-timegrid-col-events { + margin: 0 2px 0 2.5%; + } + +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/timegrid-event.css b/fullcalendar-main/packages/timegrid/src/styles/timegrid-event.css new file mode 100644 index 0000000..62f9a8b --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/timegrid-event.css @@ -0,0 +1,78 @@ + +.fc-timegrid-event-harness { + position: absolute; // top/left/right/bottom will all be set by JS + + & > .fc-timegrid-event { + position: absolute; // absolute WITHIN the harness + top: 0; // for when not yet positioned + bottom: 0; // " + left: 0; + right: 0; + } +} + +.fc-timegrid-event-harness-inset .fc-timegrid-event, +.fc-timegrid-event.fc-event-mirror, +.fc-timegrid-more-link { + box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color); +} + +.fc-timegrid-event, +.fc-timegrid-more-link { // events need to be root + font-size: var(--fc-small-font-size); + border-radius: 3px; +} + +.fc-timegrid-event { // events need to be root + margin-bottom: 1px; // give some space from bottom + + & .fc-event-main { + padding: 1px 1px 0; + } + + & .fc-event-time { + white-space: nowrap; + font-size: var(--fc-small-font-size); + margin-bottom: 1px; + } +} + +.fc-timegrid-event-short { + & .fc-event-main-frame { + flex-direction: row; + overflow: hidden; + } + + & .fc-event-time:after { + content: '\00a0-\00a0'; // dash surrounded by non-breaking spaces + } + + & .fc-event-title { + font-size: var(--fc-small-font-size) + } +} + +.fc-timegrid-more-link { // does NOT inherit from fc-timegrid-event + position: absolute; + z-index: 9999; // hack + color: var(--fc-more-link-text-color); + background: var(--fc-more-link-bg-color); + cursor: pointer; + margin-bottom: 1px; // match space below fc-timegrid-event +} + +.fc-timegrid-more-link-inner { // has fc-sticky + padding: 3px 2px; + top: 0; +} + +.fc-direction-ltr { + & .fc-timegrid-more-link { + right: 0; + } +} +.fc-direction-rtl { + & .fc-timegrid-more-link { + left: 0; + } +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/timegrid-now-indicator.css b/fullcalendar-main/packages/timegrid/src/styles/timegrid-now-indicator.css new file mode 100644 index 0000000..8ef8d99 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/timegrid-now-indicator.css @@ -0,0 +1,52 @@ + +.fc { + + // line + + & .fc-timegrid-now-indicator-line { + position: absolute; + z-index: $timegrid-now-indicator-z; + left: 0; + right: 0; + border-style: solid; + border-color: var(--fc-now-indicator-color); + border-width: 1px 0 0; + } + + // arrow + + & .fc-timegrid-now-indicator-arrow { + position: absolute; + z-index: $timegrid-now-indicator-z; + margin-top: -5px; // vertically center on top coordinate + border-style: solid; + border-color: var(--fc-now-indicator-color); + } + +} + +.fc-direction-ltr { + + & .fc-timegrid-now-indicator-arrow { + left: 0; + + // triangle pointing right. TODO: mixin + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; + } + +} + +.fc-direction-rtl { + + & .fc-timegrid-now-indicator-arrow { + right: 0; + + // triangle pointing left. TODO: mixin + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; + } + +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/timegrid-slots.css b/fullcalendar-main/packages/timegrid/src/styles/timegrid-slots.css new file mode 100644 index 0000000..6a00770 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/timegrid-slots.css @@ -0,0 +1,83 @@ + +.fc { + + & .fc-timegrid-slots { + position: relative; + z-index: $timegrid-slots-z; + } + + & .fc-timegrid-slot { // a <td> + height: 1.5em; + border-bottom: 0; // each cell owns its top border + + &:empty:before { + content: '\00a0'; // make sure there's at least an empty space to create height for height syncing + } + } + + & .fc-timegrid-slot-minor { + border-top-style: dotted; + } + + & .fc-timegrid-slot-label-cushion { + display: inline-block; + white-space: nowrap; + } + + & .fc-timegrid-slot-label { + vertical-align: middle; // vertical align the slots + } + + + // slots AND axis cells (top-left corner of view including the "all-day" text) + + & .fc-timegrid-axis-cushion, + & .fc-timegrid-slot-label-cushion { + padding: 0 4px; + } + + + // axis cells (top-left corner of view including the "all-day" text) + // vertical align is more complicated, uses flexbox + + & .fc-timegrid-axis-frame-liquid { + height: 100%; // will need liquid-hack in FF + } + + & .fc-timegrid-axis-frame { + overflow: hidden; + display: flex; + align-items: center; // vertical align + justify-content: flex-end; // horizontal align. matches text-align below + } + + & .fc-timegrid-axis-cushion { + max-width: 60px; // limits the width of the "all-day" text + flex-shrink: 0; // allows text to expand how it normally would, regardless of constrained width + } + +} + +.fc-direction-ltr { + + & .fc-timegrid-slot-label-frame { + text-align: right; + } + +} + +.fc-direction-rtl { + + & .fc-timegrid-slot-label-frame { + text-align: left; + } + +} + +.fc-liquid-hack { + + & .fc-timegrid-axis-frame-liquid { + @include liquid-absolute-override; + } + +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/timegrid.css b/fullcalendar-main/packages/timegrid/src/styles/timegrid.css new file mode 100644 index 0000000..60db43e --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/timegrid.css @@ -0,0 +1,28 @@ + +.fc { + + & .fc-timegrid .fc-daygrid-body { // the all-day daygrid within the timegrid view + z-index: 2; // put above the timegrid-body so that more-popover is above everything. TODO: better solution + } + + & .fc-timegrid-divider { + padding: 0 0 2px; // browsers get confused when you set height. use padding instead + } + + & .fc-timegrid-body { + position: relative; + z-index: 1; // scope the z-indexes of slots and cols + min-height: 100%; // fill height always, even when slat table doesn't grow + } + + & .fc-timegrid-axis-chunk { // for advanced ScrollGrid + position: relative; // offset parent for now-indicator-container + + & > table { + position: relative; + z-index: 1; // above the now-indicator-container + } + + } + +} diff --git a/fullcalendar-main/packages/timegrid/src/styles/v-event.css b/fullcalendar-main/packages/timegrid/src/styles/v-event.css new file mode 100644 index 0000000..3efba16 --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/styles/v-event.css @@ -0,0 +1,109 @@ + +/* +A VERTICAL event +*/ + +.fc-v-event { // allowed to be top-level + display: block; + border: 1px solid var(--fc-event-border-color); + background-color: var(--fc-event-bg-color); + + & .fc-event-main { + color: var(--fc-event-text-color); + height: 100%; + } + + & .fc-event-main-frame { + height: 100%; + display: flex; + flex-direction: column; + } + + & .fc-event-time { + flex-grow: 0; + flex-shrink: 0; + max-height: 100%; + overflow: hidden; + } + + & .fc-event-title-container { // a container for the sticky cushion + flex-grow: 1; + flex-shrink: 1; + min-height: 0; // important for allowing to shrink all the way + } + + & .fc-event-title { // will have fc-sticky on it + top: 0; + bottom: 0; + max-height: 100%; // clip overflow + overflow: hidden; + } + + &:not(.fc-event-start) { + border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &:not(.fc-event-end) { + border-bottom-width: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + &.fc-event-selected:before { + // expand hit area + left: -10px; + right: -10px; + } + + // resizer (mouse AND touch) + + & .fc-event-resizer-start { + cursor: n-resize; + } + + & .fc-event-resizer-end { + cursor: s-resize; + } + + // resizer for MOUSE + + &:not(.fc-event-selected) { + + & .fc-event-resizer { + height: var(--fc-event-resizer-thickness); + left: 0; + right: 0; + } + + & .fc-event-resizer-start { + top: calc(var(--fc-event-resizer-thickness) / -2); + } + + & .fc-event-resizer-end { + bottom: calc(var(--fc-event-resizer-thickness) / -2); + } + + } + + // resizer for TOUCH (when event is "selected") + + &.fc-event-selected { + + & .fc-event-resizer { + left: 50%; + margin-left: calc(var(--fc-event-resizer-dot-total-width) / -2); + } + + & .fc-event-resizer-start { + top: calc(var(--fc-event-resizer-dot-total-width) / -2); + } + + & .fc-event-resizer-end { + bottom: calc(var(--fc-event-resizer-dot-total-width) / -2); + } + + } + +} diff --git a/fullcalendar-main/packages/timegrid/src/time-slat-meta.ts b/fullcalendar-main/packages/timegrid/src/time-slat-meta.ts new file mode 100644 index 0000000..b60ac5f --- /dev/null +++ b/fullcalendar-main/packages/timegrid/src/time-slat-meta.ts @@ -0,0 +1,78 @@ +import { Duration } from '@fullcalendar/core' +import { + createDuration, + asRoughMs, + formatIsoTimeString, + addDurations, + wholeDivideDurations, + DateMarker, + DateEnv, +} from '@fullcalendar/core/internal' + +export interface TimeSlatMeta { + date: DateMarker + time: Duration + key: string + isoTimeStr: string + isLabeled: boolean +} + +// potential nice values for the slot-duration and interval-duration +// from largest to smallest +const STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 }, +] + +export function buildSlatMetas( + slotMinTime: Duration, + slotMaxTime: Duration, + explicitLabelInterval: Duration | null, + slotDuration: Duration, + dateEnv: DateEnv, +) { + let dayStart = new Date(0) + let slatTime = slotMinTime + let slatIterator = createDuration(0) + let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration) + let metas: TimeSlatMeta[] = [] + + while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) { + let date = dateEnv.add(dayStart, slatTime) + let isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null + + metas.push({ + date, + time: slatTime, + key: date.toISOString(), // we can't use the isoTimeStr for uniqueness when minTime/maxTime beyone 0h/24h + isoTimeStr: formatIsoTimeString(date), + isLabeled, + }) + + slatTime = addDurations(slatTime, slotDuration) + slatIterator = addDurations(slatIterator, slotDuration) + } + + return metas +} + +// Computes an automatic value for slotLabelInterval +function computeLabelInterval(slotDuration) { + let i + let labelInterval + let slotsPerLabel + + // find the smallest stock label interval that results in more than one slots-per-label + for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) { + labelInterval = createDuration(STOCK_SUB_DURATIONS[i]) + slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration) + if (slotsPerLabel !== null && slotsPerLabel > 1) { + return labelInterval + } + } + + return slotDuration // fall back +} diff --git a/fullcalendar-main/packages/web-component/.eslintrc.cjs b/fullcalendar-main/packages/web-component/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/packages/web-component/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/packages/web-component/README.md b/fullcalendar-main/packages/web-component/README.md new file mode 100644 index 0000000..28a77cb --- /dev/null +++ b/fullcalendar-main/packages/web-component/README.md @@ -0,0 +1,78 @@ + +# FullCalendar Web Component + +This package provides a FullCalendar [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) (aka "Custom Element"). + + +## Installing via NPM + +Install the core package, the web-component package, and any plugins you plan to use: + +```sh +npm install --save \ + @fullcalendar/core \ + @fullcalendar/web-component \ + @fullcalendar/daygrid +``` + +Then, either register the element globally under its default tag name of `<full-calendar />`: + +```js +import '@fullcalendar/web-component/global' +``` + +Or, customize the tag name: + +```js +import { FullCalendarElement } from '@fullcalendar/web-component' + +customElements.define('some-calendar-tag', FullCalendarElement); +``` + +## Installing via CDN + +Include script tags for the core package, the web-component package, and any plugins you plan to use: + +```html +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://cdn.jsdelivr.net/npm/@fullcalendar/core/index.global.min.js'></script> +<script src='https://cdn.jsdelivr.net/npm/@fullcalendar/web-component/index.global.min.js'></script> +<script src='https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid/index.global.min.js'></script> +</head> +<body> + + <full-calendar shadow options='{ + "headerToolbar": { + "left": "prev,next today", + "center": "title", + "right": "dayGridMonth,dayGridWeek,dayGridDay" + } + }' /> + +</body> +</html> +``` + + +## Options + +The full-calendar element accepts a single `options` attribute. It must be a valid JSON string. + +The `shadow` attribute is necessary for rendering the calendar within its own shadow DOM (added in v6.1.0). This is recommended. + +It is possible to set an `options` *property* on the DOM element. This property is a real JavaScript object, not merely a JSON string. + +```js +const fullCalendarElement = document.querySelector('full-calendar') + +fullCalendarElement.options = { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay' + } +} +``` diff --git a/fullcalendar-main/packages/web-component/package.json b/fullcalendar-main/packages/web-component/package.json new file mode 100644 index 0000000..b42446e --- /dev/null +++ b/fullcalendar-main/packages/web-component/package.json @@ -0,0 +1,50 @@ +{ + "name": "@fullcalendar/web-component", + "version": "6.1.11", + "title": "FullCalendar Web Component", + "description": "Custom Element for FullCalendar", + "keywords": [ + "web-component", + "custom-element" + ], + "homepage": "https://fullcalendar.io/docs/web-component", + "peerDependencies": { + "@fullcalendar/core": "~6.1.11" + }, + "devDependencies": { + "@fullcalendar/core": "~6.1.11", + "@fullcalendar-scripts/standard": "*" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true + }, + "./global": {} + }, + "iifeGlobals": { + ".": "" + } + }, + "sideEffects": true, + "publishConfig": { + "directory": "./dist", + "linkDirectory": true + } +} diff --git a/fullcalendar-main/packages/web-component/src/FullCalendarElement.ts b/fullcalendar-main/packages/web-component/src/FullCalendarElement.ts new file mode 100644 index 0000000..8ad89ab --- /dev/null +++ b/fullcalendar-main/packages/web-component/src/FullCalendarElement.ts @@ -0,0 +1,75 @@ +import { Calendar, CalendarApi, CalendarOptions } from '@fullcalendar/core' + +export class FullCalendarElement extends HTMLElement { + _calendar: Calendar | null = null + _options: CalendarOptions | null = null + + connectedCallback() { + this._handleOptionsStr(this.getAttribute('options')) + } + + disconnectedCallback() { + this._handleOptionsStr(null) + } + + attributeChangedCallback(name: string, oldVal: string, newVal: string): void { + if ( + name === 'options' && + this._calendar // initial render happened + ) { + this._handleOptionsStr(newVal) + } + } + + get options(): CalendarOptions { + return this._options + } + + set options(options: CalendarOptions | null) { + this._handleOptions(options) + } + + getApi(): CalendarApi | null { + return this._calendar + } + + _handleOptionsStr(optionsStr: string | null) { + this._handleOptions(optionsStr ? JSON.parse(optionsStr) : null) + } + + _handleOptions(options: CalendarOptions | null): void { + if (options) { + if (this._calendar) { + this._calendar.resetOptions(options) + } else { + let root: ShadowRoot | HTMLElement + + if (this.hasAttribute('shadow')) { + this.attachShadow({ mode: 'open' }) + root = this.shadowRoot + } else { + // eslint-disable-next-line @typescript-eslint/no-this-alias + root = this + } + + root.innerHTML = '<div></div>' + let calendarEl = root.querySelector('div') + + let calendar = new Calendar(calendarEl, options) + calendar.render() + this._calendar = calendar + } + this._options = options + } else { + if (this._calendar) { + this._calendar.destroy() + this._calendar = null + } + this._options = null + } + } + + static get observedAttributes() { + return ['options'] + } +} diff --git a/fullcalendar-main/packages/web-component/src/global.ts b/fullcalendar-main/packages/web-component/src/global.ts new file mode 100644 index 0000000..1ed0ddf --- /dev/null +++ b/fullcalendar-main/packages/web-component/src/global.ts @@ -0,0 +1,17 @@ +import { FullCalendarElement } from './FullCalendarElement.js' + +type FullCalendarElementType = typeof FullCalendarElement + +declare global { + // (extensions to globalThis must use `var`) + // eslint-disable-next-line no-var + var FullCalendarElement: FullCalendarElementType + + interface HTMLElementTagNameMap { + 'full-calendar': FullCalendarElement + } +} + +globalThis.FullCalendarElement = FullCalendarElement + +customElements.define('full-calendar', FullCalendarElement) diff --git a/fullcalendar-main/packages/web-component/src/index.global.ts b/fullcalendar-main/packages/web-component/src/index.global.ts new file mode 100644 index 0000000..5a504c2 --- /dev/null +++ b/fullcalendar-main/packages/web-component/src/index.global.ts @@ -0,0 +1 @@ +import './global.js' diff --git a/fullcalendar-main/packages/web-component/src/index.ts b/fullcalendar-main/packages/web-component/src/index.ts new file mode 100644 index 0000000..1b8c051 --- /dev/null +++ b/fullcalendar-main/packages/web-component/src/index.ts @@ -0,0 +1,2 @@ + +export { FullCalendarElement } from './FullCalendarElement.js' diff --git a/fullcalendar-main/pnpm-lock.yaml b/fullcalendar-main/pnpm-lock.yaml new file mode 100644 index 0000000..1a5494b --- /dev/null +++ b/fullcalendar-main/pnpm-lock.yaml @@ -0,0 +1,5555 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + jasmine-jquery@2.1.1: + hash: ii5vcd4rtfy5ngsgq7effin6p4 + path: scripts/patches/jasmine-jquery@2.1.1.patch + +importers: + + .: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:scripts + + bundle: + dependencies: + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../packages/core/dist + '@fullcalendar/daygrid': + specifier: ~6.1.11 + version: link:../packages/daygrid/dist + '@fullcalendar/interaction': + specifier: ~6.1.11 + version: link:../packages/interaction/dist + '@fullcalendar/list': + specifier: ~6.1.11 + version: link:../packages/list/dist + '@fullcalendar/multimonth': + specifier: ~6.1.11 + version: link:../packages/multimonth/dist + '@fullcalendar/timegrid': + specifier: ~6.1.11 + version: link:../packages/timegrid/dist + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../scripts + publishDirectory: ./dist + + packages/bootstrap4: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/bootstrap5: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/core: + dependencies: + preact: + specifier: ~10.12.1 + version: 10.12.1 + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + globby: + specifier: ^13.1.2 + version: 13.1.2 + handlebars: + specifier: ^4.1.2 + version: 4.7.7 + publishDirectory: ./dist + + packages/daygrid: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/google-calendar: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/icalendar: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + ical.js: + specifier: ^1.4.0 + version: 1.5.0 + publishDirectory: ./dist + + packages/interaction: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/list: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/luxon1: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + luxon: + specifier: ^1.12.1 + version: 1.28.0 + publishDirectory: ./dist + + packages/luxon2: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + '@types/luxon': + specifier: ^2.0.9 + version: 2.4.0 + luxon: + specifier: ^2.0.0 + version: 2.5.0 + publishDirectory: ./dist + + packages/luxon3: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + '@types/luxon': + specifier: ^3.3.0 + version: 3.3.0 + luxon: + specifier: ^3.0.0 + version: 3.3.0 + publishDirectory: ./dist + + packages/moment: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + moment: + specifier: ^2.29.1 + version: 2.29.4 + publishDirectory: ./dist + + packages/moment-timezone: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + moment-timezone: + specifier: ^0.5.40 + version: 0.5.40 + publishDirectory: ./dist + + packages/multimonth: + dependencies: + '@fullcalendar/daygrid': + specifier: ~6.1.11 + version: link:../daygrid/dist + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/rrule: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + rrule: + specifier: ^2.6.0 + version: 2.7.1 + publishDirectory: ./dist + + packages/timegrid: + dependencies: + '@fullcalendar/daygrid': + specifier: ~6.1.11 + version: link:../daygrid/dist + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + packages/web-component: + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../../scripts + '@fullcalendar/core': + specifier: ~6.1.11 + version: link:../core/dist + publishDirectory: ./dist + + scripts: + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^12.0.0 + version: 12.0.0(rollup@2.79.1) + '@rollup/plugin-json': + specifier: ^4.0.3 + version: 4.1.0(rollup@2.79.1) + '@rollup/plugin-node-resolve': + specifier: ^14.0.1 + version: 14.1.0(rollup@2.79.1) + '@rollup/plugin-replace': + specifier: ^5.0.1 + version: 5.0.1(rollup@2.79.1) + '@tsconfig/node16': + specifier: ^1.0.3 + version: 1.0.3 + '@types/archiver': + specifier: ^5.3.1 + version: 5.3.1 + '@types/cross-spawn': + specifier: ^6.0.2 + version: 6.0.2 + '@types/js-yaml': + specifier: ^4.0.5 + version: 4.0.5 + '@types/karma': + specifier: ^6.3.3 + version: 6.3.3 + '@types/node': + specifier: ^16.11.7 + version: 16.18.3 + '@types/semver': + specifier: ^7.3.12 + version: 7.3.13 + '@typescript-eslint/eslint-plugin': + specifier: ^5.40.0 + version: 5.44.0(@typescript-eslint/parser@5.44.0)(eslint@8.28.0)(typescript@4.9.3) + '@typescript-eslint/parser': + specifier: ^5.40.0 + version: 5.44.0(eslint@8.28.0)(typescript@4.9.3) + archiver: + specifier: ^5.3.1 + version: 5.3.1 + autoprefixer: + specifier: ^9.8.4 + version: 9.8.8 + chalk: + specifier: ^5.0.1 + version: 5.1.2 + chokidar: + specifier: ^2.1.5 + version: 2.1.8 + cleye: + specifier: ^1.2.1 + version: 1.3.1 + components-jqueryui: + specifier: ^1.12.1 + version: 1.12.1 + cross-spawn: + specifier: ^7.0.3 + version: 7.0.3 + esbuild: + specifier: ^0.15.7 + version: 0.15.15 + eslint: + specifier: ^8.25.0 + version: 8.28.0 + eslint-plugin-react: + specifier: ^7.31.10 + version: 7.31.11(eslint@8.28.0) + globby: + specifier: ^13.1.2 + version: 13.1.2 + handlebars: + specifier: ^4.1.2 + version: 4.7.7 + jasmine-jquery: + specifier: ^2.1.1 + version: 2.1.1(patch_hash=ii5vcd4rtfy5ngsgq7effin6p4) + jquery: + specifier: ^3.4.0 + version: 3.6.1 + jquery-simulate: + specifier: ^1.0.2 + version: 1.0.2 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + karma: + specifier: ^6.3.2 + version: 6.4.1 + karma-chrome-launcher: + specifier: ^3.1.0 + version: 3.1.1 + karma-jasmine: + specifier: ^4.0.1 + version: 4.0.2(karma@6.4.1) + karma-sourcemap-loader: + specifier: ^0.3.8 + version: 0.3.8 + karma-verbose-reporter: + specifier: 0.0.6 + version: 0.0.6(karma@6.4.1) + postcss: + specifier: ^8.4.20 + version: 8.4.20 + postcss-advanced-variables: + specifier: ^3.0.1 + version: 3.0.1 + postcss-comment: + specifier: ^2.0.0 + version: 2.0.0 + postcss-nesting: + specifier: ^7.0.1 + version: 7.0.1 + rollup: + specifier: ^2.79.0 + version: 2.79.1 + rollup-plugin-dts: + specifier: ^3.0.2 + version: 3.0.2(rollup@2.79.1)(typescript@4.9.3) + rollup-plugin-postcss: + specifier: ^4.0.2 + version: 4.0.2(postcss@8.4.20) + rollup-plugin-sourcemaps: + specifier: ^0.6.3 + version: 0.6.3(@types/node@16.18.3)(rollup@2.79.1) + semver: + specifier: ^7.3.8 + version: 7.3.8 + terser: + specifier: ^4.8.0 + version: 4.8.1 + turbo: + specifier: ^1.5.6 + version: 1.6.3 + typescript: + specifier: ^4.8.2 + version: 4.9.3 + + tests: + dependencies: + '@fullcalendar/bootstrap': + specifier: ~6.1.10 + version: link:../packages/bootstrap4/dist + '@fullcalendar/core': + specifier: ~6.1.10 + version: link:../packages/core/dist + '@fullcalendar/daygrid': + specifier: ~6.1.10 + version: link:../packages/daygrid/dist + '@fullcalendar/google-calendar': + specifier: ~6.1.10 + version: link:../packages/google-calendar/dist + '@fullcalendar/icalendar': + specifier: ~6.1.10 + version: link:../packages/icalendar/dist + '@fullcalendar/interaction': + specifier: ~6.1.10 + version: link:../packages/interaction/dist + '@fullcalendar/list': + specifier: ~6.1.10 + version: link:../packages/list/dist + '@fullcalendar/luxon3': + specifier: ~6.1.10 + version: link:../packages/luxon3/dist + '@fullcalendar/moment': + specifier: ~6.1.10 + version: link:../packages/moment/dist + '@fullcalendar/moment-timezone': + specifier: ~6.1.10 + version: link:../packages/moment-timezone/dist + '@fullcalendar/multimonth': + specifier: ~6.1.10 + version: link:../packages/multimonth/dist + '@fullcalendar/rrule': + specifier: ~6.1.10 + version: link:../packages/rrule/dist + '@fullcalendar/timegrid': + specifier: ~6.1.10 + version: link:../packages/timegrid/dist + fullcalendar: + specifier: ~6.1.10 + version: link:../bundle/dist + luxon: + specifier: ^2.0.0 + version: 2.5.0 + moment: + specifier: ^2.29.1 + version: 2.29.4 + moment-timezone: + specifier: ^0.5.40 + version: 0.5.40 + xhr-mock: + specifier: ^2.5.1 + version: 2.5.1 + devDependencies: + '@fullcalendar-scripts/standard': + specifier: '*' + version: link:../scripts + '@types/jasmine': + specifier: ^3.3.12 + version: 3.10.6 + '@types/jasmine-jquery': + specifier: ^1.5.33 + version: 1.5.34 + '@types/jquery': + specifier: ^3.3.29 + version: 3.5.14 + fetch-mock: + specifier: ^9.11.0 + version: 9.11.0 + handlebars: + specifier: ^4.7.7 + version: 4.7.7 + +packages: + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@babel/code-frame@7.23.4: + resolution: {integrity: sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.23.3: + resolution: {integrity: sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.21.4: + resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.4 + '@babel/generator': 7.23.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.21.4) + '@babel/helpers': 7.23.4 + '@babel/parser': 7.23.4 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.4 + '@babel/types': 7.23.4 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.4: + resolution: {integrity: sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.4 + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.3 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.22.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.4 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.4 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.4 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.21.4): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.4 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.4 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.23.4: + resolution: {integrity: sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.4 + '@babel/types': 7.23.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.4: + resolution: {integrity: sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/types': 7.23.4 + dev: true + + /@babel/runtime@7.20.1: + resolution: {integrity: sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.4 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 + dev: true + + /@babel/traverse@7.23.4: + resolution: {integrity: sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.4 + '@babel/generator': 7.23.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.4 + '@babel/types': 7.23.4 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.4: + resolution: {integrity: sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: true + + /@csstools/sass-import-resolve@1.0.0: + resolution: {integrity: sha512-pH4KCsbtBLLe7eqUrw8brcuFO8IZlN36JjdKlOublibVdAIPHCzEnpBWOVUXK5sCf+DpBi8ZtuWtjF0srybdeA==} + engines: {node: '>=4.0.0'} + dev: true + + /@esbuild/android-arm@0.15.15: + resolution: {integrity: sha512-JJjZjJi2eBL01QJuWjfCdZxcIgot+VoK6Fq7eKF9w4YHm9hwl7nhBR1o2Wnt/WcANk5l9SkpvrldW1PLuXxcbw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.15.15: + resolution: {integrity: sha512-lhz6UNPMDXUhtXSulw8XlFAtSYO26WmHQnCi2Lg2p+/TMiJKNLtZCYUxV4wG6rZMzXmr8InGpNwk+DLT2Hm0PA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@eslint/eslintrc@1.3.3: + resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.4.1 + globals: 13.18.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/config-array@0.11.7: + resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@jridgewell/gen-mapping@0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@jridgewell/resolve-uri@3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + + /@rollup/plugin-commonjs@12.0.0(rollup@2.79.1): + resolution: {integrity: sha512-8+mDQt1QUmN+4Y9D3yCG8AJNewuTSLYPJVzKKUZ+lGeQrI+bV12Tc5HCyt2WdlnG6ihIL/DPbKRJlB40DX40mw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^2.3.4 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + commondir: 1.0.1 + estree-walker: 1.0.1 + glob: 7.2.3 + is-reference: 1.2.1 + magic-string: 0.25.9 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-json@4.1.0(rollup@2.79.1): + resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + rollup: 2.79.1 + dev: true + + /@rollup/plugin-node-resolve@14.1.0(rollup@2.79.1): + resolution: {integrity: sha512-5G2niJroNCz/1zqwXtk0t9+twOSDlG00k1Wfd7bkbbXmwg8H8dvgHdIWAun53Ps/rckfvOC7scDBjuGFg5OaWw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.78.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@types/resolve': 1.17.1 + deepmerge: 4.2.2 + is-builtin-module: 3.2.0 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-replace@5.0.1(rollup@2.79.1): + resolution: {integrity: sha512-Z3MfsJ4CK17BfGrZgvrcp/l6WXoKb0kokULO+zt/7bmcyayokDaQ2K3eDJcRLCTAlp5FPI4/gz9MHAsosz4Rag==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + magic-string: 0.26.7 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils@3.1.0(rollup@2.79.1): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils@5.0.2(rollup@2.79.1): + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: true + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /@tsconfig/node16@1.0.3: + resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} + dev: true + + /@types/archiver@5.3.1: + resolution: {integrity: sha512-wKYZaSXaDvTZuInAWjCeGG7BEAgTWG2zZW0/f7IYFcoHB2X2d9lkVFnrOlXl3W6NrvO6Ml3FLLu8Uksyymcpnw==} + dependencies: + '@types/glob': 8.0.0 + dev: true + + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: true + + /@types/cors@2.8.12: + resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} + dev: true + + /@types/cross-spawn@6.0.2: + resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} + dependencies: + '@types/node': 18.16.5 + dev: true + + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/estree@1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + dev: true + + /@types/glob@8.0.0: + resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.16.5 + dev: true + + /@types/jasmine-jquery@1.5.34: + resolution: {integrity: sha512-PQpKqgzQa3nAtJb/zlBpWjagMtQK7xiAy6DsOhSla5lDAeBCvqyHfzBeTJ7YPDto0fernTSycPif8msdWH3Nlg==} + dependencies: + '@types/jasmine': 3.10.6 + '@types/jquery': 3.5.14 + dev: true + + /@types/jasmine@3.10.6: + resolution: {integrity: sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==} + dev: true + + /@types/jquery@3.5.14: + resolution: {integrity: sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==} + dependencies: + '@types/sizzle': 2.3.3 + dev: true + + /@types/js-yaml@4.0.5: + resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} + dev: true + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/karma@6.3.3: + resolution: {integrity: sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==} + dependencies: + '@types/node': 18.16.5 + log4js: 6.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@types/luxon@2.4.0: + resolution: {integrity: sha512-oCavjEjRXuR6URJEtQm0eBdfsBiEcGBZbq21of8iGkeKxU1+1xgKuFPClaBZl2KB8ZZBSWlgk61tH6Mf+nvZVw==} + dev: true + + /@types/luxon@3.3.0: + resolution: {integrity: sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==} + dev: true + + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: true + + /@types/node@16.18.3: + resolution: {integrity: sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==} + dev: true + + /@types/node@18.16.5: + resolution: {integrity: sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==} + dev: true + + /@types/resolve@1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.16.5 + dev: true + + /@types/semver@7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + dev: true + + /@types/sizzle@2.3.3: + resolution: {integrity: sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==} + dev: true + + /@typescript-eslint/eslint-plugin@5.44.0(@typescript-eslint/parser@5.44.0)(eslint@8.28.0)(typescript@4.9.3): + resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.44.0(eslint@8.28.0)(typescript@4.9.3) + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/type-utils': 5.44.0(eslint@8.28.0)(typescript@4.9.3) + '@typescript-eslint/utils': 5.44.0(eslint@8.28.0)(typescript@4.9.3) + debug: 4.3.4 + eslint: 8.28.0 + ignore: 5.2.0 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.9.3) + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.44.0(eslint@8.28.0)(typescript@4.9.3): + resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/typescript-estree': 5.44.0(typescript@4.9.3) + debug: 4.3.4 + eslint: 8.28.0 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.44.0: + resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/visitor-keys': 5.44.0 + dev: true + + /@typescript-eslint/type-utils@5.44.0(eslint@8.28.0)(typescript@4.9.3): + resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.44.0(typescript@4.9.3) + '@typescript-eslint/utils': 5.44.0(eslint@8.28.0)(typescript@4.9.3) + debug: 4.3.4 + eslint: 8.28.0 + tsutils: 3.21.0(typescript@4.9.3) + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.44.0: + resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.44.0(typescript@4.9.3): + resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/visitor-keys': 5.44.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.9.3) + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.44.0(eslint@8.28.0)(typescript@4.9.3): + resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/typescript-estree': 5.44.0(typescript@4.9.3) + eslint: 8.28.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.28.0) + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.44.0: + resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.44.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: true + + /acorn-jsx@5.3.2(acorn@8.8.1): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.1 + dev: true + + /acorn@8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.7 + dev: true + + /archiver@5.3.1: + resolution: {integrity: sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 2.1.0 + async: 3.2.4 + buffer-crc32: 0.2.13 + readable-stream: 3.6.0 + readdir-glob: 1.1.2 + tar-stream: 2.2.0 + zip-stream: 4.1.0 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + dev: true + + /arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + dev: true + + /arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + dev: true + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + get-intrinsic: 1.1.3 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.tosorted@1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.1.3 + dev: true + + /assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + dev: true + + /async-each@1.0.3: + resolution: {integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==} + dev: true + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + dev: true + + /autoprefixer@9.8.8: + resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==} + dependencies: + browserslist: 4.21.4 + caniuse-lite: 1.0.30001434 + normalize-range: 0.1.2 + num2fraction: 1.2.2 + picocolors: 0.2.1 + postcss: 7.0.39 + postcss-value-parser: 4.2.0 + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: true + + /base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.0 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + dev: true + + /binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + dependencies: + file-uri-to-path: 1.0.0 + dev: true + optional: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.4 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist@4.21.4: + resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + dependencies: + caniuse-lite: 1.0.30001565 + electron-to-chromium: 1.4.595 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.21.4) + dev: true + + /browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + dependencies: + caniuse-lite: 1.0.30001565 + electron-to-chromium: 1.4.595 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.22.1) + dev: true + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: true + + /cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.0 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.1.3 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.22.1 + caniuse-lite: 1.0.30001565 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: true + + /caniuse-lite@1.0.30001434: + resolution: {integrity: sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==} + dev: true + + /caniuse-lite@1.0.30001565: + resolution: {integrity: sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@5.1.2: + resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + + /chokidar@2.1.8: + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + dependencies: + anymatch: 2.0.0 + async-each: 1.0.3 + braces: 2.3.2 + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + dev: true + + /cleye@1.3.1: + resolution: {integrity: sha512-8Za9ohNiygniCAHj6sVKJFI1FkEdmJM7XGA+tI3TVMSf9wQ1o8k0I6CZSRDx4AG5WZuqr0Zj4i/6Bw1TdoMyJQ==} + dependencies: + terminal-columns: 1.4.1 + type-flag: 3.0.0 + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + + /colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /component-emitter@1.3.0: + resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} + dev: true + + /components-jqueryui@1.12.1: + resolution: {integrity: sha512-efkkM1OzVyxMiyZRB+eu3fhweE3T2fGs2Syia8A6l38sfB0Y8MRvYENhVS3UO21Gel93nt2HZiv+zsV8/4y83w==} + dev: true + + /compress-commons@4.1.1: + resolution: {integrity: sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==} + engines: {node: '>= 10'} + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.2 + normalize-path: 3.0.0 + readable-stream: 3.6.0 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /concat-with-sourcemaps@1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + dependencies: + source-map: 0.6.1 + dev: true + + /connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /content-type@1.0.4: + resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + engines: {node: '>= 0.6'} + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: true + + /copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + dev: true + + /core-js@3.26.1: + resolution: {integrity: sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==} + requiresBuild: true + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: true + + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + dev: true + + /crc32-stream@4.0.2: + resolution: {integrity: sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==} + engines: {node: '>= 10'} + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.0 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-declaration-sorter@6.3.1(postcss@8.4.20): + resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.4.20 + dev: true + + /css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + dev: true + + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + dev: true + + /cssnano-preset-default@5.2.13(postcss@8.4.20): + resolution: {integrity: sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.3.1(postcss@8.4.20) + cssnano-utils: 3.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-calc: 8.2.4(postcss@8.4.20) + postcss-colormin: 5.3.0(postcss@8.4.20) + postcss-convert-values: 5.1.3(postcss@8.4.20) + postcss-discard-comments: 5.1.2(postcss@8.4.20) + postcss-discard-duplicates: 5.1.0(postcss@8.4.20) + postcss-discard-empty: 5.1.1(postcss@8.4.20) + postcss-discard-overridden: 5.1.0(postcss@8.4.20) + postcss-merge-longhand: 5.1.7(postcss@8.4.20) + postcss-merge-rules: 5.1.3(postcss@8.4.20) + postcss-minify-font-values: 5.1.0(postcss@8.4.20) + postcss-minify-gradients: 5.1.1(postcss@8.4.20) + postcss-minify-params: 5.1.4(postcss@8.4.20) + postcss-minify-selectors: 5.2.1(postcss@8.4.20) + postcss-normalize-charset: 5.1.0(postcss@8.4.20) + postcss-normalize-display-values: 5.1.0(postcss@8.4.20) + postcss-normalize-positions: 5.1.1(postcss@8.4.20) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.20) + postcss-normalize-string: 5.1.0(postcss@8.4.20) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.20) + postcss-normalize-unicode: 5.1.1(postcss@8.4.20) + postcss-normalize-url: 5.1.0(postcss@8.4.20) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.20) + postcss-ordered-values: 5.1.3(postcss@8.4.20) + postcss-reduce-initial: 5.1.1(postcss@8.4.20) + postcss-reduce-transforms: 5.1.0(postcss@8.4.20) + postcss-svgo: 5.1.0(postcss@8.4.20) + postcss-unique-selectors: 5.1.1(postcss@8.4.20) + dev: true + + /cssnano-utils@3.1.0(postcss@8.4.20): + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /cssnano@5.1.14(postcss@8.4.20): + resolution: {integrity: sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 5.2.13(postcss@8.4.20) + lilconfig: 2.0.6 + postcss: 8.4.20 + yaml: 1.10.2 + dev: true + + /csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + dev: true + + /custom-event@1.0.1: + resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==} + dev: true + + /date-format@4.0.14: + resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} + engines: {node: '>=4.0'} + dev: true + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decode-uri-component@0.2.0: + resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} + engines: {node: '>=0.10'} + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + dev: true + + /define-properties@1.1.4: + resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 0.1.6 + dev: true + + /define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.2 + dev: true + + /define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.2 + isobject: 3.0.1 + dev: true + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: true + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: true + + /di@0.0.1: + resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-serialize@2.2.1: + resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==} + dependencies: + custom-event: 1.0.1 + ent: 2.2.0 + extend: 3.0.2 + void-elements: 2.0.1 + dev: true + + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: true + + /dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: true + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: true + + /electron-to-chromium@1.4.595: + resolution: {integrity: sha512-+ozvXuamBhDOKvMNUQvecxfbyICmIAwS4GpLmR0bsiSBlGnLaOcs2Cj7J8XSbW+YEaN3Xl3ffgpm+srTUWFwFQ==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /engine.io-parser@5.0.4: + resolution: {integrity: sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==} + engines: {node: '>=10.0.0'} + dev: true + + /engine.io@6.2.1: + resolution: {integrity: sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==} + engines: {node: '>=10.0.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.12 + '@types/node': 18.16.5 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.0.4 + ws: 8.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /ent@2.2.0: + resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} + dev: true + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: true + + /es-abstract@1.20.4: + resolution: {integrity: sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.2 + function.prototype.name: 1.1.5 + get-intrinsic: 1.1.3 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-weakref: 1.0.2 + object-inspect: 1.12.2 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + unbox-primitive: 1.0.2 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild-android-64@0.15.15: + resolution: {integrity: sha512-F+WjjQxO+JQOva3tJWNdVjouFMLK6R6i5gjDvgUthLYJnIZJsp1HlF523k73hELY20WPyEO8xcz7aaYBVkeg5Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64@0.15.15: + resolution: {integrity: sha512-attlyhD6Y22jNyQ0fIIQ7mnPvDWKw7k6FKnsXlBvQE6s3z6s6cuEHcSgoirquQc7TmZgVCK5fD/2uxmRN+ZpcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64@0.15.15: + resolution: {integrity: sha512-ohZtF8W1SHJ4JWldsPVdk8st0r9ExbAOSrBOh5L+Mq47i696GVwv1ab/KlmbUoikSTNoXEhDzVpxUR/WIO19FQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64@0.15.15: + resolution: {integrity: sha512-P8jOZ5zshCNIuGn+9KehKs/cq5uIniC+BeCykvdVhx/rBXSxmtj3CUIKZz4sDCuESMbitK54drf/2QX9QHG5Ag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64@0.15.15: + resolution: {integrity: sha512-KkTg+AmDXz1IvA9S1gt8dE24C8Thx0X5oM0KGF322DuP+P3evwTL9YyusHAWNsh4qLsR80nvBr/EIYs29VSwuA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64@0.15.15: + resolution: {integrity: sha512-FUcML0DRsuyqCMfAC+HoeAqvWxMeq0qXvclZZ/lt2kLU6XBnDA5uKTLUd379WYEyVD4KKFctqWd9tTuk8C/96g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32@0.15.15: + resolution: {integrity: sha512-q28Qn5pZgHNqug02aTkzw5sW9OklSo96b5nm17Mq0pDXrdTBcQ+M6Q9A1B+dalFeynunwh/pvfrNucjzwDXj+Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64@0.15.15: + resolution: {integrity: sha512-217KPmWMirkf8liO+fj2qrPwbIbhNTGNVtvqI1TnOWJgcMjUWvd677Gq3fTzXEjilkx2yWypVnTswM2KbXgoAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64@0.15.15: + resolution: {integrity: sha512-/ltmNFs0FivZkYsTzAsXIfLQX38lFnwJTWCJts0IbCqWZQe+jjj0vYBNbI0kmXLb3y5NljiM5USVAO1NVkdh2g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm@0.15.15: + resolution: {integrity: sha512-RYVW9o2yN8yM7SB1yaWr378CwrjvGCyGybX3SdzPHpikUHkME2AP55Ma20uNwkNyY2eSYFX9D55kDrfQmQBR4w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le@0.15.15: + resolution: {integrity: sha512-PksEPb321/28GFFxtvL33yVPfnMZihxkEv5zME2zapXGp7fA1X2jYeiTUK+9tJ/EGgcNWuwvtawPxJG7Mmn86A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le@0.15.15: + resolution: {integrity: sha512-ek8gJBEIhcpGI327eAZigBOHl58QqrJrYYIZBWQCnH3UnXoeWMrMZLeeZL8BI2XMBhP+sQ6ERctD5X+ajL/AIA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64@0.15.15: + resolution: {integrity: sha512-H5ilTZb33/GnUBrZMNJtBk7/OXzDHDXjIzoLXHSutwwsLxSNaLxzAaMoDGDd/keZoS+GDBqNVxdCkpuiRW4OSw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x@0.15.15: + resolution: {integrity: sha512-jKaLUg78mua3rrtrkpv4Or2dNTJU7bgHN4bEjT4OX4GR7nLBSA9dfJezQouTxMmIW7opwEC5/iR9mpC18utnxQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64@0.15.15: + resolution: {integrity: sha512-aOvmF/UkjFuW6F36HbIlImJTTx45KUCHJndtKo+KdP8Dhq3mgLRKW9+6Ircpm8bX/RcS3zZMMmaBLkvGY06Gvw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64@0.15.15: + resolution: {integrity: sha512-HFFX+WYedx1w2yJ1VyR1Dfo8zyYGQZf1cA69bLdrHzu9svj6KH6ZLK0k3A1/LFPhcEY9idSOhsB2UyU0tHPxgQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64@0.15.15: + resolution: {integrity: sha512-jOPBudffG4HN8yJXcK9rib/ZTFoTA5pvIKbRrt3IKAGMq1EpBi4xoVoSRrq/0d4OgZLaQbmkHp8RO9eZIn5atA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32@0.15.15: + resolution: {integrity: sha512-MDkJ3QkjnCetKF0fKxCyYNBnOq6dmidcwstBVeMtXSgGYTy8XSwBeIE4+HuKiSsG6I/mXEb++px3IGSmTN0XiA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64@0.15.15: + resolution: {integrity: sha512-xaAUIB2qllE888SsMU3j9nrqyLbkqqkpQyWVkfwSil6BBPgcPk3zOFitTTncEKCLTQy3XV9RuH7PDj3aJDljWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64@0.15.15: + resolution: {integrity: sha512-ttuoCYCIJAFx4UUKKWYnFdrVpoXa3+3WWkXVI6s09U+YjhnyM5h96ewTq/WgQj9LFSIlABQvadHSOQyAVjW5xQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild@0.15.15: + resolution: {integrity: sha512-TEw/lwK4Zzld9x3FedV6jy8onOUHqcEX3ADFk4k+gzPUwrxn8nWV62tH0udo8jOtjFodlEfc4ypsqX3e+WWO6w==} + engines: {node: '>=12'} + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.15.15 + '@esbuild/linux-loong64': 0.15.15 + esbuild-android-64: 0.15.15 + esbuild-android-arm64: 0.15.15 + esbuild-darwin-64: 0.15.15 + esbuild-darwin-arm64: 0.15.15 + esbuild-freebsd-64: 0.15.15 + esbuild-freebsd-arm64: 0.15.15 + esbuild-linux-32: 0.15.15 + esbuild-linux-64: 0.15.15 + esbuild-linux-arm: 0.15.15 + esbuild-linux-arm64: 0.15.15 + esbuild-linux-mips64le: 0.15.15 + esbuild-linux-ppc64le: 0.15.15 + esbuild-linux-riscv64: 0.15.15 + esbuild-linux-s390x: 0.15.15 + esbuild-netbsd-64: 0.15.15 + esbuild-openbsd-64: 0.15.15 + esbuild-sunos-64: 0.15.15 + esbuild-windows-32: 0.15.15 + esbuild-windows-64: 0.15.15 + esbuild-windows-arm64: 0.15.15 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-react@7.31.11(eslint@8.28.0): + resolution: {integrity: sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 + doctrine: 2.1.0 + eslint: 8.28.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.3 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.8 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.28.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.28.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.28.0: + resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@eslint/eslintrc': 1.3.3 + '@humanwhocodes/config-array': 0.11.7 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-utils: 3.0.0(eslint@8.28.0) + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.4.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.18.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.0 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.2.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.4.1: + resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.1 + acorn-jsx: 5.3.2(acorn@8.8.1) + eslint-visitor-keys: 3.3.0 + dev: true + + /esquery@1.4.0: + resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: true + + /expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: true + + /extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + dev: true + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: true + + /extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fetch-mock@9.11.0: + resolution: {integrity: sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==} + engines: {node: '>=4.0.0'} + peerDependencies: + node-fetch: '*' + peerDependenciesMeta: + node-fetch: + optional: true + dependencies: + '@babel/core': 7.21.4 + '@babel/runtime': 7.20.1 + core-js: 3.26.1 + debug: 4.3.4 + glob-to-regexp: 0.4.1 + is-subset: 0.1.1 + lodash.isequal: 4.5.0 + path-to-regexp: 2.4.0 + querystring: 0.2.1 + whatwg-url: 6.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: true + optional: true + + /fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + + /for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + dev: true + + /fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + dependencies: + map-cache: 0.2.2 + dev: true + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.17.0 + dev: true + optional: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /generic-names@4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + dependencies: + loader-utils: 3.2.1 + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-intrinsic@1.1.3: + resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + dev: true + + /get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + dev: true + + /glob-parent@3.1.0: + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} + dependencies: + is-glob: 3.1.0 + path-dirname: 1.0.2 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.0 + process: 0.11.10 + dev: false + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.18.0: + resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globby@13.1.2: + resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + dependencies: + minimist: 1.2.7 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.1.3 + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + dev: true + + /has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + dev: true + + /has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + dev: true + + /has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: true + + /http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.2 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + dev: true + + /ical.js@1.5.0: + resolution: {integrity: sha512-7ZxMkogUkkaCx810yp0ZGKvq1ZpRgJeornPttpoxe6nYZ3NLesZe1wWMXDdwTkj/b5NtXT+Y16Aakph/ao98ZQ==} + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /icss-replace-symbols@1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + dev: true + + /icss-utils@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.20 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-cwd@3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + dependencies: + import-from: 3.0.0 + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-from@3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot@1.0.3: + resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.3 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-accessor-descriptor@0.1.6: + resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-accessor-descriptor@1.0.0: + resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 6.0.3 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + dependencies: + binary-extensions: 1.13.1 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: true + + /is-builtin-module@3.2.0: + resolution: {integrity: sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + + /is-data-descriptor@0.1.4: + resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-data-descriptor@1.0.0: + resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 6.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-descriptor@0.1.6: + resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} + engines: {node: '>=0.10.0'} + dependencies: + is-accessor-descriptor: 0.1.6 + is-data-descriptor: 0.1.4 + kind-of: 5.1.0 + dev: true + + /is-descriptor@1.0.2: + resolution: {integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==} + engines: {node: '>=0.10.0'} + dependencies: + is-accessor-descriptor: 1.0.0 + is-data-descriptor: 1.0.0 + kind-of: 6.0.3 + dev: true + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: true + + /is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-object: 2.0.4 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.0 + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-subset@0.1.1: + resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==} + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + dependencies: + isarray: 1.0.0 + dev: true + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /jasmine-core@3.8.0: + resolution: {integrity: sha512-zl0nZWDrmbCiKns0NcjkFGYkVTGCPUgoHypTaj+G2AzaWus7QGoXARSlYsSle2VRpSdfJmM+hzmFKzQNhF2kHg==} + dev: true + + /jasmine-jquery@2.1.1(patch_hash=ii5vcd4rtfy5ngsgq7effin6p4): + resolution: {integrity: sha512-13d18zXTGeiNAjd4sAhoUGQbCEXTMbUyjrnQ9JFQM/atyyFubHejL5RCE5iyW7jOh1cwjoqVefAAsSA4SFtooQ==} + dev: true + patched: true + + /jquery-simulate@1.0.2: + resolution: {integrity: sha512-Bq610fSrwTwvH5d06z5oskYaX/79s0BNrKiJZjZOiXRib3iL4ZkSn/wvLwzhf3P9KeXCEpk9wlIaGui/1arOpQ==} + dev: true + + /jquery@3.6.1: + resolution: {integrity: sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==} + dev: true + + /js-sdsl@4.2.0: + resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + dev: true + + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsx-ast-utils@3.3.3: + resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + object.assign: 4.1.4 + dev: true + + /karma-chrome-launcher@3.1.1: + resolution: {integrity: sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==} + dependencies: + which: 1.3.1 + dev: true + + /karma-jasmine@4.0.2(karma@6.4.1): + resolution: {integrity: sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==} + engines: {node: '>= 10'} + peerDependencies: + karma: '*' + dependencies: + jasmine-core: 3.8.0 + karma: 6.4.1 + dev: true + + /karma-sourcemap-loader@0.3.8: + resolution: {integrity: sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==} + dependencies: + graceful-fs: 4.2.10 + dev: true + + /karma-verbose-reporter@0.0.6(karma@6.4.1): + resolution: {integrity: sha512-xBkQTj7JUByofR1sounBEL0sVKTCBeoiPEMtNBU8Dr/Gtqt6W6XUq2hj36r+EKYHQPPJch7moINsYUWO8moWXQ==} + peerDependencies: + karma: '>=0.12' + dependencies: + colors: 1.4.0 + karma: 6.4.1 + dev: true + + /karma@6.4.1: + resolution: {integrity: sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==} + engines: {node: '>= 10'} + dependencies: + '@colors/colors': 1.5.0 + body-parser: 1.20.1 + braces: 3.0.2 + chokidar: 3.5.3 + connect: 3.7.0 + di: 0.0.1 + dom-serialize: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + http-proxy: 1.18.1 + isbinaryfile: 4.0.10 + lodash: 4.17.21 + log4js: 6.7.0 + mime: 2.6.0 + minimatch: 3.1.2 + mkdirp: 0.5.6 + qjobs: 1.2.0 + range-parser: 1.2.1 + rimraf: 3.0.2 + socket.io: 4.5.3 + source-map: 0.6.1 + tmp: 0.2.1 + ua-parser-js: 0.7.32 + yargs: 16.2.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: true + + /kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: true + + /kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: true + + /kind-of@5.1.0: + resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} + engines: {node: '>=0.10.0'} + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.7 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lilconfig@2.0.6: + resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} + engines: {node: '>=10'} + dev: true + + /loader-utils@3.2.1: + resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} + engines: {node: '>= 12.13.0'} + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: true + + /lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + dev: true + + /lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /log4js@6.7.0: + resolution: {integrity: sha512-KA0W9ffgNBLDj6fZCq/lRbgR6ABAodRIDHrZnS48vOtfKa4PzWImb0Md1lmGCdO3n3sbCm/n1/WmrNlZ8kCI3Q==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4 + flatted: 3.2.7 + rfdc: 1.3.0 + streamroller: 3.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /luxon@1.28.0: + resolution: {integrity: sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==} + dev: true + + /luxon@2.5.0: + resolution: {integrity: sha512-IDkEPB80Rb6gCAU+FEib0t4FeJ4uVOuX1CQ9GsvU3O+JAGIgu0J7sf1OarXKaKDygTZIoJyU6YdZzTFRu+YR0A==} + engines: {node: '>=12'} + + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} + engines: {node: '>=12'} + dev: true + + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + dependencies: + object-visit: 1.0.1 + dev: true + + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + dev: true + + /min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + dependencies: + dom-walk: 0.1.2 + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.1.0: + resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + dev: true + + /mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + dependencies: + minimist: 1.2.7 + dev: true + + /moment-timezone@0.5.40: + resolution: {integrity: sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==} + dependencies: + moment: 2.29.4 + + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nan@2.17.0: + resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} + dev: true + optional: true + + /nanoid@3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + dev: true + + /nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: true + + /normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + dependencies: + remove-trailing-separator: 1.1.0 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /num2fraction@1.2.2: + resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + dev: true + + /object-inspect@1.12.2: + resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /object.hasown@1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: true + + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: true + + /pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + dev: true + + /path-dirname@1.0.2: + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@2.4.0: + resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + dev: true + + /posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + dev: true + + /postcss-advanced-variables@3.0.1: + resolution: {integrity: sha512-JqVjfkmqPoazMobVeQYzbt7djcDGJfMlpwBd9abTqmzWR40tvIUMXpTU5w3riqz7h+wYPY7V6GF8BIXL/ybEfg==} + engines: {node: '>=6.0.0'} + dependencies: + '@csstools/sass-import-resolve': 1.0.0 + postcss: 7.0.39 + dev: true + + /postcss-calc@8.2.4(postcss@8.4.20): + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-colormin@5.3.0(postcss@8.4.20): + resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-comment@2.0.0: + resolution: {integrity: sha512-5zT5iKU7c0tK9KJFNrVf+g1MGTkzf/4V3e0Zzm2g1uoFQC5jeTHmB9O1iAqh97+jnKpc6al204e0pwFUiCwseg==} + dependencies: + postcss: 6.0.23 + dev: true + + /postcss-convert-values@5.1.3(postcss@8.4.20): + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-discard-comments@5.1.2(postcss@8.4.20): + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-discard-duplicates@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-discard-empty@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-discard-overridden@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-load-config@3.1.4(postcss@8.4.20): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.0.6 + postcss: 8.4.20 + yaml: 1.10.2 + dev: true + + /postcss-merge-longhand@5.1.7(postcss@8.4.20): + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.4.20) + dev: true + + /postcss-merge-rules@5.1.3(postcss@8.4.20): + resolution: {integrity: sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-minify-font-values@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-gradients@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-params@5.1.4(postcss@8.4.20): + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + cssnano-utils: 3.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-selectors@5.2.1(postcss@8.4.20): + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-modules-extract-imports@3.0.0(postcss@8.4.20): + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-modules-local-by-default@4.0.0(postcss@8.4.20): + resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-modules-scope@3.0.0(postcss@8.4.20): + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-modules-values@4.0.0(postcss@8.4.20): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.20) + postcss: 8.4.20 + dev: true + + /postcss-modules@4.3.1(postcss@8.4.20): + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.4.20 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.20) + postcss-modules-local-by-default: 4.0.0(postcss@8.4.20) + postcss-modules-scope: 3.0.0(postcss@8.4.20) + postcss-modules-values: 4.0.0(postcss@8.4.20) + string-hash: 1.1.3 + dev: true + + /postcss-nesting@7.0.1: + resolution: {integrity: sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==} + engines: {node: '>=6.0.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-normalize-charset@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + dev: true + + /postcss-normalize-display-values@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-positions@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-string@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-unicode@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-url@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + normalize-url: 6.1.0 + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-whitespace@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-ordered-values@5.1.3(postcss@8.4.20): + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0(postcss@8.4.20) + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-reduce-initial@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + caniuse-api: 3.0.0 + postcss: 8.4.20 + dev: true + + /postcss-reduce-transforms@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-selector-parser@6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-svgo@5.1.0(postcss@8.4.20): + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: true + + /postcss-unique-selectors@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@6.0.23: + resolution: {integrity: sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==} + engines: {node: '>=4.0.0'} + dependencies: + chalk: 2.4.2 + source-map: 0.6.1 + supports-color: 5.5.0 + dev: true + + /postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + dev: true + + /postcss@8.4.20: + resolution: {integrity: sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /preact@10.12.1: + resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + + /promise.series@0.2.0: + resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} + engines: {node: '>=0.12'} + dev: true + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: true + + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: false + + /punycode@2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: true + + /qjobs@1.2.0: + resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==} + engines: {node: '>=0.9'} + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + dev: false + + /querystring@0.2.1: + resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} + engines: {node: '>=0.4.x'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: true + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: true + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: true + + /readable-stream@2.3.7: + resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readable-stream@3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdir-glob@1.1.2: + resolution: {integrity: sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==} + dependencies: + minimatch: 5.1.0 + dev: true + + /readdirp@2.2.1: + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10 + readable-stream: 2.3.7 + transitivePeerDependencies: + - supports-color + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true + + /regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + dev: true + + /regexp.prototype.flags@1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + functions-have-names: 1.2.3 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + dev: true + + /repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + dev: true + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + dev: true + + /resolve@1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve@2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + dependencies: + glob: 7.2.3 + dev: true + + /rollup-plugin-dts@3.0.2(rollup@2.79.1)(typescript@4.9.3): + resolution: {integrity: sha512-hswlsdWu/x7k5pXzaLP6OvKRKcx8Bzprksz9i9mUe72zvt8LvqAb/AZpzs6FkLgmyRaN8B6rUQOVtzA3yEt9Yw==} + engines: {node: '>=v12.22.1'} + peerDependencies: + rollup: ^2.48.0 + typescript: ^4.2.4 + dependencies: + magic-string: 0.25.9 + rollup: 2.79.1 + typescript: 4.9.3 + optionalDependencies: + '@babel/code-frame': 7.23.4 + dev: true + + /rollup-plugin-postcss@4.0.2(postcss@8.4.20): + resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} + engines: {node: '>=10'} + peerDependencies: + postcss: 8.x + dependencies: + chalk: 4.1.2 + concat-with-sourcemaps: 1.1.0 + cssnano: 5.1.14(postcss@8.4.20) + import-cwd: 3.0.0 + p-queue: 6.6.2 + pify: 5.0.0 + postcss: 8.4.20 + postcss-load-config: 3.1.4(postcss@8.4.20) + postcss-modules: 4.3.1(postcss@8.4.20) + promise.series: 0.2.0 + resolve: 1.22.1 + rollup-pluginutils: 2.8.2 + safe-identifier: 0.4.2 + style-inject: 0.3.0 + transitivePeerDependencies: + - ts-node + dev: true + + /rollup-plugin-sourcemaps@0.6.3(@types/node@16.18.3)(rollup@2.79.1): + resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==} + engines: {node: '>=10.0.0'} + peerDependencies: + '@types/node': '>=10.0.0' + rollup: '>=0.31.2' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@types/node': 16.18.3 + rollup: 2.79.1 + source-map-resolve: 0.6.0 + dev: true + + /rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /rrule@2.7.1: + resolution: {integrity: sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==} + dependencies: + tslib: 2.6.2 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-identifier@0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-regex: 1.1.4 + dev: true + + /safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + dependencies: + ret: 0.1.15 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + object-inspect: 1.12.2 + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + dev: true + + /snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /socket.io-adapter@2.4.0: + resolution: {integrity: sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==} + dev: true + + /socket.io-parser@4.2.1: + resolution: {integrity: sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /socket.io@4.5.3: + resolution: {integrity: sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==} + engines: {node: '>=10.0.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + debug: 4.3.4 + engine.io: 6.2.1 + socket.io-adapter: 2.4.0 + socket.io-parser: 4.2.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.0 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + dev: true + + /source-map-resolve@0.6.0: + resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.0 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + dev: true + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: true + + /split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + dev: true + + /stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + dev: true + + /static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + dev: true + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + + /streamroller@3.1.3: + resolution: {integrity: sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4 + fs-extra: 8.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /string-hash@1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string.prototype.matchall@4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + get-intrinsic: 1.1.3 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /style-inject@0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + dev: true + + /stylehacks@5.1.1(postcss@8.4.20): + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.22.1 + postcss: 8.4.20 + postcss-selector-parser: 6.0.11 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: true + + /terminal-columns@1.4.1: + resolution: {integrity: sha512-IKVL/itiMy947XWVv4IHV7a0KQXvKjj4ptbi7Ew9MPMcOLzkiQeyx3Gyvh62hKrfJ0RZc4M1nbhzjNM39Kyujw==} + dev: true + + /terser@4.8.1: + resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} + engines: {node: '>=6.0.0'} + dependencies: + acorn: 8.8.1 + commander: 2.20.3 + source-map: 0.6.1 + source-map-support: 0.5.21 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tmp@0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + dependencies: + rimraf: 3.0.2 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.1.1 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /tsutils@3.21.0(typescript@4.9.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.3 + dev: true + + /turbo-darwin-64@1.6.3: + resolution: {integrity: sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /turbo-darwin-arm64@1.6.3: + resolution: {integrity: sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /turbo-linux-64@1.6.3: + resolution: {integrity: sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /turbo-linux-arm64@1.6.3: + resolution: {integrity: sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /turbo-windows-64@1.6.3: + resolution: {integrity: sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /turbo-windows-arm64@1.6.3: + resolution: {integrity: sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /turbo@1.6.3: + resolution: {integrity: sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw==} + requiresBuild: true + optionalDependencies: + turbo-darwin-64: 1.6.3 + turbo-darwin-arm64: 1.6.3 + turbo-linux-64: 1.6.3 + turbo-linux-arm64: 1.6.3 + turbo-windows-64: 1.6.3 + turbo-windows-arm64: 1.6.3 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-flag@3.0.0: + resolution: {integrity: sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA==} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: true + + /typescript@4.9.3: + resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + engines: {node: '>=4.2.0'} + dev: true + + /ua-parser-js@0.7.32: + resolution: {integrity: sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==} + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + requiresBuild: true + dev: true + optional: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + dev: true + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: true + + /unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + dev: true + + /upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.21.4): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.4 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.1): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.1 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: true + + /urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + dev: true + + /url@0.11.0: + resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: false + + /use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: true + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: true + + /void-elements@2.0.1: + resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==} + engines: {node: '>=0.10.0'} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /whatwg-url@6.5.0: + resolution: {integrity: sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + dependencies: + isexe: 2.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + dependencies: + isexe: 2.0.0 + dev: true + + /word-wrap@1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws@8.2.3: + resolution: {integrity: sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xhr-mock@2.5.1: + resolution: {integrity: sha512-UKOjItqjFgPUwQGPmRAzNBn8eTfIhcGjBVGvKYAWxUQPQsXNGD6KEckGTiHwyaAUp9C9igQlnN1Mp79KWCg7CQ==} + dependencies: + global: 4.4.0 + url: 0.11.0 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zip-stream@4.1.0: + resolution: {integrity: sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 2.1.0 + compress-commons: 4.1.1 + readable-stream: 3.6.0 + dev: true diff --git a/fullcalendar-main/pnpm-workspace.yaml b/fullcalendar-main/pnpm-workspace.yaml new file mode 100644 index 0000000..4093044 --- /dev/null +++ b/fullcalendar-main/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - ./scripts + - ./tests + - ./bundle + - ./packages/* diff --git a/fullcalendar-main/scripts/.eslintrc.cjs b/fullcalendar-main/scripts/.eslintrc.cjs new file mode 100644 index 0000000..a740fdc --- /dev/null +++ b/fullcalendar-main/scripts/.eslintrc.cjs @@ -0,0 +1,5 @@ + +module.exports = { + root: true, + extends: ['./config/eslint.pkg.node.cjs'], +} diff --git a/fullcalendar-main/scripts/bin/standard-scripts.js b/fullcalendar-main/scripts/bin/standard-scripts.js new file mode 100755 index 0000000..8b48e17 --- /dev/null +++ b/fullcalendar-main/scripts/bin/standard-scripts.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +import { join as joinPaths } from 'path' +import { fileURLToPath } from 'url' + +const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..') + +// temporary (for compiling ts) +import { promisify } from 'util' +import { execFile } from 'child_process' +import { copyFile } from 'fs/promises' +await copyFile( + joinPaths(thisPkgDir, 'tsconfig.safe.json'), + joinPaths(thisPkgDir, 'tsconfig.json'), +) +const execFileP = promisify(execFile) +await execFileP( + './node_modules/.bin/tsc', + ['-b'], + { cwd: thisPkgDir, stdio: 'inherit' }, +) + +const stuff = await import('../dist/utils/script-runner.js') +stuff.runScript(thisPkgDir) diff --git a/fullcalendar-main/scripts/config/banner.tpl b/fullcalendar-main/scripts/config/banner.tpl new file mode 100644 index 0000000..910e931 --- /dev/null +++ b/fullcalendar-main/scripts/config/banner.tpl @@ -0,0 +1,5 @@ +/*! +{{ title }} v{{ version }} +Docs & License: {{ homepage }} +(c) {{ copyright }} +*/ diff --git a/fullcalendar-main/scripts/config/eslint.base.cjs b/fullcalendar-main/scripts/config/eslint.base.cjs new file mode 100644 index 0000000..41702b7 --- /dev/null +++ b/fullcalendar-main/scripts/config/eslint.base.cjs @@ -0,0 +1,65 @@ + +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + ], + plugins: [ + '@typescript-eslint', + ], + settings: { + react: { + version: '18.2.0', // can't detect b/c we don't use React. hardcode a recent version + }, + }, + env: { + es2022: true, + }, + rules: { + indent: ['error', 2, { SwitchCase: 1 }], + semi: ['error', 'never'], + quotes: ['error', 'single'], + 'jsx-quotes': ['error', 'prefer-double'], // rethink this? + 'comma-dangle': ['error', { + 'arrays': 'always-multiline', + 'objects': 'always-multiline', + 'imports': 'always-multiline', + 'exports': 'always-multiline', + 'functions': 'always-multiline', // not included in single-value specification + }], + + // typescript will check unknown vars, even for js (with checkJs) + 'no-undef': 'off', + + // jsx + 'react/react-in-jsx-scope': 'off', // not compat w/ Preact (checked in ts anyway) + 'react/display-name': 'off', + + // easy fixes in near-term + '@typescript-eslint/no-unused-vars': 'off', + + // hard fixes in long-term + 'prefer-const': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + + // legitimately want disabled + '@typescript-eslint/no-empty-interface': 'off', // need empty interfaces for decl merging + '@typescript-eslint/no-non-null-assertion': 'off', + + // TODO: merge rules from this legacy file: + // https://github.com/fullcalendar/fullcalendar/blob/v5.11.3/.eslintrc.yml + }, + overrides: [ + { + files: '*.cjs', // at any depth + rules: { + '@typescript-eslint/no-var-requires': 'off', // allow require() statements + }, + }, + ], +} diff --git a/fullcalendar-main/scripts/config/eslint.pkg.browser.cjs b/fullcalendar-main/scripts/config/eslint.pkg.browser.cjs new file mode 100644 index 0000000..513fcf3 --- /dev/null +++ b/fullcalendar-main/scripts/config/eslint.pkg.browser.cjs @@ -0,0 +1,28 @@ + +module.exports = { + extends: [ + './eslint.base.cjs', + ], + ignorePatterns: [ + 'dist', + ], + overrides: [ + { + files: [ + './*.{js,cjs,ts}', + './{scripts,config}/**/*.{js,cjs}', + ], + env: { + node: true, + }, + }, + { + files: [ + './{src,tests}/**/*.{js,ts,jsx,tsx}', + ], + env: { + browser: true, + }, + }, + ], +} diff --git a/fullcalendar-main/scripts/config/eslint.pkg.node.cjs b/fullcalendar-main/scripts/config/eslint.pkg.node.cjs new file mode 100644 index 0000000..76210fa --- /dev/null +++ b/fullcalendar-main/scripts/config/eslint.pkg.node.cjs @@ -0,0 +1,12 @@ + +module.exports = { + extends: [ + './eslint.base.cjs', + ], + ignorePatterns: [ + 'dist', + ], + env: { + node: true, + }, +} diff --git a/fullcalendar-main/scripts/config/karma.d.ts b/fullcalendar-main/scripts/config/karma.d.ts new file mode 100644 index 0000000..9eb768f --- /dev/null +++ b/fullcalendar-main/scripts/config/karma.d.ts @@ -0,0 +1,9 @@ +import karma from 'karma' + +declare function buildKarmaConfig( + pkgFilePaths: string[], + isDev: boolean, + cliArgs: string[], +): karma.ConfigOptions + +export { buildKarmaConfig as default } diff --git a/fullcalendar-main/scripts/config/karma.js b/fullcalendar-main/scripts/config/karma.js new file mode 100644 index 0000000..2318e08 --- /dev/null +++ b/fullcalendar-main/scripts/config/karma.js @@ -0,0 +1,76 @@ +import { createRequire } from 'module' +import karma from 'karma' + +const require = createRequire(import.meta.url) + +export default function(filePaths, isDev, cliArgs) { + const filePathsWithSrcMaps = filePaths + .filter((filePath) => ( + // TODO: must proper built dist files (HACK) + filePath.match(/[\\/]dist[\\/]/) + )) + + const files = [ + require.resolve('jquery'), + require.resolve('jasmine-jquery'), + require.resolve('jquery-simulate'), + require.resolve('components-jqueryui'), + ...filePaths, + ...filePathsWithSrcMaps + .map((path) => ({ + pattern: path.replace(/\.js$/, '.js.map'), + included: false, + })), + ] + + const preprocessors = filePathsWithSrcMaps.reduce((props, distFile) => ( + Object.assign(props, { [distFile]: ['sourcemap'] }) + ), {}) + + return { + singleRun: !isDev, + autoWatch: isDev, + browsers: !isDev ? ['ChromeHeadless_custom'] : [], + client: { cliArgs }, // access via `window.__karma__.config.cliArgs` + + files, + preprocessors, + + plugins: [ + require('karma-chrome-launcher'), + require('karma-jasmine'), + require('karma-sourcemap-loader'), + require('karma-verbose-reporter'), + ], + + // frameworks to use + frameworks: ['jasmine'], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'verbose' + reporters: ['dots'], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG + logLevel: karma.constants.LOG_INFO, + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + customLaunchers: { + ChromeHeadless_custom: { + base: 'ChromeHeadless', + flags: [ + '--no-sandbox', // needed for TravisCI: https://docs.travis-ci.com/user/chrome#Sandboxing + '--window-size=1280,1696', // some tests only work with larger window (w?, h?) + ], + }, + }, + } +} diff --git a/fullcalendar-main/scripts/config/postcss.config.cjs b/fullcalendar-main/scripts/config/postcss.config.cjs new file mode 100644 index 0000000..35712f7 --- /dev/null +++ b/fullcalendar-main/scripts/config/postcss.config.cjs @@ -0,0 +1,12 @@ +/* +NOTE: unfortunately can't rename this file to `postcss.cjs` or error is thrown +*/ + +module.exports = { + parser: require('postcss-comment'), // for "//" style comments + plugins: [ + require('postcss-advanced-variables'), + require('postcss-nesting'), + require('autoprefixer'), + ], +} diff --git a/fullcalendar-main/scripts/config/terser.json b/fullcalendar-main/scripts/config/terser.json new file mode 100644 index 0000000..16c7311 --- /dev/null +++ b/fullcalendar-main/scripts/config/terser.json @@ -0,0 +1,7 @@ +{ + "compress": {}, + "mangle": {}, + "output": {}, + "parse": {}, + "rename": {} +} diff --git a/fullcalendar-main/scripts/config/tsconfig.browser.json b/fullcalendar-main/scripts/config/tsconfig.browser.json new file mode 100644 index 0000000..3d9045b --- /dev/null +++ b/fullcalendar-main/scripts/config/tsconfig.browser.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "es2015", + "moduleResolution": "node16", + "allowJs": true, + "lib": [ + "es2015", + "dom" + ], + "noUnusedLocals": true, + "noImplicitUseStrict": true, + "resolveJsonModule": true, + "jsx": "react", + "jsxFactory": "createElement", + "strictBindCallApply": true, + "skipLibCheck": true, + "sourceMap": true, + "types": [], + "composite": true, + "declaration": true, + "declarationMap": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/fullcalendar-main/scripts/config/tsconfig.node.json b/fullcalendar-main/scripts/config/tsconfig.node.json new file mode 100644 index 0000000..ae767e0 --- /dev/null +++ b/fullcalendar-main/scripts/config/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "module": "es2022", + "moduleResolution": "node16", + "types": ["node"], + "sourceMap": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/fullcalendar-main/scripts/config/turbo.txt b/fullcalendar-main/scripts/config/turbo.txt new file mode 100644 index 0000000..6a38997 --- /dev/null +++ b/fullcalendar-main/scripts/config/turbo.txt @@ -0,0 +1,2 @@ + +The turbo.json needs to be at the monorepo root diff --git a/fullcalendar-main/scripts/package.json b/fullcalendar-main/scripts/package.json new file mode 100644 index 0000000..2d9f956 --- /dev/null +++ b/fullcalendar-main/scripts/package.json @@ -0,0 +1,81 @@ +{ + "private": true, + "name": "@fullcalendar-scripts/standard", + "version": "0.0.0", + "devDependencies": { + "@rollup/plugin-commonjs": "^12.0.0", + "@rollup/plugin-json": "^4.0.3", + "@rollup/plugin-node-resolve": "^14.0.1", + "@rollup/plugin-replace": "^5.0.1", + "@tsconfig/node16": "^1.0.3", + "@types/archiver": "^5.3.1", + "@types/cross-spawn": "^6.0.2", + "@types/js-yaml": "^4.0.5", + "@types/karma": "^6.3.3", + "@types/node": "^16.11.7", + "@types/semver": "^7.3.12", + "@typescript-eslint/eslint-plugin": "^5.40.0", + "@typescript-eslint/parser": "^5.40.0", + "archiver": "^5.3.1", + "autoprefixer": "^9.8.4", + "chalk": "^5.0.1", + "chokidar": "^2.1.5", + "cleye": "^1.2.1", + "components-jqueryui": "^1.12.1", + "cross-spawn": "^7.0.3", + "esbuild": "^0.15.7", + "eslint": "^8.25.0", + "eslint-plugin-react": "^7.31.10", + "globby": "^13.1.2", + "handlebars": "^4.1.2", + "jasmine-jquery": "^2.1.1", + "jquery": "^3.4.0", + "jquery-simulate": "^1.0.2", + "js-yaml": "^4.1.0", + "karma": "^6.3.2", + "karma-chrome-launcher": "^3.1.0", + "karma-jasmine": "^4.0.1", + "karma-sourcemap-loader": "^0.3.8", + "karma-verbose-reporter": "0.0.6", + "postcss": "^8.4.20", + "postcss-advanced-variables": "^3.0.1", + "postcss-comment": "^2.0.0", + "postcss-nesting": "^7.0.1", + "rollup": "^2.79.0", + "rollup-plugin-dts": "^3.0.2", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-sourcemaps": "^0.6.3", + "semver": "^7.3.8", + "terser": "^4.8.0", + "turbo": "^1.5.6", + "typescript": "^4.8.2" + }, + "bin": { + "standard-scripts": "./bin/standard-scripts.js", + "ss": "./bin/standard-scripts.js", + "eslint": "./node_modules/eslint/bin/eslint.js" + }, + "scripts": { + "lint": "eslint .", + "safe-build": "cp tsconfig.safe.json tsconfig.json && tsc -b" + }, + "type": "module", + "tsConfig": { + "extends": "./config/tsconfig.node.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "./src/**/*" + ] + }, + "exports": { + "./package.json": "./package.json", + "./config/*": "./config/*", + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + } +} diff --git a/fullcalendar-main/scripts/patches/jasmine-jquery@2.1.1.patch b/fullcalendar-main/scripts/patches/jasmine-jquery@2.1.1.patch new file mode 100644 index 0000000..7e751a1 --- /dev/null +++ b/fullcalendar-main/scripts/patches/jasmine-jquery@2.1.1.patch @@ -0,0 +1,21 @@ +diff --git a/lib/jasmine-jquery.js b/lib/jasmine-jquery.js +index 8d35a8793832996842e6ea491dce48ef0f345e11..dbe57f3b9c011701e394dcdbe95762f86a1c577f 100644 +--- a/lib/jasmine-jquery.js ++++ b/lib/jasmine-jquery.js +@@ -657,11 +657,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + } + }, + +- toHaveBeenTriggeredOnAndWith: function (j$, customEqualityTesters) { ++ // removed customEqualityTesters ++ // https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#matchers-cet ++ // https://stackoverflow.com/questions/71117936/how-do-i-fix-deprecation-the-matcher-factory-for-tohavebeentriggeredonandwith ++ toHaveBeenTriggeredOnAndWith: function (j$) { + return { + compare: function (actual, selector, expectedArgs) { + var wasTriggered = jasmine.jQuery.events.wasTriggered(selector, actual) +- , result = { pass: wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, actual, expectedArgs, j$, customEqualityTesters) } ++ , result = { pass: wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, actual, expectedArgs, j$) } + + if (wasTriggered) { + var actualArgs = jasmine.jQuery.events.args(selector, actual, expectedArgs)[1] \ No newline at end of file diff --git a/fullcalendar-main/scripts/src/archive.ts b/fullcalendar-main/scripts/src/archive.ts new file mode 100644 index 0000000..b4b46c1 --- /dev/null +++ b/fullcalendar-main/scripts/src/archive.ts @@ -0,0 +1,80 @@ +import { join as joinPaths, dirname, sep as pathSeparator } from 'path' +import { createWriteStream } from 'fs' +import { mkdir, readFile, rm } from 'fs/promises' +import { globby } from 'globby' +import archiver from 'archiver' +import { MonorepoStruct } from './utils/monorepo-struct.js' +import { ScriptContext } from './utils/script-runner.js' +import { getArchiveRootDirs } from './utils/monorepo-config.js' +import { iifeSubextension } from './pkg/utils/config.js' + +export default function(this: ScriptContext) { + return writeMonorepoArchives(this.monorepoStruct) +} + +export async function writeMonorepoArchives(monorepoStruct: MonorepoStruct): Promise<void> { + await Promise.all( + getArchiveRootDirs(monorepoStruct).map((rootDir) => createArchive(rootDir)), + ) +} + +export async function deleteMonorepoArchives(monorepoStruct: MonorepoStruct): Promise<void> { + await Promise.all( + getArchiveRootDirs(monorepoStruct).map((rootDir) => deleteArchives(rootDir)), + ) +} + +async function createArchive(rootDir: string): Promise<void> { + const bundleDir = joinPaths(rootDir, 'bundle') + const bundleJson = await readFile(joinPaths(bundleDir, 'package.json'), 'utf8') + const bundleMeta = JSON.parse(bundleJson) + const archiveId = `${bundleMeta.name}-${bundleMeta.version}` + + const archivePath = joinPaths(rootDir, `dist/${archiveId}.zip`) + await mkdir(dirname(archivePath), { recursive: true }) + + const archiveStream = createWriteStream(archivePath) + archiveStream.on('close', () => { + console.log(`${archive.pointer()} bytes written to ${archivePath}`) + }) + + const archive = archiver('zip', { zlib: { level: 9 } }) + archive.pipe(archiveStream) + + ;['README.md', 'LICENSE.md'].forEach((subpath) => { + archive.file( + joinPaths(rootDir, subpath), + { name: `${archiveId}/${subpath}` }, + ) + }) + + archive.directory(joinPaths(bundleDir, 'examples'), `${archiveId}/examples`) + archive.glob( + `dist/*${iifeSubextension}.+(js|min.js)`, + { cwd: bundleDir }, + { prefix: archiveId }, + ) + + const subpaths = await globby( + `packages/*/dist/**/*${iifeSubextension}.+(js|min.js)`, + { cwd: rootDir }, + ) + + for (const subpath of subpaths) { + const subpathParts = subpath.split(pathSeparator) + subpathParts.splice(2, 1) // remove 'dist' + + archive.file( + joinPaths(rootDir, subpath), + { name: [archiveId].concat(subpathParts).join('/') }, + ) + } + + return archive.finalize() +} + +async function deleteArchives(rootDir: string): Promise<void> { + const distDir = joinPaths(rootDir, 'dist') + + await rm(distDir, { recursive: true, force: true }) +} diff --git a/fullcalendar-main/scripts/src/build.ts b/fullcalendar-main/scripts/src/build.ts new file mode 100644 index 0000000..102f80a --- /dev/null +++ b/fullcalendar-main/scripts/src/build.ts @@ -0,0 +1,12 @@ +import { ScriptContext } from './utils/script-runner.js' +import { writeMonorepoArchives } from './archive.js' +import { runTurboTasks } from './utils/turbo.js' +import { refineFilterArgs } from './utils/monorepo-config.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const monorepoDir = this.cwd + const { monorepoStruct } = this + + await runTurboTasks(monorepoDir, ['build', ...refineFilterArgs(args, monorepoStruct)]) + await writeMonorepoArchives(monorepoStruct) +} diff --git a/fullcalendar-main/scripts/src/clean.ts b/fullcalendar-main/scripts/src/clean.ts new file mode 100644 index 0000000..faa3751 --- /dev/null +++ b/fullcalendar-main/scripts/src/clean.ts @@ -0,0 +1,58 @@ +import { join as joinPaths } from 'path' +import { rm } from 'fs/promises' +import { ScriptContext } from './utils/script-runner.js' +import { deleteMonorepoArchives } from './archive.js' +import { runTurboTasks } from './utils/turbo.js' +import { MonorepoStruct, traverseMonorepoGreedy } from './utils/monorepo-struct.js' +import { cleanPkg } from './pkg/clean.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const { monorepoStruct } = this + const { monorepoDir } = monorepoStruct + const isAll = args.includes('--all') + + await Promise.all([ + deleteRootDist(monorepoDir), + deleteRootTsconfig(monorepoDir), + deleteGlobalTurboCache(monorepoDir), + deleteMonorepoArchives(monorepoStruct), + isAll ? + runTurboTasks(monorepoDir, ['clean']) : + cleanPkgsDirectly(monorepoStruct), + ]) +} + +// for deleting archives (only applies to 'standard') +function deleteRootDist(monorepoDir: string): Promise<void> { + return rm( + joinPaths(monorepoDir, 'dist'), + { force: true }, + ) +} + +function deleteRootTsconfig(monorepoDir: string): Promise<void> { + return rm( + joinPaths(monorepoDir, 'tsconfig.json'), + { force: true }, + ) +} + +function deleteGlobalTurboCache(monorepoDir: string): Promise<void> { + return rm( + joinPaths(monorepoDir, 'node_modules/.cache/turbo'), + { force: true, recursive: true }, + ) +} + +function cleanPkgsDirectly(monorepoStruct: MonorepoStruct): Promise<void> { + return traverseMonorepoGreedy(monorepoStruct, (pkgStruct) => { + const { pkgJson } = pkgStruct + + if ( + pkgJson.buildConfig || + pkgJson.tsConfig + ) { + return cleanPkg(pkgStruct.pkgDir) + } + }) +} diff --git a/fullcalendar-main/scripts/src/dev.ts b/fullcalendar-main/scripts/src/dev.ts new file mode 100644 index 0000000..d354e6c --- /dev/null +++ b/fullcalendar-main/scripts/src/dev.ts @@ -0,0 +1,60 @@ +import { writeDistPkgJsons } from './json.js' +import { deleteBuiltFiles } from './pkg/build.js' +import { watchBundles } from './pkg/bundle.js' +import { + MonorepoStruct, + PkgStruct, + traverseMonorepo, + watchMonorepo, +} from './utils/monorepo-struct.js' +import { watchTs, writeTsconfigs } from './utils/monorepo-ts.js' +import { untilSigInt } from './utils/process.js' +import { ScriptContext } from './utils/script-runner.js' + +// TODO: if error with rollup, kill typescript, and vice-versa + +export default async function(this: ScriptContext) { + const monorepoDir = this.cwd + const initialMonorepoStruct = this.monorepoStruct + + async function handleMonorepo(monorepoStruct: MonorepoStruct) { + // Clear previous bundles + // TODO: have watchBundles/writeBundles do this automatically + // (but don't clear package.json) + await traverseMonorepo(monorepoStruct, async (pkgStruct: PkgStruct) => { + const { pkgDir, pkgJson } = pkgStruct + + if (pkgJson.buildConfig) { + await deleteBuiltFiles(pkgDir) + } + }) + + await writeTsconfigs(monorepoStruct) + await writeDistPkgJsons(monorepoStruct, true) // isDev=true + + // tsc needs tsconfig.json and package.json from above + const stopTs = await watchTs(monorepoDir, ['--pretty', '--preserveWatchOutput']) + + const stopPkgs = await traverseMonorepo(monorepoStruct, async (pkgStruct: PkgStruct) => { + const { pkgDir, pkgJson } = pkgStruct + + if (pkgJson.buildConfig) { + return watchBundles(pkgDir, pkgJson, monorepoStruct, true) // isDev=true + } + }) + + return () => { // a "stop" function + stopTs() + stopPkgs() + } + } + + const stopMonorepo = await watchMonorepo( + monorepoDir, + handleMonorepo, + initialMonorepoStruct, + ) + + await untilSigInt() + stopMonorepo() +} diff --git a/fullcalendar-main/scripts/src/globals.d.ts b/fullcalendar-main/scripts/src/globals.d.ts new file mode 100644 index 0000000..3c06b20 --- /dev/null +++ b/fullcalendar-main/scripts/src/globals.d.ts @@ -0,0 +1,3 @@ + +declare module '@rollup/plugin-node-resolve'; +declare module 'rollup-plugin-dts'; diff --git a/fullcalendar-main/scripts/src/json.ts b/fullcalendar-main/scripts/src/json.ts new file mode 100644 index 0000000..0854bd7 --- /dev/null +++ b/fullcalendar-main/scripts/src/json.ts @@ -0,0 +1,30 @@ +import { join as joinPaths } from 'path' +import { fileExists } from './utils/fs.js' +import { ScriptContext } from './utils/script-runner.js' +import { MonorepoStruct, PkgStruct, traverseMonorepoGreedy } from './utils/monorepo-struct.js' +import { writeDistPkgJson } from './pkg/json.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const isDev = args.includes('--dev') + + await writeDistPkgJsons(this.monorepoStruct, isDev) +} + +export function writeDistPkgJsons( + monorepoStruct: MonorepoStruct, + isDev: boolean, + reuseExisting = false, +) { + return traverseMonorepoGreedy(monorepoStruct, async (pkgStruct: PkgStruct) => { + const { pkgDir, pkgJson } = pkgStruct + + if (pkgJson.buildConfig) { + if ( + !reuseExisting || + !(await fileExists(joinPaths(pkgDir, 'dist/package.json'))) + ) { + await writeDistPkgJson(pkgDir, pkgJson, isDev) + } + } + }) +} diff --git a/fullcalendar-main/scripts/src/lint.ts b/fullcalendar-main/scripts/src/lint.ts new file mode 100644 index 0000000..8463321 --- /dev/null +++ b/fullcalendar-main/scripts/src/lint.ts @@ -0,0 +1,10 @@ +import { refineFilterArgs } from './utils/monorepo-config.js' +import { ScriptContext } from './utils/script-runner.js' +import { runTurboTasks } from './utils/turbo.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const monorepoDir = this.cwd + const { monorepoStruct } = this + + runTurboTasks(monorepoDir, ['lint', ...refineFilterArgs(args, monorepoStruct)]) +} diff --git a/fullcalendar-main/scripts/src/pkg/build.ts b/fullcalendar-main/scripts/src/pkg/build.ts new file mode 100644 index 0000000..3f0657d --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/build.ts @@ -0,0 +1,85 @@ +import { join as joinPaths } from 'path' +import { copyFile, rm, writeFile } from 'fs/promises' +import { globby } from 'globby' +import { writeDistPkgJson } from './json.js' +import { analyzePkg, PkgAnalysis } from '../utils/pkg-analysis.js' +import { ScriptContext } from '../utils/script-runner.js' +import { writeBundles } from './bundle.js' +import { compileTs, writeTsconfigs } from '../utils/monorepo-ts.js' +import { MonorepoStruct } from '../utils/monorepo-struct.js' + +const tscArtifacts = [ + '.tsout', + 'tsconfig.tsbuildinfo', +] + +const pathsToDelete = [ + './dist/*', + // leave tscArtifacts + ...tscArtifacts.map((artifact) => `!./dist/${artifact}`), +] + +export default async function(this: ScriptContext, ...args: string[]) { + const { monorepoStruct } = this + const pkgDir = this.cwd + const isDev = args.includes('--dev') + + await buildPkg(pkgDir, monorepoStruct, isDev) +} + +export async function buildPkg(pkgDir: string, monorepoStruct: MonorepoStruct, isDev: boolean) { + const pkgJson = monorepoStruct.pkgDirToJson[pkgDir] + const pkgAnalysis = analyzePkg(pkgDir) + const { isTests } = pkgAnalysis + + await deleteBuiltFiles(pkgDir) + await writeTsconfigs(monorepoStruct, pkgDir) + + if (!isTests) { + await writeDistPkgJson(pkgDir, pkgJson, isDev) + } + + // tsc needs tsconfig.json and package.json from above + await compileTs(pkgDir) + + await Promise.all([ + writeBundles(pkgDir, pkgJson, monorepoStruct, isDev), + !isTests && writeDistNpmIgnore(pkgDir), + !isTests && writeDistReadme(pkgDir), // needs dist folder + !isTests && writeDistLicense(pkgAnalysis), // needs dist folder + ]) +} + +export async function writeDistNpmIgnore(pkgDir: string): Promise<void> { + await writeFile( + joinPaths(pkgDir, 'dist', '.npmignore'), + tscArtifacts.join('\n') + '\n', + ) +} + +export async function writeDistReadme(pkgDir: string): Promise<void> { + await copyFile( + joinPaths(pkgDir, 'README.md'), + joinPaths(pkgDir, 'dist', 'README.md'), + ) +} + +export async function writeDistLicense(pkgAnalysis: PkgAnalysis): Promise<void> { + await copyFile( + joinPaths(pkgAnalysis.metaRootDir, 'LICENSE.md'), + joinPaths(pkgAnalysis.pkgDir, 'dist', 'LICENSE.md'), + ) +} + +export async function deleteBuiltFiles(pkgDir: string): Promise<void> { + const relPaths = await globby(pathsToDelete, { cwd: pkgDir, onlyFiles: false }) + + await Promise.all( + relPaths.map(async (relPath) => { + await rm( + joinPaths(pkgDir, relPath), + { force: true, recursive: true }, + ) + }), + ) +} diff --git a/fullcalendar-main/scripts/src/pkg/bundle.ts b/fullcalendar-main/scripts/src/pkg/bundle.ts new file mode 100644 index 0000000..41969ad --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/bundle.ts @@ -0,0 +1,133 @@ +import { basename } from 'path' +import { watch } from 'chokidar' +import { rollup, watch as rollupWatch, RollupOptions, OutputOptions } from 'rollup' +import { MonorepoStruct } from '../utils/monorepo-struct.js' +import { buildPkgBundleStruct, PkgBundleStruct } from './utils/bundle-struct.js' +import { analyzePkg } from '../utils/pkg-analysis.js' +import { buildEsmOptions, buildCjsOptions, buildIifeOptions, buildDtsOptions } from './utils/rollup-presets.js' +import { arrayify, continuousAsync } from '../utils/lang.js' +import { ScriptContext } from '../utils/script-runner.js' +import { untilSigInt } from '../utils/process.js' +import { pkgLog } from '../utils/log.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const { monorepoStruct } = this + const pkgDir = this.cwd + const pkgJson = monorepoStruct.pkgDirToJson[pkgDir] + + const isWatch = args.includes('--watch') + const isDev = args.includes('--dev') + + if (!isWatch) { + await writeBundles(pkgDir, pkgJson, monorepoStruct, isDev) + } else { + const stopWatch = await watchBundles(pkgDir, pkgJson, monorepoStruct, isDev) + + await untilSigInt() + stopWatch() + } +} + +export async function writeBundles( + pkgDir: string, + pkgJson: any, + monorepoStruct: MonorepoStruct, + isDev: boolean, +): Promise<void> { + const pkgBundleStruct = await buildPkgBundleStruct(pkgDir, pkgJson) + const optionsObjs = await buildRollupOptionObjs(pkgBundleStruct, monorepoStruct, isDev) + + await Promise.all( + optionsObjs.map(async (options) => { + const bundle = await rollup(options) + const outputOptionObjs: OutputOptions[] = arrayify(options.output) + + await Promise.all( + outputOptionObjs.map((outputOptions) => bundle.write(outputOptions)), + ) + }), + ) +} + +export async function watchBundles( + pkgDir: string, + pkgJson: any, + monorepoStruct: MonorepoStruct, + isDev: boolean, +): Promise<() => void> { + return continuousAsync(async (rerun: any) => { + const pkgName = pkgJson.name + const pkgBundleStruct = await buildPkgBundleStruct(pkgDir, pkgJson) + const optionsObjs = await buildRollupOptionObjs(pkgBundleStruct, monorepoStruct, isDev) + + const rollupWatcher = rollupWatch(optionsObjs) + await new Promise<void>((resolve) => { + rollupWatcher.on('event', (ev) => { + switch (ev.code) { + case 'ERROR': + console.error(ev.error) + break + case 'BUNDLE_END': + pkgLog(pkgName, formatWriteMessage(ev.input, ev.output as string[])) + break + case 'END': + resolve() + break + } + }) + }) + + const fileWatcher = watch(pkgBundleStruct.miscWatchPaths, { ignoreInitial: true }) + fileWatcher.once('all', () => { + pkgLog(pkgName, 'Misc file change detected. Rebuilding all.') + rerun() + }) + + return () => { + rollupWatcher.close() + fileWatcher.close() + } + }) +} + +async function buildRollupOptionObjs( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + isDev: boolean, +): Promise<RollupOptions[]> { + const { isBundle, isTests } = analyzePkg(pkgBundleStruct.pkgDir) + + const esm = !isTests + const cjs = !isDev && !isTests + const moduleSourcemap = isDev || isTests + const iife = true // !isDev || isBundle || isTests + const iifeMinify = !isDev && !isTests + const iifeSourcemap = (isBundle && isDev) || isTests + const dts = !isDev && !isTests + + return [ + ...(esm ? [buildEsmOptions(pkgBundleStruct, monorepoStruct, moduleSourcemap)] : []), + ...(cjs ? [buildCjsOptions(pkgBundleStruct, monorepoStruct, moduleSourcemap)] : []), + ...(iife ? await buildIifeOptions(pkgBundleStruct, monorepoStruct, iifeMinify, iifeSourcemap) : []), + ...(dts ? [buildDtsOptions(pkgBundleStruct)] : []), + ] +} + +function formatWriteMessage(input: any, outputPaths: string[]): string { + const inputPaths: string[] = typeof input === 'object' ? Object.values(input) : [input] + const inputNames = inputPaths.map((inputPath) => basename(inputPath)) + const outputNames = outputPaths.map((outputPath) => basename(outputPath)) + + return `Wrote ${formatNames(inputNames)} to ${formatNames(outputNames)}` +} + +function formatNames(names: string[]) { + if (names.length <= 2) { + return names.join(', ') + } else { + const otherCnt = names.length - 2 + + return names.slice(0, 2).join(', ') + ', and ' + + otherCnt + ' ' + (otherCnt === 1 ? 'other' : 'others') + } +} diff --git a/fullcalendar-main/scripts/src/pkg/clean.ts b/fullcalendar-main/scripts/src/pkg/clean.ts new file mode 100644 index 0000000..2ca8e9e --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/clean.ts @@ -0,0 +1,27 @@ +import { join as joinPaths } from 'path' +import { rm } from 'fs/promises' +import { ScriptContext } from '../utils/script-runner.js' + +const pathsToDelete = [ + './dist', + './tsconfig.json', + './tsconfig.tsbuildinfo', // for when pkg transpiles directly into dist + './.turbo', +] + +export default async function(this: ScriptContext) { + const pkgDir = this.cwd + + await cleanPkg(pkgDir) +} + +export async function cleanPkg(pkgDir: string): Promise<void> { + await Promise.all( + pathsToDelete.map((path) => { + return rm( + joinPaths(pkgDir, path), + { force: true, recursive: true }, + ) + }), + ) +} diff --git a/fullcalendar-main/scripts/src/pkg/json.ts b/fullcalendar-main/scripts/src/pkg/json.ts new file mode 100644 index 0000000..38d5040 --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/json.ts @@ -0,0 +1,113 @@ +import { join as joinPaths, relative as relativizePath } from 'path' +import { mkdir } from 'fs/promises' +import { analyzePkg } from '../utils/pkg-analysis.js' +import { readPkgJson, writePkgJson } from '../utils/pkg-json.js' +import { ScriptContext } from '../utils/script-runner.js' +import { cjsExtension, esmExtension, iifeSubextension } from './utils/config.js' + +const cdnFields = [ + 'unpkg', + 'jsdelivr', +] + +export default async function(this: ScriptContext, ...args: string[]) { + const isDev = args.includes('--dev') + const pkgDir = this.cwd + const pkgJson = this.monorepoStruct.pkgDirToJson[pkgDir] + + await writeDistPkgJson(pkgDir, pkgJson, isDev) +} + +/* +Ensures the dist directory is created +*/ +export async function writeDistPkgJson( + pkgDir: string, + pkgJson: any, + isDev: boolean, +): Promise<void> { + const { buildConfig } = pkgJson + + if (!buildConfig) { + throw new Error('Can only generate dist package.json for a buildConfig') + } + + const pkgAnalysis = analyzePkg(pkgDir) + const basePkgJson = await readPkgJson(pkgAnalysis.metaRootDir) + const typesRoot = isDev ? './.tsout' : '.' // TODO: make config var for .tsout? + + const entryConfigMap = buildConfig.exports + const exportsMap: any = { + './package.json': './package.json', + } + + for (const entryName in entryConfigMap) { + const entrySubpath = entryName === '.' ? './index' : entryName + + // inter-package imports use explicit extensions to avoid format confusion + exportsMap[entrySubpath + cjsExtension] = entrySubpath + cjsExtension + exportsMap[entrySubpath + esmExtension] = entrySubpath + esmExtension + + exportsMap[entryName] = { + types: entrySubpath.replace(/^\./, typesRoot) + '.d.ts', // tsc likes this first + require: entrySubpath + cjsExtension, + import: entrySubpath + esmExtension, + } + } + + const finalPkgJson = { + ...pkgJson, // hack to prefer key order of original file + ...basePkgJson, + ...pkgJson, // overrides base + keywords: (basePkgJson.keywords || []).concat(pkgJson.keywords || []), + types: `${typesRoot}/index.d.ts`, + main: './index' + cjsExtension, + module: './index' + esmExtension, + ...cdnFields.reduce( + (props, cdnField) => Object.assign(props, { + [cdnField]: './index' + iifeSubextension + '.min.js', + }), + {}, + ), + exports: exportsMap, + } + + // add typesVersions as a fallback for build systems that don't understand export maps + if (isDev) { + const typeVersionsEntryMap: any = {} + + // TODO: use mapProps + for (const entryName in entryConfigMap) { + const entryAlias = entryName.replace(/^\.\/?/, '') || 'index' + + typeVersionsEntryMap[entryAlias] = [`.tsout/${entryAlias}.d.ts`] + } + + finalPkgJson.typesVersions = { '*': typeVersionsEntryMap } + } + + if ( + pkgJson.sideEffects === undefined && + !pkgAnalysis.isTests && + !pkgAnalysis.isBundle + ) { + finalPkgJson.sideEffects = false + } + + finalPkgJson.repository.directory = + (basePkgJson.repository.directory ? `${basePkgJson.repository.directory}/` : '') + + relativizePath(pkgAnalysis.metaRootDir, pkgDir) + + delete finalPkgJson.scripts + delete finalPkgJson.devDependencies + delete finalPkgJson.tsConfig + delete finalPkgJson.buildConfig + delete finalPkgJson.publishConfig + delete finalPkgJson.private + delete finalPkgJson.pnpm + delete finalPkgJson.engines + + const distDir = joinPaths(pkgDir, 'dist') + await mkdir(distDir, { recursive: true }) + await writePkgJson(distDir, finalPkgJson) +} diff --git a/fullcalendar-main/scripts/src/pkg/test.ts b/fullcalendar-main/scripts/src/pkg/test.ts new file mode 100644 index 0000000..d2fa261 --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/test.ts @@ -0,0 +1,114 @@ +import { join as joinPaths } from 'path' +import karma from 'karma' +import buildKarmaConfig from '../../config/karma.js' +import { ScriptContext } from '../utils/script-runner.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const pkgDir = this.cwd + const pkgJson = this.monorepoStruct.pkgDirToJson[pkgDir] + const karmaConfig = pkgJson.karmaConfig + + if (!karmaConfig) { + throw new Error('Package being tested must have karmaConfig') + } + + // TODO: util for this + const flagArgs: string[] = [] + const orderedArgs: string[] = [] + for (let arg of args) { + if (arg.startsWith('-')) { + flagArgs.push(arg) + } else { + orderedArgs.push(arg) + } + } + + const isDev = flagArgs.includes('--dev') + const suiteConfigs = normalizeSuites(karmaConfig) + const suiteNames = orderedArgs.length ? + orderedArgs : + (isDev ? ['default'] : Object.keys(suiteConfigs)) + + for (const suiteName of suiteNames) { + const suiteConfig = suiteConfigs[suiteName] + const server = await createKarmaServer( + pkgDir, + suiteConfig.files, + isDev, + flagArgs, + ) + + server.start() + + if (!isDev) { + await untilKarmaSuccess(server) + } + } +} + +// Config +// ------------------------------------------------------------------------------------------------- + +type SuiteConfigMap = { [suiteName: string]: SuiteConfig } + +interface SuiteConfig { + files: string[] +} + +function normalizeSuites(karmaConfig: any): SuiteConfigMap { + const suites = { ...karmaConfig.suites } + + if (karmaConfig.files) { + suites.default = { files: karmaConfig.files } + } + + return suites +} + +// Karma Server +// ------------------------------------------------------------------------------------------------- + +async function createKarmaServer( + pkgDir: string, + filePaths: string[], + isDev: boolean, + cliArgs: string[], +): Promise<karma.Server> { + const absPaths = filePaths.map((filePath) => joinPaths(pkgDir, filePath)) + + // karma JS API: https://karma-runner.github.io/6.4/dev/public-api.html + const parsedConfig = await karma.config.parseConfig( + undefined, + buildKarmaConfig(absPaths, isDev, cliArgs), + { + promiseConfig: true, + throwErrors: true, + }, + ) + + return new karma.Server(parsedConfig, (exitCode) => { + if (exitCode !== 0) { + process.exit(exitCode) + } + }) +} + +function untilKarmaSuccess(server: karma.Server): Promise<void> { + const onSigInt = () => { + server.stop().then(() => process.exit(1)) + } + + process.on('SIGINT', onSigInt) + + return new Promise<void>((resolve, reject) => { + server.on('run_complete', (browsers, testResults) => { + process.off('SIGINT', onSigInt) + + if (testResults.exitCode === 0) { + resolve() + } else { + process.exit(testResults.exitCode) + } + }) + }) +} diff --git a/fullcalendar-main/scripts/src/pkg/utils/bundle-struct.ts b/fullcalendar-main/scripts/src/pkg/utils/bundle-struct.ts new file mode 100644 index 0000000..ba8e734 --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/utils/bundle-struct.ts @@ -0,0 +1,386 @@ +import { join as joinPaths } from 'path' +import { globby } from 'globby' +import { MonorepoStruct, computeLocalDepDirs } from '../../utils/monorepo-struct.js' +import { filterProps } from '../../utils/lang.js' +import { pkgLog } from '../../utils/log.js' +import { srcExtensions, transpiledSubdir, transpiledExtension, srcIifeSubextension } from './config.js' + +export interface PkgBundleStruct { + pkgDir: string, + pkgJson: any + entryConfigMap: EntryConfigMap + entryStructMap: { [entryAlias: string]: EntryStruct } // entryAlias like "index" + iifeGlobalsMap: IifeGlobalsMap + miscWatchPaths: string[] +} + +export interface EntryConfig { + generator?: string + iifeGenerator?: string + iife?: boolean +} + +export interface EntryStruct { + entryGlob: string // like "." or "./locales/*" + entrySrcPath: string // transpiled src. like "<absroot>/index.js" + entrySrcBase: string // transpiled src. like "<absroot>/index" + content?: string +} + +export interface PkgJsonBuildConfig { + exports?: EntryConfigMap + iifeGlobals?: IifeGlobalsMap +} + +export type EntryConfigMap = { [entryGlob: string]: EntryConfig } +export type EntryStructMap = { [entryAlias: string]: EntryStruct } +export type IifeGlobalsMap = { [importPath: string]: string } + +export type GeneratorFunc = ( + config: { pkgDir: string, entryGlob: string, log: (message: string) => void } +) => (string | { [entryName: string]: string }) + +export type IifeGeneratorFunc = ( + config: { pkgDir: string, entryAlias: string, log: (message: string) => void } +) => string + +export type WatchPathsFunc = (pkgDir: string) => string[] + +export async function buildPkgBundleStruct( + pkgDir: string, + pkgJson: any, +): Promise<PkgBundleStruct> { + const buildConfig: PkgJsonBuildConfig = pkgJson.buildConfig || {} + const entryConfigMap: EntryConfigMap = buildConfig.exports || {} + const entryStructMap: { [entryAlias: string]: EntryStruct } = {} + const iifeGlobalsMap: IifeGlobalsMap = buildConfig.iifeGlobals || {} + const miscWatchPaths: string[] = [] + + await Promise.all( + Object.keys(entryConfigMap).map(async (entryGlob) => { + const entryConfig = entryConfigMap[entryGlob] + const newEntryStructMap = entryConfig.generator ? + await generateEntryStructMap(pkgDir, pkgJson, entryGlob, entryConfig.generator, miscWatchPaths) : + await unglobEntryStructMap(pkgDir, entryGlob) + + Object.assign(entryStructMap, newEntryStructMap) + }), + ) + + return { pkgDir, pkgJson, entryConfigMap, entryStructMap, iifeGlobalsMap, miscWatchPaths } +} + +// Source-File Entrypoints +// ------------------------------------------------------------------------------------------------- + +async function unglobEntryStructMap( + pkgDir: string, + entryGlob: string, +): Promise<EntryStructMap> { + const entryStructMap: EntryStructMap = {} + const massagedGlob = + (entryGlob === '.' ? 'index' : removeDotSlash(entryGlob)) + + '{' + srcExtensions.join(',') + '}' + + const transpiledDir = joinPaths(pkgDir, transpiledSubdir) + const srcDir = joinPaths(pkgDir, 'src') + const srcPaths = await globby(massagedGlob, { cwd: srcDir }) + + if (!srcPaths.length) { + throw new Error(`Glob '${entryGlob}' does not exist in package '${pkgDir}'`) + } + + for (const srcPath of srcPaths) { + for (const srcExtension of srcExtensions) { + if (srcPath.endsWith(srcExtension)) { + const entryAlias = srcPath.substring(0, srcPath.length - srcExtension.length) + const entrySrcBase = joinPaths(transpiledDir, entryAlias) + const entrySrcPath = entrySrcBase + transpiledExtension + + entryStructMap[entryAlias] = { entryGlob, entrySrcPath, entrySrcBase } + } + } + } + + return entryStructMap +} + +// Dynamically-Generated Entrypoint Content +// ------------------------------------------------------------------------------------------------- + +async function generateEntryStructMap( + pkgDir: string, + pkgJson: any, + entryGlob: string, + generatorSubpath: string, + miscWatchPaths: string[], // pass-by-reference, modified +): Promise<EntryStructMap> { + const generatorPath = joinPaths(pkgDir, generatorSubpath) + const generatorExports = await import(generatorPath) + const generatorFunc: GeneratorFunc = generatorExports.default + + if (typeof generatorFunc !== 'function') { + throw new Error('Generator must have a default function export') + } + + const generatorConfig = { pkgDir, entryGlob, log: pkgLog.bind(undefined, pkgJson.name) } + const generatorRes = await generatorFunc(generatorConfig) + + const transpiledDir = joinPaths(pkgDir, transpiledSubdir) + const entryStructMap: EntryStructMap = {} + + if (typeof generatorRes === 'string') { + if (entryGlob.includes('*')) { + throw new Error('Generator string output can\'t have blob entrypoint name') + } + + const entrySrcBase = joinPaths(transpiledDir, entryGlob) + const entrySrcPath = entrySrcBase + transpiledExtension + const entryAlias = removeDotSlash(entryGlob) + + entryStructMap[entryAlias] = { + entryGlob, + entrySrcPath, + entrySrcBase, + content: generatorRes, + } + } else if (typeof generatorRes === 'object') { + if (entryGlob.includes('*')) { + throw new Error('Generator object output must have blob entrypoint name') + } + + for (const key in generatorRes) { + const entryAlias = removeDotSlash(entryGlob).replace('*', key) + const entrySrcBase = joinPaths(transpiledDir, entryAlias) + const entrySrcPath = entrySrcBase + transpiledExtension + + entryStructMap[entryAlias] = { + entryGlob, + entrySrcPath, + entrySrcBase, + content: generatorRes[key], + } + } + } else { + throw new Error('Invalid type of generator output') + } + + miscWatchPaths.push( + generatorPath, + ...(generatorExports.getWatchPaths ? generatorExports.getWatchPaths(generatorConfig) : []), + ) + + return entryStructMap +} + +export function entryStructsToContentMap( + entryStructMap: EntryStructMap, +): { [path: string]: string } { + const contentMap: { [path: string]: string } = {} + + for (const entryAlias in entryStructMap) { + const entryStruct = entryStructMap[entryAlias] + + if (typeof entryStruct.content === 'string') { + contentMap[entryStruct.entrySrcPath] = entryStruct.content + } + } + + return contentMap +} + +export async function generateIifeContent( + pkgBundleStruct: PkgBundleStruct, +): Promise<{ [path: string]: string }> { + const { pkgDir, entryConfigMap, entryStructMap } = pkgBundleStruct + const contentMap: { [path: string]: string } = {} + + for (const entryAlias in entryStructMap) { + const entryStruct = entryStructMap[entryAlias] + const entryConfig = entryConfigMap[entryStruct.entryGlob] + const { iifeGenerator } = entryConfig + + if (iifeGenerator) { + const iifeGeneratorPath = joinPaths(pkgDir, iifeGenerator) + const iifeGeneratorExports = await import(iifeGeneratorPath) + const iifeGeneratorFunc: IifeGeneratorFunc = iifeGeneratorExports.default + + if (typeof iifeGeneratorFunc !== 'function') { + throw new Error('iifeGenerator must have a default function export') + } + + const iifeGeneratorConfig = { + pkgDir, + entryAlias, + log: pkgLog.bind(undefined, pkgBundleStruct.pkgJson.name), + } + const iifeGeneratorRes = await iifeGeneratorFunc(iifeGeneratorConfig) + + if (typeof iifeGeneratorRes !== 'string') { + throw new Error('iifeGenerator must return a string') + } + + const transpiledDir = joinPaths(pkgDir, transpiledSubdir) + const transpiledPath = joinPaths(transpiledDir, entryAlias) + + srcIifeSubextension + transpiledExtension + + contentMap[transpiledPath] = iifeGeneratorRes + + pkgBundleStruct.miscWatchPaths.push( // HACK: modify passed-in struct + iifeGeneratorPath, + ...(iifeGeneratorExports.getWatchPaths ? + iifeGeneratorExports.getWatchPaths(iifeGeneratorConfig) : + []), + ) + } + } + + return contentMap +} + +// External Packages +// ------------------------------------------------------------------------------------------------- + +export function computeExternalPkgs(pkgBundleStruct: PkgBundleStruct): string[] { + const { pkgJson } = pkgBundleStruct + + return Object.keys({ + ...pkgJson.dependencies, + ...pkgJson.peerDependencies, + ...pkgJson.optionalDependencies, + }) +} + +/* +For IIFE, some third-party packages are bundled +*/ +export function computeIifeExternalPkgs(pkgBundleStruct: PkgBundleStruct): string[] { + const { iifeGlobalsMap } = pkgBundleStruct + + return computeExternalPkgs(pkgBundleStruct) + .filter((pkgName) => ( + iifeGlobalsMap[pkgName] !== '' && + iifeGlobalsMap['*'] !== '' + )) +} + +export function splitPkgNames( + pkgNames: string[], + monorepoStruct: MonorepoStruct, +): { ourPkgNames: string[], theirPkgNames: string[] } { + const ourPkgNames: string[] = [] + const theirPkgNames: string[] = [] + + for (let pkgName of pkgNames) { + if (monorepoStruct.pkgNameToDir[pkgName]) { + ourPkgNames.push(pkgName) + } else { + theirPkgNames.push(pkgName) + } + } + + return { ourPkgNames, theirPkgNames } +} + +// External File Paths +// ------------------------------------------------------------------------------------------------- + +export function computeOwnExternalPaths(pkgBundleStruct: PkgBundleStruct): string[] { + return Object.values(pkgBundleStruct.entryStructMap) + .map((entryStruct) => entryStruct.entrySrcPath) +} + +export function computeOwnIifeExternalPaths( + currentEntryStruct: EntryStruct, + pkgBundleStruct: PkgBundleStruct, +): string[] { + const { entryStructMap, iifeGlobalsMap } = pkgBundleStruct + const currentGlobalName = iifeGlobalsMap[currentEntryStruct.entryGlob] + + const iifeEntryStructMap = filterProps(entryStructMap, (entryStruct) => { + const globalName = iifeGlobalsMap[entryStruct.entryGlob] + + return Boolean( + // not the current entrypoint + entryStruct.entryGlob !== currentEntryStruct.entryGlob && + // has a global variable + globalName && + // not nested within current global variable + (!currentGlobalName || !globalName.startsWith(currentGlobalName + '.')), + ) + }) + + return Object.values(iifeEntryStructMap) + .map((entryStruct) => entryStruct.entrySrcPath) +} + +// IIFE Browser Globals +// ------------------------------------------------------------------------------------------------- + +export function computeIifeGlobals( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, +): IifeGlobalsMap { + const allGlobalsMap: IifeGlobalsMap = {} + + const { pkgJson, entryStructMap, iifeGlobalsMap } = pkgBundleStruct + const pkgName = pkgJson.name + + // scan the package's own unglobbed entrypoints + for (const entryAlias in entryStructMap) { + const { entrySrcPath, entryGlob } = entryStructMap[entryAlias] + const globalName = iifeGlobalsMap[entryGlob] + + if (globalName) { + const fullImportId = entryGlob === '.' ? + pkgName : + pkgName + '/' + entryAlias + + allGlobalsMap[fullImportId] = globalName + allGlobalsMap[entrySrcPath] = globalName // add file path too + } + } + + // scan the package's external dependencies + // TODO: scan dependencies of dependencies (or just do a global scan) + for (const importId in iifeGlobalsMap) { + const globalName = iifeGlobalsMap[importId] + + if (globalName) { + if (importId !== '.' && !importId.startsWith('./')) { + allGlobalsMap[importId] = globalName + } + } + } + + const depDirs = computeLocalDepDirs(monorepoStruct, pkgJson) + const depPkgJsons = depDirs.map((depDir) => monorepoStruct.pkgDirToJson[depDir]) + + // scan the package's dependencies that live in the monorepo + for (const depPkgJson of depPkgJsons) { + const depPkgName = depPkgJson.name + const depBuildConfig: PkgJsonBuildConfig = depPkgJson.buildConfig || {} + const depIifeGlobalsMap = depBuildConfig.iifeGlobals || {} + + for (const importId in depIifeGlobalsMap) { + const globalName = depIifeGlobalsMap[importId] + + if (globalName) { + if (importId === '.') { + allGlobalsMap[depPkgName] = globalName + } else if (importId.startsWith('./')) { + allGlobalsMap[depPkgName + importId.substring(1)] = globalName + } + } + } + } + + return allGlobalsMap +} + +// Utils +// ------------------------------------------------------------------------------------------------- + +function removeDotSlash(path: string): string { + return path.replace(/^\.\//, '') +} diff --git a/fullcalendar-main/scripts/src/pkg/utils/config.ts b/fullcalendar-main/scripts/src/pkg/utils/config.ts new file mode 100644 index 0000000..cf8dfa2 --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/utils/config.ts @@ -0,0 +1,17 @@ + +// input +export const srcExtensions = ['.ts', '.tsx'] +export const srcIifeSubextension = '.global' // always ends in srcExtensions +export const transpiledSubdir = 'dist/.tsout' +export const transpiledExtension = '.js' +export const assetExtensions = ['.css'] + +// output +export const cjsExtension = '.cjs' +export const esmExtension = '.js' +export const iifeSubextension = '.global' // always ends in .js + +// for consistent chunk names +export const manualChunkEntryAliases: { [chunkName: string]: string[] } = { + 'internal-common': ['internal'], +} diff --git a/fullcalendar-main/scripts/src/pkg/utils/rollup-plugins.ts b/fullcalendar-main/scripts/src/pkg/utils/rollup-plugins.ts new file mode 100644 index 0000000..944b4ee --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/utils/rollup-plugins.ts @@ -0,0 +1,311 @@ +import { + join as joinPaths, + resolve as resolvePath, + dirname, + sep as pathSep, + isAbsolute, +} from 'path' +import { Plugin } from 'rollup' +import { execLive } from '../../utils/exec.js' +import { strsToProps } from '../../utils/lang.js' +import { standardScriptsDir } from '../../utils/script-runner.js' + +// Generated Content +// ------------------------------------------------------------------------------------------------- + +export function generatedContentPlugin(contentMap: { [path: string]: string }): Plugin { + return { + name: 'generated-content', + resolveId(importId, importerPath) { + const importPath = computeImportPath(importId, importerPath) + + // whitelist the import path + if (importPath && contentMap[importPath]) { + return { id: importPath } + } + }, + load(importPath) { + return contentMap[importPath] // if undefined, fallback to normal file load + }, + } +} + +// Externalize certain paths +// ------------------------------------------------------------------------------------------------- + +export interface ExteralizePathsOptions { + paths: string[] + extensions?: ExtensionInput +} + +export function externalizePathsPlugin(options: ExteralizePathsOptions): Plugin { + const pathMap = strsToProps(options.paths) + const extensionMap = options.extensions && normalizeExtensionMap(options.extensions) + + return { + name: 'externalize-paths', + resolveId(importId, importerPath) { + let importPath = computeImportPath(importId, importerPath) + + if (importPath && pathMap[importPath]) { + if (extensionMap) { + importPath = findAndReplaceExtensions(importPath, extensionMap) + } + + if (importPath) { + // return absolute is possible via makeAbsoluteExternalsRelative + return { id: importPath, external: true } + } + } + }, + } +} + +// Externalize certain packages +// ------------------------------------------------------------------------------------------------- + +export interface ExternalizePkgsOptions { + pkgNames: string[], + moduleSideEffects?: boolean + forceExtension?: string +} + +export function externalizePkgsPlugin( + { pkgNames, moduleSideEffects, forceExtension }: ExternalizePkgsOptions, +): Plugin { + return { + name: 'externalize-pkgs', + resolveId(importId) { + if (!isImportRelative(importId)) { + for (const pkgName of pkgNames) { + if (importId === pkgName || importId.startsWith(pkgName + '/')) { + if (forceExtension) { + if (importId === pkgName) { + importId += '/index' + forceExtension + } else { + importId += forceExtension + } + } + + return { id: importId, external: true, moduleSideEffects } + } + } + } + }, + } +} + +// Externalize certain extensions +// ------------------------------------------------------------------------------------------------- + +export function externalizeExtensionsPlugin(extensionsInput: ExtensionInput): Plugin { + let extensionMap = normalizeExtensionMap(extensionsInput) + + return { + name: 'externalize-extensions', + resolveId(importId) { + const newImportId = findAndReplaceExtensions(importId, extensionMap) + + if (newImportId) { + return { id: newImportId, external: true } + } + }, + } +} + +// Reroot Paths +// ------------------------------------------------------------------------------------------------- + +export interface RerootOptions { + oldRoot: string + newRoot: string + extensions?: ExtensionInput +} + +export function rerootPlugin(options: RerootOptions): Plugin { + const oldRootAndSep = options.oldRoot + pathSep + const newRootAndSep = options.newRoot + pathSep + const extensionMap = options.extensions && normalizeExtensionMap(options.extensions) + + return { + name: 'reroot', + resolveId(importId, importerPath) { + const importPath = computeImportPath(importId, importerPath) + + if ( + (!extensionMap || findAndReplaceExtensions(importId, extensionMap)) && + (importPath && importPath.startsWith(oldRootAndSep)) + ) { + return newRootAndSep + importPath.substring(oldRootAndSep.length) + } + }, + } +} + +// Simple Global-Name Dot Assignment +// ------------------------------------------------------------------------------------------------- + +export function simpleDotAssignment(): Plugin { + return { + name: 'simple-dot-assignment', + outputOptions(outputOptions) { + const { name } = outputOptions + + if (name && name.includes('.')) { + return { + ...outputOptions, + name: encodeDotName(name), + } + } + }, + renderChunk(code, chunk, outputOptions) { + const { name } = outputOptions + + if (name && isEncodedDotName(name)) { + return replaceDotAssignments(code) + } + }, + } +} + +function encodeDotName(dotName: string): string { + return '__dot_name_' + dotName.replaceAll('.', '_') + '__' +} + +function isEncodedDotName(name: string): boolean { + return name.startsWith('__dot_name_') +} + +function replaceDotAssignments(code: string): string { + let replaced = false + + code = code.replace(/var __dot_name_(\w+)__ =/, (whole, dotName) => { + replaced = true + return dotName.replaceAll('_', '.') + ' =' + }) + + if (!replaced) { + throw new Error('Error transforming dot assignment') + } + + return code +} + +// Minify +// ------------------------------------------------------------------------------------------------- + +export function minifySeparatelyPlugin(): Plugin { + return { + name: 'minify-separately', + async writeBundle(options, bundles) { + const { file, dir } = options + + if (file) { + await minifySeparately(resolvePath(file)) + } else if (dir) { + await Promise.all( + Object.keys(bundles).map((bundlePath) => { + return minifySeparately(resolvePath(joinPaths(dir, bundlePath))) + }), + ) + } else { + this.error('For minification, must specify dir or file output option') + } + }, + } +} + +async function minifySeparately(path: string): Promise<void> { + const pathMatch = path.match(/^(.*)(\.[cm]?js)$/) + + if (!pathMatch) { + throw new Error('Invalid extension for minification') + } + + return execLive([ + joinPaths(standardScriptsDir, 'node_modules/.bin/terser'), + '--config-file', 'config/terser.json', + '--output', pathMatch[1] + '.min' + pathMatch[2], + '--', path, + ], { + cwd: standardScriptsDir, + }) +} + +// .d.ts +// ------------------------------------------------------------------------------------------------- + +/* +Workarounds rollup-plugin-dts +*/ +export function massageDtsPlugin(): Plugin { + return { + name: 'massage-dts', + renderChunk(code) { + // force all import statements (especially auto-generated chunks) to have a .js extension + // TODO: file a bug. code splitting w/ es2016 modules + code = code.replace(/(} from ['"])([^'"]*)(['"])/g, (whole, start, importId, end) => { + if ( + importId.startsWith('./') && // relative ID + !importId.endsWith('.js') + ) { + return start + importId + '.js' + end + } + return whole + }) + + return code + }, + } +} + +// Extensions Find & Replace Utils +// ------------------------------------------------------------------------------------------------- + +type ExtensionMap = { [findExtension: string]: string } +type ExtensionInput = string[] | ExtensionMap + +function normalizeExtensionMap(input: ExtensionInput): ExtensionMap { + let map: ExtensionMap = {} + + if (Array.isArray(input)) { + for (const extension of input) { + map[extension] = extension + } + } else { + map = input + } + + return map +} + +function findAndReplaceExtensions(path: string, extensionMap: ExtensionMap): string | undefined { + for (let extension in extensionMap) { + if (path.endsWith(extension)) { + const newExtension = extensionMap[extension] + + return path.substring(0, path.length - extension.length) + newExtension + } + } +} + +// Import ID Utils +// ------------------------------------------------------------------------------------------------- + +function computeImportPath(importId: string, importerPath: string | undefined): string | undefined { + if (isAbsolute(importId)) { + return importId + } + + if (isImportRelative(importId)) { + return importerPath ? + joinPaths(dirname(importerPath), importId) : + resolvePath(importId) // from CWD + } + + // otherwise, probably an external dependency +} + +function isImportRelative(importId: string): boolean { + return importId.startsWith('./') || importId.startsWith('../') +} diff --git a/fullcalendar-main/scripts/src/pkg/utils/rollup-presets.ts b/fullcalendar-main/scripts/src/pkg/utils/rollup-presets.ts new file mode 100644 index 0000000..76ea5cd --- /dev/null +++ b/fullcalendar-main/scripts/src/pkg/utils/rollup-presets.ts @@ -0,0 +1,441 @@ +import { readFile } from 'fs/promises' +import { join as joinPaths } from 'path' +import { RollupOptions, Plugin, OutputOptions, RollupWarning } from 'rollup' +import handlebars from 'handlebars' +import nodeResolvePlugin from '@rollup/plugin-node-resolve' +import dtsPlugin from 'rollup-plugin-dts' +import sourcemapsPlugin from 'rollup-plugin-sourcemaps' +import commonjsPluginLib from '@rollup/plugin-commonjs' +import jsonPluginLib from '@rollup/plugin-json' +import postcssPluginLib from 'rollup-plugin-postcss' +import replacePluginLib from '@rollup/plugin-replace' +import { mapProps } from '../../utils/lang.js' +import { MonorepoStruct } from '../../utils/monorepo-struct.js' +import { analyzePkg } from '../../utils/pkg-analysis.js' +import { readPkgJson } from '../../utils/pkg-json.js' +import { standardScriptsDir } from '../../utils/script-runner.js' +import { + transpiledExtension, + transpiledSubdir, + cjsExtension, + esmExtension, + iifeSubextension, + assetExtensions, + srcIifeSubextension, + manualChunkEntryAliases, +} from './config.js' +import { + computeExternalPkgs, + computeIifeExternalPkgs, + computeIifeGlobals, + computeOwnExternalPaths, + computeOwnIifeExternalPaths, + EntryStruct, + entryStructsToContentMap, + generateIifeContent, + PkgBundleStruct, + splitPkgNames, +} from './bundle-struct.js' +import { + externalizeExtensionsPlugin, + externalizePathsPlugin, + externalizePkgsPlugin, + generatedContentPlugin, + minifySeparatelyPlugin, + massageDtsPlugin, + rerootPlugin, + simpleDotAssignment, +} from './rollup-plugins.js' + +const commonjsPlugin = cjsInterop(commonjsPluginLib) +const jsonPlugin = cjsInterop(jsonPluginLib) +const postcssPlugin = cjsInterop(postcssPluginLib) +const replacePlugin = cjsInterop(replacePluginLib) + +/* +TODO: converge with buildCjsOptions and just have multiple outputs? +*/ +export function buildEsmOptions( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + sourcemap: boolean, +): RollupOptions { + return { + input: buildModuleInput(pkgBundleStruct), + plugins: buildModulePlugins(pkgBundleStruct, monorepoStruct, esmExtension, sourcemap), + output: buildEsmOutputOptions(pkgBundleStruct, sourcemap), + onwarn, + } +} + +export function buildCjsOptions( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + sourcemap: boolean, +): RollupOptions { + return { + input: buildModuleInput(pkgBundleStruct), + plugins: buildModulePlugins(pkgBundleStruct, monorepoStruct, cjsExtension, sourcemap), + output: buildCjsOutputOptions(pkgBundleStruct, sourcemap), + onwarn, + } +} + +export async function buildIifeOptions( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + minify: boolean, + sourcemap: boolean, +): Promise<RollupOptions[]> { + const { entryConfigMap, entryStructMap } = pkgBundleStruct + const banner = await buildBanner(pkgBundleStruct) + const iifeContentMap = await generateIifeContent(pkgBundleStruct) + const optionsObjs: RollupOptions[] = [] + + for (let entryAlias in entryStructMap) { + const entryStruct = entryStructMap[entryAlias] + const entryConfig = entryConfigMap[entryStruct.entryGlob] + + if (entryConfig.iife) { + optionsObjs.push({ + input: buildIifeInput(entryStruct), + plugins: buildIifePlugins(entryStruct, pkgBundleStruct, iifeContentMap, sourcemap, minify), + output: buildIifeOutputOptions(entryStruct, entryAlias, pkgBundleStruct, monorepoStruct, banner, sourcemap), + onwarn, + }) + } + } + + return optionsObjs +} + +export function buildDtsOptions(pkgBundleStruct: PkgBundleStruct): RollupOptions { + return { + input: buildDtsInput(pkgBundleStruct), + plugins: buildDtsPlugins(pkgBundleStruct), + output: buildDtsOutputOptions(pkgBundleStruct), + onwarn, + } +} + +// Input +// ------------------------------------------------------------------------------------------------- + +type InputMap = { [entryAlias: string]: string } + +function buildModuleInput(pkgBundleStruct: PkgBundleStruct): InputMap { + return mapProps(pkgBundleStruct.entryStructMap, (entryStruct: EntryStruct) => { + return entryStruct.entrySrcPath + }) +} + +function buildIifeInput(entryStruct: EntryStruct): string { + return entryStruct.entrySrcBase + srcIifeSubextension + transpiledExtension +} + +function buildDtsInput(pkgBundleStruct: PkgBundleStruct): InputMap { + return mapProps(pkgBundleStruct.entryStructMap, (entryStruct: EntryStruct) => { + return entryStruct.entrySrcBase + '.d.ts' + }) +} + +// Output +// ------------------------------------------------------------------------------------------------- + +function buildEsmOutputOptions( + pkgBundleStruct: PkgBundleStruct, + sourcemap: boolean, +): OutputOptions { + return { + format: 'esm', + dir: joinPaths(pkgBundleStruct.pkgDir, 'dist'), + entryFileNames: '[name]' + esmExtension, + chunkFileNames: '[name]' + esmExtension, + manualChunks: buildManualChunks(pkgBundleStruct, transpiledExtension), + sourcemap, + } +} + +function buildCjsOutputOptions( + pkgBundleStruct: PkgBundleStruct, + sourcemap: boolean, +): OutputOptions { + return { + format: 'cjs', + exports: 'named', + dir: joinPaths(pkgBundleStruct.pkgDir, 'dist'), + entryFileNames: '[name]' + cjsExtension, + chunkFileNames: '[name]' + cjsExtension, + manualChunks: buildManualChunks(pkgBundleStruct, transpiledExtension), + sourcemap, + } +} + +function buildIifeOutputOptions( + entryStruct: EntryStruct, + entryAlias: string, + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + banner: string, + sourcemap: boolean, +): OutputOptions { + const { pkgDir, iifeGlobalsMap } = pkgBundleStruct + const globalName = iifeGlobalsMap[entryStruct.entryGlob] + + return { + format: 'iife', + banner, + file: joinPaths(pkgDir, 'dist', entryAlias) + iifeSubextension + '.js', + globals: computeIifeGlobals(pkgBundleStruct, monorepoStruct), + ...( + globalName + ? { exports: 'named', name: globalName } + : { exports: 'none' } + ), + interop: 'auto', + freeze: false, + sourcemap, + } +} + +function buildDtsOutputOptions(pkgBundleStruct: PkgBundleStruct): OutputOptions { + return { + format: 'esm', + dir: joinPaths(pkgBundleStruct.pkgDir, 'dist'), + entryFileNames: '[name].d.ts', + chunkFileNames: '[name].d.ts', + manualChunks: buildManualChunks(pkgBundleStruct, '.d.ts'), + } +} + +// Chunk Options +// ------------------------------------------------------------------------------------------------- + +function buildManualChunks( + pkgBundleStruct: PkgBundleStruct, + inExtension: string, +): { [absPath: string]: string[] } { + const { pkgDir, entryStructMap } = pkgBundleStruct + const manualChunks: { [absPath: string]: string[] } = {} + + for (const chunkName in manualChunkEntryAliases) { + const entryAliases = manualChunkEntryAliases[chunkName] + const validEntryPaths: string[] = [] + + for (const entryAlias of entryAliases) { + if (entryStructMap[entryAlias]) { + validEntryPaths.push(joinPaths(pkgDir, transpiledSubdir, entryAlias + inExtension)) + } + } + + if (validEntryPaths.length) { + manualChunks[chunkName] = validEntryPaths + } + } + + return manualChunks +} + +// Plugins Lists +// ------------------------------------------------------------------------------------------------- + +function buildModulePlugins( + pkgBundleStruct: PkgBundleStruct, + monorepoStruct: MonorepoStruct, + forceOurExtension: string, + sourcemap: boolean, +): Plugin[] { + const { pkgDir, entryStructMap } = pkgBundleStruct + const { ourPkgNames, theirPkgNames } = splitPkgNames( + computeExternalPkgs(pkgBundleStruct), + monorepoStruct, + ) + + return [ + rerootAssetsPlugin(pkgDir), + externalizePkgsPlugin({ + pkgNames: theirPkgNames, + }), + externalizePkgsPlugin({ + pkgNames: ourPkgNames, + forceExtension: forceOurExtension, + }), + generatedContentPlugin( + entryStructsToContentMap(entryStructMap), + ), + ...buildJsPlugins(pkgBundleStruct), + ...(sourcemap ? [sourcemapsPlugin()] : []), // load preexisting sourcemaps + ] +} + +/* +TODO: inefficient to repeatedly generate all this? +*/ +function buildIifePlugins( + currentEntryStruct: EntryStruct, + pkgBundleStruct: PkgBundleStruct, + iifeContentMap: { [path: string]: string }, + sourcemap: boolean, + minify: boolean, +): Plugin[] { + const { pkgDir, entryStructMap } = pkgBundleStruct + + return [ + rerootAssetsPlugin(pkgDir), + externalizePkgsPlugin({ + pkgNames: computeIifeExternalPkgs(pkgBundleStruct), + }), + externalizePathsPlugin({ + paths: computeOwnIifeExternalPaths(currentEntryStruct, pkgBundleStruct), + }), + generatedContentPlugin({ + ...entryStructsToContentMap(entryStructMap), + ...iifeContentMap, + }), + simpleDotAssignment(), + ...buildJsPlugins(pkgBundleStruct), + ...(sourcemap ? [sourcemapsPlugin()] : []), + ...(minify ? [minifySeparatelyPlugin()] : []), + ] +} + +function buildDtsPlugins(pkgBundleStruct: PkgBundleStruct): Plugin[] { + return [ + externalizeAssetsPlugin(), + externalizePkgsPlugin({ + pkgNames: computeExternalPkgs(pkgBundleStruct), + moduleSideEffects: true, // for including ambient declarations in other packages + }), + // rollup-plugin-dts normally gets confused with code splitting. this helps a lot. + externalizePathsPlugin({ + paths: computeOwnExternalPaths(pkgBundleStruct), + }), + dtsPlugin(), + massageDtsPlugin(), + nodeResolvePlugin({ + ignoreSideEffectsForRoot: true, + }), + ] +} + +function buildJsPlugins(pkgBundleStruct: PkgBundleStruct): Plugin[] { + const pkgAnalysis = analyzePkg(pkgBundleStruct.pkgDir) + + if (pkgAnalysis.isTests) { + return buildTestJsPlugins() + } else { + return buildNormalJsPlugins(pkgBundleStruct) + } +} + +function buildNormalJsPlugins(pkgBundleStruct: PkgBundleStruct): Plugin[] { + const { pkgDir, pkgJson } = pkgBundleStruct + + return [ + nodeResolvePlugin({ + ignoreSideEffectsForRoot: true, + }), + cssPlugin({ + inject: { + importId: pkgJson.name === '@fullcalendar/core' ? + joinPaths(pkgDir, transpiledSubdir, 'styleUtils' + transpiledExtension) : + '@fullcalendar/core/internal', + importProp: 'injectStyles', + }, + }), + replacePlugin({ + delimiters: ['<%= ', ' %>'], + preventAssignment: true, + values: { + releaseDate: new Date().toISOString().replace(/T.*/, ''), // just YYYY-MM-DD + pkgName: pkgJson.name, + pkgVersion: pkgJson.version, + }, + }), + ] +} + +function buildTestJsPlugins(): Plugin[] { + return [ + nodeResolvePlugin({ // determines index.js and .js/cjs/mjs + browser: true, // for xhr-mock (use non-node shims that it wants to) + preferBuiltins: false, // for xhr-mock (use 'url' npm package) + ignoreSideEffectsForRoot: true, + }), + commonjsPlugin(), // for moment and moment-timezone + jsonPlugin(), // for moment-timezone + cssPlugin({ inject: true }), + replacePlugin({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': '"development"', + }, + }), + ] +} + +// Plugins Wrappers +// ------------------------------------------------------------------------------------------------- + +interface CssInjector { + importId: string + importProp: string +} + +function cssPlugin(options?: { inject?: CssInjector | boolean }): Plugin { + const { inject } = options || {} + + return postcssPlugin({ + config: { + path: joinPaths(standardScriptsDir, 'config/postcss.config.cjs'), + ctx: {}, // arguments given to config file + }, + inject: typeof inject === 'object' ? + (cssVarName: string) => { + return `import { ${inject.importProp} } from ${JSON.stringify(inject.importId)};\n` + + `injectStyles(${cssVarName});\n` + } : + (inject || false), + minimize: true, + }) +} + +function rerootAssetsPlugin(pkgDir: string): Plugin { + return rerootPlugin({ + extensions: assetExtensions, + oldRoot: joinPaths(pkgDir, 'dist', '.tsout'), + newRoot: joinPaths(pkgDir, 'src'), + }) +} + +function externalizeAssetsPlugin(): Plugin { + return externalizeExtensionsPlugin(assetExtensions) +} + +// Misc +// ------------------------------------------------------------------------------------------------- + +async function buildBanner(pkgBundleStruct: PkgBundleStruct): Promise<string> { + const { pkgDir, pkgJson } = pkgBundleStruct + + const pkgAnalysis = analyzePkg(pkgDir) + const basePkgJson = await readPkgJson(pkgAnalysis.metaRootDir) // TODO: use a cached version + const fullPkgJson = { ...basePkgJson, ...pkgJson } + + // TODO: cache the template + const templatePath = joinPaths(standardScriptsDir, 'config/banner.tpl') + const templateText = await readFile(templatePath, 'utf8') + const template = handlebars.compile(templateText) + + return template(fullPkgJson).trim() +} + +function onwarn(warning: RollupWarning) { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + console.error(warning.toString()) + } +} + +function cjsInterop<DefaultExport>(namespace: { default: DefaultExport }): DefaultExport { + return namespace.default || (namespace as DefaultExport) +} diff --git a/fullcalendar-main/scripts/src/postinstall.ts b/fullcalendar-main/scripts/src/postinstall.ts new file mode 100644 index 0000000..0412dce --- /dev/null +++ b/fullcalendar-main/scripts/src/postinstall.ts @@ -0,0 +1,14 @@ +import { ScriptContext } from './utils/script-runner.js' +import { writeTsconfigs } from './utils/monorepo-ts.js' +import { writeDistPkgJsons } from './json.js' + +export default async function(this: ScriptContext) { + await Promise.all([ + writeTsconfigs(this.monorepoStruct), + writeDistPkgJsons( + this.monorepoStruct, + true, // isDev + true, // reuseExisting + ), + ]) +} diff --git a/fullcalendar-main/scripts/src/test.ts b/fullcalendar-main/scripts/src/test.ts new file mode 100644 index 0000000..207934e --- /dev/null +++ b/fullcalendar-main/scripts/src/test.ts @@ -0,0 +1,40 @@ +import chalk from 'chalk' +import { ScriptContext } from './utils/script-runner.js' +import { execLive } from './utils/exec.js' + +export default async function(this: ScriptContext, ...args: string[]) { + const isDev = args.includes('--dev') + const isAll = args.includes('--all') + const isOther = args.includes('--other') + const { monorepoStruct } = this + const { pkgDirToJson } = monorepoStruct + const promises: Promise<any>[] = [] + + for (const pkgDir in pkgDirToJson) { + const pkgJson = pkgDirToJson[pkgDir] + + if ( + isAll || + (isOther && !pkgJson.karmaConfig) || + (!isOther && pkgJson.karmaConfig) + ) { + const subcommand = isDev ? 'test:dev' : 'test' + + if (pkgJson.scripts?.[subcommand]) { + console.log() + console.log(chalk.green(pkgJson.name)) + console.log() + + const promise = execLive(['pnpm', 'run', subcommand], { cwd: pkgDir }) + + if (isDev) { + promises.push(promise) + } else { + await promise + } + } + } + } + + await Promise.all(promises) +} diff --git a/fullcalendar-main/scripts/src/utils/exec.ts b/fullcalendar-main/scripts/src/utils/exec.ts new file mode 100644 index 0000000..ccf4e07 --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/exec.ts @@ -0,0 +1,119 @@ +import { promisify } from 'util' +import { + exec as execCb, + execFile as execFileCb, + spawn, + ChildProcess, + ExecOptions, + StdioOptions, + SpawnOptions, +} from 'child_process' + +const exec = promisify(execCb) +const execFile = promisify(execFileCb) + +export function execCapture( + command: string | string[], + options: ExecOptions = {}, +): Promise<string> { + if (typeof command === 'string') { + return exec(command, options) + .then((res) => res.stdout) + } else if (Array.isArray(command)) { + return execFile(command[0], command.slice(1), options) + .then((res) => res.stdout) + } else { + throw new Error('Invalid command type for execCapture()') + } +} + +export function execLive( + command: string | string[], + options: SpawnOptions = {}, +): Promise<void> { + return execWithStdio(command, options, 'inherit') +} + +export function execSilent( + command: string | string[], + options: SpawnOptions = {}, +): Promise<void> { + return execWithStdio(command, options, 'ignore') +} + +// TODO: just return the childProcess +export function spawnLive( + command: string | string[], + options: SpawnOptions = {}, +): () => void { + const child = spawnWithStdio(command, options, 'inherit') + return () => { + child.disconnect && child.disconnect() + } +} + +// TODO: just return the childProcess +export function spawnSilent( + command: string | string[], + options: SpawnOptions = {}, +): () => void { + const child = spawnWithStdio(command, options, 'ignore') + return () => { + child.disconnect && child.disconnect() + } +} + +function execWithStdio( + command: string | string[], + options: SpawnOptions, + stdio: StdioOptions, +): Promise<void> { + const childProcess = spawnWithStdio(command, options, stdio) + + return new Promise((resolve, reject) => { + childProcess.on('close', (exitCode) => { + if (exitCode === 0) { + resolve() + } else { + reject(new SpawnError(command, exitCode)) + } + }) + }) +} + +function spawnWithStdio( + command: string | string[], + options: SpawnOptions, + stdio: StdioOptions, +): ChildProcess { + let commandPath: string + let commandArgs: string[] + let shell: boolean + + if (typeof command === 'string') { + commandPath = command + commandArgs = [] + shell = true + } else if (Array.isArray(command)) { + commandPath = command[0] + commandArgs = command.slice(1) + shell = false + } else { + throw new Error('Invalid command type for execLive()') + } + + return spawn(commandPath, commandArgs, { + ...options, + shell, + stdio, + }) +} + +export class SpawnError extends Error { + constructor( + public command: string | string[], + public exitCode: number | null, + ) { + super(`Exited ${JSON.stringify(command)} with error code ${exitCode}`) + } +} diff --git a/fullcalendar-main/scripts/src/utils/fs.ts b/fullcalendar-main/scripts/src/utils/fs.ts new file mode 100644 index 0000000..9d3b8dc --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/fs.ts @@ -0,0 +1,38 @@ +import { dirname } from 'path' +import { readFile, writeFile, mkdir, lstat } from 'fs/promises' + +export async function ensureFileDir(path: string): Promise<any> { + await mkdir(dirname(path), { recursive: true }) +} + +export async function readJson(path: string): Promise<any> { + const srcJson = await readFile(path, 'utf8') + const srcMeta = JSON.parse(srcJson) + return srcMeta +} + +export async function writeJson(path: string, obj: any): Promise<any> { + await writeFile(path, stringifyJson(obj)) +} + +export function stringifyJson(obj: any): string { + return JSON.stringify(obj, undefined, 2) + '\n' +} + +export async function writeIfDifferent(path: string, content: string): Promise<boolean> { + const existingContent = await readFile(path, 'utf8').catch(() => false) + + if (existingContent === false || existingContent !== content) { + await writeFile(path, content) + return true + } + + return false +} + +export function fileExists(path: string): Promise<boolean> { + return lstat(path).then( + () => true, + () => false, + ) +} diff --git a/fullcalendar-main/scripts/src/utils/git.ts b/fullcalendar-main/scripts/src/utils/git.ts new file mode 100644 index 0000000..0e7645d --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/git.ts @@ -0,0 +1,47 @@ +import { dirname } from 'path' +import { SpawnError, execSilent, execLive } from './exec.js' + +export function assumeUnchanged(path: string, toggle = true): Promise<void> { + return execSilent([ + 'git', 'update-index', + toggle ? '--assume-unchanged' : '--no-assume-unchanged', + path, + ], { + cwd: dirname(path), + }) +} + +export function checkoutFile(path: string): Promise<void> { + return execSilent([ + 'git', 'checkout', '--', path, + ], { + cwd: dirname(path), + }) +} + +export function addFile(path: string): Promise<void> { + return execSilent([ + 'git', 'add', path, + ], { + cwd: dirname(path), + }) +} + +export function commitDir(dir: string, message: string): Promise<void> { + return execLive([ + 'git', 'commit', '-m', message, + ], { + cwd: dir, + }) +} + +export function isStaged(path: string): Promise<boolean> { + return execSilent([ + 'git', 'diff', '--quiet', '--staged', path, // implies --exit-code + ], { + cwd: dirname(path), + }).then( + () => false, // 0 exitCode means no difference + (error: SpawnError) => error.exitCode === 1, // 1 exitCode means difference + ) +} diff --git a/fullcalendar-main/scripts/src/utils/lang.ts b/fullcalendar-main/scripts/src/utils/lang.ts new file mode 100644 index 0000000..81e085a --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/lang.ts @@ -0,0 +1,99 @@ + +export function mapProps<V, R>( + props: { [key: string]: V }, + func: (val: V, key: string) => R, +): { [key: string]: R } { + const newProps: { [key: string]: R } = {} + + for (const key in props) { + newProps[key] = func(props[key], key) + } + + return newProps +} + +export function filterProps<V>( + props: { [key: string]: V }, + func: (val: V, key: string) => boolean, +): { [key: string]: V } { + const newProps: { [key: string]: V } = {} + + for (const key in props) { + if (func(props[key], key)) { + newProps[key] = props[key] + } + } + + return newProps +} + +export function strsToProps(strs: string[]): { [str: string]: true } { + const map: { [str: string]: true } = {} + + for (const str of strs) { + map[str] = true + } + + return map +} + +export function boolPromise(promise: Promise<any>): Promise<boolean> { + return promise.then( + () => true, + () => false, + ) +} + +export function arrayify(input: any): any[] { + return Array.isArray(input) ? input : (input == null ? [] : [input]) +} + +export function wait(ms: number): Promise<void> { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +// Async +// ------------------------------------------------------------------------------------------------- + +export type ContinuousAsyncFunc = (rerun: () => void) => ContinuousAsyncFuncRes +export type ContinuousAsyncFuncRes = + Promise<(() => void) | void> | + (() => void) | + void + +export async function continuousAsync(workerFunc: ContinuousAsyncFunc): Promise<() => void> { + let currentRun: Promise<ContinuousAsyncFuncRes> | undefined + let currentCleanupFunc: (() => void) | undefined + let isDirty = false + let isStopped = false + + async function run() { + if (!isStopped) { + if (!currentRun) { + currentCleanupFunc && currentCleanupFunc() + currentCleanupFunc = undefined + + currentRun = Promise.resolve(workerFunc(run)) + currentCleanupFunc = (await currentRun) || undefined + currentRun = undefined + + // had scan requests during previous run? + if (isDirty) { + isDirty = false + run() + } + } else { + isDirty = true + } + } + } + + await run() + + return () => { // the "stop" function + isStopped = true + currentCleanupFunc && currentCleanupFunc() + } +} diff --git a/fullcalendar-main/scripts/src/utils/log.ts b/fullcalendar-main/scripts/src/utils/log.ts new file mode 100644 index 0000000..ae96ff0 --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/log.ts @@ -0,0 +1,23 @@ +import chalk from 'chalk' + +const timeFormat = new Intl.DateTimeFormat('en', { + timeStyle: 'medium', +}) + +export function pkgLog(pkgName: string, message: string) { + log(message, chalk.green(pkgName.replace(/^@[^/]*\//, ''))) +} + +export function log(message: string, label?: string) { + const now = Date.now() + const nowStr = timeFormat.format(now) + const prefix = `[${chalk.grey(nowStr)}]` + const subprefix = label ? `${label}: ` : '' + const lines = message.trimEnd().split('\n') + + for (const line of lines) { + console.log(`${prefix} ${subprefix}${line}`) + } + + console.log() +} diff --git a/fullcalendar-main/scripts/src/utils/monorepo-config.ts b/fullcalendar-main/scripts/src/utils/monorepo-config.ts new file mode 100644 index 0000000..64c1d5b --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/monorepo-config.ts @@ -0,0 +1,37 @@ +import { join as joinPaths, basename } from 'path' +import { MonorepoStruct } from './monorepo-struct.js' + +export function getArchiveRootDirs(monorepoStruct: MonorepoStruct): string[] { + const { monorepoDir, monorepoPkgJson } = monorepoStruct + const archiveSubtrees: string[] | undefined = monorepoPkgJson.monorepoConfig?.archiveSubtrees + + if (archiveSubtrees) { + return archiveSubtrees.map((subdir) => joinPaths(monorepoDir, subdir)) + } else { + return [monorepoDir] + } +} + +export function refineFilterArgs(args: string[], monorepoStruct: MonorepoStruct): string[] { + const isAllIndex = args.indexOf('--all') + const isAll = isAllIndex !== -1 + + if (isAll) { + args = args.slice() + args.splice(isAllIndex, 1) + } else { + const monorepoConfig = monorepoStruct.monorepoPkgJson.monorepoConfig || {} + const filterSubtrees: string[] = monorepoConfig.filterSubtrees || ['.'] + + args = args.concat(filterSubtrees.map((subdir) => `--filter=${subdir}/**`)) + } + + // HACK + // In the ROOT monorepo? + // Exclude the 'standard' monorepo because will double-tread + if (basename(process.cwd()) === 'fullcalendar-workspace') { + args.push('--filter=!@fullcalendar-monorepos/standard') + } + + return args +} diff --git a/fullcalendar-main/scripts/src/utils/monorepo-struct.ts b/fullcalendar-main/scripts/src/utils/monorepo-struct.ts new file mode 100644 index 0000000..cee34ce --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/monorepo-struct.ts @@ -0,0 +1,234 @@ +import { join as joinPaths, dirname } from 'path' +import { readFile } from 'fs/promises' +import { watch as watchPaths } from 'chokidar' +import { globby } from 'globby' +import * as semver from 'semver' +import * as yaml from 'js-yaml' +import { getPkgJsonPath, readPkgJson } from './pkg-json.js' +import { continuousAsync, ContinuousAsyncFuncRes } from './lang.js' + +export interface MonorepoStruct { + monorepoDir: string + monorepoPkgJson: any + monorepoConfigPath: string + pkgNameToDir: { [name: string]: string } + pkgDirToJson: { [dir: string]: any } +} + +export interface PkgStruct { // only for traversing + pkgDir: string + pkgJson: any + localDepDirs: string[] +} + +export async function watchMonorepo( + monorepoDir: string, + handleMonorepo: (monorepoStruct: MonorepoStruct) => ContinuousAsyncFuncRes, + initialMonorepoStruct?: MonorepoStruct, +): Promise<() => void> { + return continuousAsync(async (rerun) => { + const monorepoStruct = initialMonorepoStruct || (await readMonorepo(monorepoDir)) + initialMonorepoStruct = undefined + + const relevantPaths = getMonorepoRelevantPaths(monorepoStruct) + const watcher = watchPaths(relevantPaths, { ignoreInitial: true }) + watcher.once('all', rerun) + + const cleanupFunc = await handleMonorepo(monorepoStruct) + + return () => { + watcher.close() + cleanupFunc && cleanupFunc() + } + }) +} + +/* +Like traverseMonorepo, but handlers will not delay traversal of dependents +*/ +export async function traverseMonorepoGreedy( + monorepoStruct: MonorepoStruct, + handlePkg: (pkgStruct: PkgStruct) => (Promise<void> | void), + startPkgDir: string = '', +) { + const promises: Promise<void>[] = [] + + await traverseMonorepo( + monorepoStruct, + (pkgStruct: PkgStruct) => { + const promise = handlePkg(pkgStruct) + if (promise) { + promises.push(promise) + } + }, + startPkgDir, + ) + + await Promise.all(promises) +} + +export async function traverseMonorepo( + monorepoStruct: MonorepoStruct, + handlePkg: (pkgStruct: PkgStruct) => ContinuousAsyncFuncRes, + startPkgDir: string = '', +): Promise<() => void> { + const { pkgDirToJson } = monorepoStruct + const promiseMap: { [pkgDir: string]: Promise<void> } = {} + const cleanupFuncs: (() => void)[] = [] + + if (startPkgDir) { + await traversePkg(startPkgDir) + } else { + await Promise.all( + Object.keys(pkgDirToJson).map((pkgDir) => traversePkg(pkgDir)), + ) + } + + return () => { + for (let cleanupFunc of cleanupFuncs) { + cleanupFunc() + } + } + + function traversePkg(pkgDir: string): Promise<void> { + return (promiseMap[pkgDir] || (promiseMap[pkgDir] = (async function() { + const pkgJson = pkgDirToJson[pkgDir] + if (!pkgJson) { + throw new Error(`Unknown package at '${pkgDir}'`) + } + + const localDepDirs = computeLocalDepDirs(monorepoStruct, pkgJson) + await Promise.all( + localDepDirs.map((localDepDir) => traversePkg(localDepDir)), + ) + + const cleanupFunc = await handlePkg({ + pkgDir, + pkgJson, + localDepDirs, + }) + + if (cleanupFunc) { + cleanupFuncs.push(cleanupFunc) + } + })())) + } +} + +export function computeLocalDepDirs(monorepoStruct: MonorepoStruct, pkgJson: any): string[] { + const { pkgNameToDir, pkgDirToJson } = monorepoStruct + const depMap = { ...pkgJson.dependencies, ...pkgJson.devDependencies } + const localDepDirs: string[] = [] + + for (let depName in depMap) { + const depSpecifier = depMap[depName] + + // TODO: workspace protocol accepts directory too + const localDepMatch = depSpecifier.match(/^workspace:(.*)$/) + const depVersionRange = localDepMatch ? localDepMatch[1] : depSpecifier + + const depDir = pkgNameToDir[depName] + const depJsonObj = pkgDirToJson[depDir] + + if (depJsonObj && ( + depVersionRange === '*' || // workaround for '*' not matching prerelease tags + semver.satisfies(depJsonObj.version, depVersionRange) + )) { + localDepDirs.push(depDir) + } else if (localDepMatch) { + throw new Error( + `Workspace package '${depName}@${depJsonObj.version}' ` + + `does not match '${depSpecifier}'`, + ) + } + } + + return localDepDirs +} + +export async function readMonorepo(monorepoDir: string): Promise<MonorepoStruct> { + const { monorepoPkgJson, monorepoConfigPath, pkgDirGlobs } = await getMonorepoMeta(monorepoDir) + const pkgDirs = await expandMonorepoPkgDirGlobs(monorepoDir, pkgDirGlobs) + + const pkgJsonObjs = await Promise.all( + pkgDirs.map((pkgDir) => readPkgJson(pkgDir)), + ) + + const pkgNameToDir: { [name: string]: string } = {} + const pkgDirToJson: { [name: string]: any } = {} + + for (let i = 0; i < pkgJsonObjs.length; i++) { + const pkgJson = pkgJsonObjs[i] + const pkgName = pkgJson.name + const pkgDir = pkgDirs[i] + + if (!pkgName) { + throw new Error(`Package '${pkgDir}' must have a name`) + } + + if (!pkgJson.version) { + throw new Error(`Package '${pkgDir}' must have a version`) + } + + pkgNameToDir[pkgName] = pkgDir + pkgDirToJson[pkgDir] = pkgJson + } + + return { monorepoPkgJson, monorepoConfigPath, monorepoDir, pkgNameToDir, pkgDirToJson } +} + +async function getMonorepoMeta(monorepoDir: string): Promise<{ + monorepoPkgJson: any, + monorepoConfigPath: string, + pkgDirGlobs: string[], +}> { + const monorepoPkgJson = await readPkgJson(monorepoDir) + const pnpmWorkspaceConfigPath = joinPaths(monorepoDir, 'pnpm-workspace.yaml') + const pnpmWorkspaceConfig = await readFile(pnpmWorkspaceConfigPath, 'utf8').then( + (str) => yaml.load(str) as any, + () => false, + ) + + let monorepoConfigPath: string + let pkgDirGlobs: string[] + + if (pnpmWorkspaceConfig) { + monorepoConfigPath = pnpmWorkspaceConfigPath + pkgDirGlobs = pnpmWorkspaceConfig.packages + } else { + monorepoConfigPath = getPkgJsonPath(monorepoDir) + pkgDirGlobs = monorepoPkgJson + } + + if (!pkgDirGlobs) { + throw new Error(`${monorepoDir} does not appear to be a monorepo`) + } + + return { monorepoPkgJson, monorepoConfigPath, pkgDirGlobs } +} + +async function expandMonorepoPkgDirGlobs( + monorepoDir: string, + pkgDirGlobs: string[], +): Promise<string[]> { + const relJsonPaths = await globby( + pkgDirGlobs.map((pkgDirGlob) => getPkgJsonPath(pkgDirGlob)), + { cwd: monorepoDir }, + ) + + const pkgDirs = relJsonPaths.map( + (relJsonPath) => joinPaths(monorepoDir, dirname(relJsonPath)), + ) + + return pkgDirs +} + +function getMonorepoRelevantPaths(monorepoStruct: MonorepoStruct): string[] { + const relevantPaths = [monorepoStruct.monorepoConfigPath] + + for (const pkgDir in monorepoStruct.pkgDirToJson) { + relevantPaths.push(getPkgJsonPath(pkgDir)) + } + + return relevantPaths +} diff --git a/fullcalendar-main/scripts/src/utils/monorepo-ts.ts b/fullcalendar-main/scripts/src/utils/monorepo-ts.ts new file mode 100644 index 0000000..c03accb --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/monorepo-ts.ts @@ -0,0 +1,103 @@ +import { join as joinPaths, relative as relativizePath } from 'path' +import { execLive, spawnLive } from './exec.js' +import { stringifyJson, writeIfDifferent } from './fs.js' +import { MonorepoStruct, PkgStruct, traverseMonorepoGreedy } from './monorepo-struct.js' +import { standardScriptsDir } from './script-runner.js' +import { log } from './log.js' + +export async function compileTs(dir: string, tscArgs: string[] = []): Promise<void> { + await execLive([ + joinPaths(standardScriptsDir, 'node_modules/.bin/tsc'), + '-b', + ...tscArgs, + ], { + cwd: dir, + }) +} + +export async function watchTs(dir: string, tscArgs: string[] = []): Promise<() => void> { + log('Pre-watch tsc compiling...') + await compileTs(dir, tscArgs) + + // for watching, will compile again but will be quick + return spawnLive([ + joinPaths(standardScriptsDir, 'node_modules/.bin/tsc'), + '-b', '--watch', + ...tscArgs, + ], { + cwd: dir, + }) +} + +export async function writeTsconfigs( + monorepoStruct: MonorepoStruct, + startPkgDir = '', +): Promise<void> { + const refDirs: string[] = [] + + await traverseMonorepoGreedy( + monorepoStruct, + async (pkgStruct) => { + if (await writePkgTsconfig(pkgStruct, monorepoStruct)) { + refDirs.push(pkgStruct.pkgDir) + } + }, + startPkgDir, + ) + + if (!startPkgDir) { + await writePkgTsconfigWithRefs( + monorepoStruct.monorepoDir, + refDirs, + { files: [] }, + ) + } +} + +async function writePkgTsconfig( + pkgStruct: PkgStruct, + monorepoStruct: MonorepoStruct, +): Promise<boolean> { + const { pkgDir, pkgJson, localDepDirs } = pkgStruct + const { tsConfig } = pkgJson + + if (tsConfig) { + const refDirs: string[] = [] + + for (let localDepDir of localDepDirs) { + const depPkgJson = monorepoStruct.pkgDirToJson[localDepDir] + + if (depPkgJson.tsConfig) { + refDirs.push(localDepDir) + } + } + + await writePkgTsconfigWithRefs(pkgDir, refDirs, tsConfig) + return true + } + + return false +} + +async function writePkgTsconfigWithRefs( + pkgDir: string, + refDirs: string[], // gets modified in-place + tsConfigBase: any, +): Promise<void> { + refDirs.sort() // deterministic order + + const finalTsConfig = { + ...tsConfigBase, + references: [ + ...(tsConfigBase.references || []), + ...refDirs.map((refDir) => ({ + path: relativizePath(pkgDir, refDir), + })), + ], + } + + await writeIfDifferent( + joinPaths(pkgDir, 'tsconfig.json'), + stringifyJson(finalTsConfig), + ) +} diff --git a/fullcalendar-main/scripts/src/utils/pkg-analysis.ts b/fullcalendar-main/scripts/src/utils/pkg-analysis.ts new file mode 100644 index 0000000..0716adb --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/pkg-analysis.ts @@ -0,0 +1,22 @@ +import { join as joinPaths, basename } from 'path' + +export interface PkgAnalysis { + metaRootDir: string // where LICENSE lives + pkgDir: string + isBundle: boolean + isTests: boolean +} + +export function analyzePkg(pkgDir: string): PkgAnalysis { + const pkgDirName = basename(pkgDir) + const isTests = pkgDirName === 'tests' + const isBundle = pkgDirName === 'bundle' + const metaRootDir = joinPaths(pkgDir, (isTests || isBundle) ? '..' : '../..') + + return { + metaRootDir, + pkgDir, + isTests, + isBundle, + } +} diff --git a/fullcalendar-main/scripts/src/utils/pkg-json.ts b/fullcalendar-main/scripts/src/utils/pkg-json.ts new file mode 100644 index 0000000..3db0bf3 --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/pkg-json.ts @@ -0,0 +1,14 @@ +import { join as joinPaths } from 'path' +import { readJson, writeJson } from './fs.js' + +export function readPkgJson(pkgDir: string): Promise<any> { + return readJson(getPkgJsonPath(pkgDir)) +} + +export async function writePkgJson(pkgDir: string, pkgJson: any): Promise<any> { + return writeJson(getPkgJsonPath(pkgDir), pkgJson) +} + +export function getPkgJsonPath(pkgDir: string): string { + return joinPaths(pkgDir, 'package.json') +} diff --git a/fullcalendar-main/scripts/src/utils/process.ts b/fullcalendar-main/scripts/src/utils/process.ts new file mode 100644 index 0000000..be5d56f --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/process.ts @@ -0,0 +1,8 @@ + +export function untilSigInt(): Promise<void> { + return new Promise<void>((resolve) => { + process.once('SIGINT', () => { + resolve() + }) + }) +} diff --git a/fullcalendar-main/scripts/src/utils/script-runner.ts b/fullcalendar-main/scripts/src/utils/script-runner.ts new file mode 100644 index 0000000..3a58060 --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/script-runner.ts @@ -0,0 +1,61 @@ +import { join as joinPaths, sep as pathSep } from 'path' +import { fileURLToPath } from 'url' +import { fileExists } from './fs.js' +import { MonorepoStruct, readMonorepo } from './monorepo-struct.js' +// import { compileTs, writeTsconfigs } from './monorepo-ts.js' + +export interface ScriptContext { + cwd: string + monorepoStruct: MonorepoStruct + scriptName: string +} + +export const standardScriptsDir = joinPaths(fileURLToPath(import.meta.url), '../../..') + +export async function runScript(scriptPkgDir: string): Promise<void> { + const cwd = process.cwd() + const scriptName = process.argv[2] + const scriptArgs = process.argv.slice(3) + + if (!scriptName) { + throw new Error('Must provide a script name') + } + + const monorepoDir = await findNearestMonorepoRoot(cwd) + const monorepoStruct = await readMonorepo(monorepoDir) + // await writeTsconfigs(monorepoStruct, scriptPkgDir) + // await compileTs(scriptPkgDir) + + const scriptPath = joinPaths(scriptPkgDir, 'dist', scriptName.replace(':', '/') + '.js') + const scriptExports = await import(scriptPath) + const scriptMain = scriptExports.default + + if (typeof scriptMain !== 'function') { + throw new Error(`Script '${scriptPath}' must export a default function`) + } + + const scriptContext: ScriptContext = { + cwd, + monorepoStruct, + scriptName, + } + + await scriptMain.apply(scriptContext, scriptArgs) +} + +// TODO: cleanup +async function findNearestMonorepoRoot(currentDir: string): Promise<string> { + const parts = currentDir.split(pathSep) + + while (parts.length) { + const dir = parts.join(pathSep) + + if (await fileExists(joinPaths(dir, 'pnpm-workspace.yaml'))) { + return dir + } + + parts.pop() + } + + return '' +} diff --git a/fullcalendar-main/scripts/src/utils/turbo.ts b/fullcalendar-main/scripts/src/utils/turbo.ts new file mode 100644 index 0000000..15ce51b --- /dev/null +++ b/fullcalendar-main/scripts/src/utils/turbo.ts @@ -0,0 +1,12 @@ +import { join as joinPaths } from 'path' +import { execLive } from './exec.js' +import { standardScriptsDir } from './script-runner.js' + +export function runTurboTasks(monorepoDir: string, turboRunArgs: string[]): Promise<void> { + return execLive([ + joinPaths(standardScriptsDir, 'node_modules/.bin/turbo'), + 'run', ...turboRunArgs, + ], { + cwd: monorepoDir, + }) +} diff --git a/fullcalendar-main/scripts/tsconfig.safe.json b/fullcalendar-main/scripts/tsconfig.safe.json new file mode 100644 index 0000000..8481a53 --- /dev/null +++ b/fullcalendar-main/scripts/tsconfig.safe.json @@ -0,0 +1,11 @@ +{ + "extends": "./config/tsconfig.node.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "./src/**/*" + ], + "references": [] +} diff --git a/fullcalendar-main/tests/.eslintrc.cjs b/fullcalendar-main/tests/.eslintrc.cjs new file mode 100644 index 0000000..c58b543 --- /dev/null +++ b/fullcalendar-main/tests/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: require.resolve('@fullcalendar-scripts/standard/config/eslint.pkg.browser.cjs'), +} diff --git a/fullcalendar-main/tests/manual/bootstrap4.html b/fullcalendar-main/tests/manual/bootstrap4.html new file mode 100644 index 0000000..39bc448 --- /dev/null +++ b/fullcalendar-main/tests/manual/bootstrap4.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> + +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/interaction/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/multimonth/dist/index.global.js'></script> +<script src='../../packages/bootstrap4/dist/index.global.js'></script> + +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> +<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous"> +<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> +<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + customButtons: { + myCustomButton: { + text: 'custom!', + bootstrapFontAwesome: 'fa-bell', + click: function() { + alert('clicked the custom button!'); + } + } + }, + headerToolbar: { + left: 'prevYear,prev,today,next,nextYear myCustomButton', + center: 'title', + right: 'multiMonthYear,dayGridMonth,timeGridWeek,listMonth' + }, + themeSystem: 'bootstrap', + bootstrapFontAwesome: { + close: 'fa-times-circle', + prev: 'fa-caret-left', + next: 'fa-caret-right', + prevYear: 'fa-arrow-left', + nextYear: 'fa-arrow-right', + month: 'fa-calendar' + }, + initialDate: '2014-08-12', + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + navLinks: true, + events: [ + { + title: 'All Day Event', + start: '2014-08-01' + }, + { + title: 'Long Event', + start: '2014-08-07', + end: '2014-08-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-08-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-08-16T16:00:00' + }, + { + title: 'Conference', + start: '2014-08-11', + end: '2014-08-13' + }, + { + title: 'Meeting', + start: '2014-08-12T10:30:00', + end: '2014-08-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-08-12T12:00:00' + }, + { + title: 'Meeting', + start: '2014-08-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2014-08-12T17:30:00' + }, + { + title: 'Dinner', + start: '2014-08-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2014-08-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-08-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1200px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/bootstrap5.html b/fullcalendar-main/tests/manual/bootstrap5.html new file mode 100644 index 0000000..aaec2f9 --- /dev/null +++ b/fullcalendar-main/tests/manual/bootstrap5.html @@ -0,0 +1,132 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> + +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/interaction/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/multimonth/dist/index.global.js'></script> +<script src='../../packages/bootstrap5/dist/index.global.js'></script> + +<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet"> +<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> +<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> + +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + customButtons: { + myCustomButton: { + icon: 'bell', + click: function() { + alert('clicked the custom button!'); + } + } + }, + headerToolbar: { + left: 'prevYear,prev,today,next,nextYear myCustomButton', + center: 'title', + right: 'multiMonthYear,dayGridMonth,timeGridWeek,listMonth' + }, + themeSystem: 'bootstrap5', + // buttonIcons: { + // close: 'x-octagon', + // prev: 'arrow-left-circle-fill', + // next: 'arrow-right-circle-fill', + // prevYear: 'arrow-return-left', + // nextYear: 'arrow-return-right' + // }, + initialDate: '2014-08-12', + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + navLinks: true, + events: [ + { + title: 'All Day Event', + start: '2014-08-01' + }, + { + title: 'Long Event', + start: '2014-08-07', + end: '2014-08-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-08-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-08-16T16:00:00' + }, + { + title: 'Conference', + start: '2014-08-11', + end: '2014-08-13' + }, + { + title: 'Meeting', + start: '2014-08-12T10:30:00', + end: '2014-08-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-08-12T12:00:00' + }, + { + title: 'Meeting', + start: '2014-08-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2014-08-12T17:30:00' + }, + { + title: 'Dinner', + start: '2014-08-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2014-08-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-08-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1200px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/csp-nonce.html b/fullcalendar-main/tests/manual/csp-nonce.html new file mode 100644 index 0000000..47b2340 --- /dev/null +++ b/fullcalendar-main/tests/manual/csp-nonce.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<!-- <meta name='csp-nonce' content='abc123shouldbreakthings' /> --> +<meta http-equiv='Content-Security-Policy' content="default-src 'nonce-abc123'; font-src data:"> +<script nonce='abc123' src='../../bundle/dist/index.global.js'></script> +<script nonce='abc123'> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2020-09-12', + initialView: 'timeGridWeek', + nowIndicator: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + selectMirror: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01', + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style nonce='abc123'> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/external-dragging-dragula.html b/fullcalendar-main/tests/manual/external-dragging-dragula.html new file mode 100644 index 0000000..d492fe8 --- /dev/null +++ b/fullcalendar-main/tests/manual/external-dragging-dragula.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='https://cdn.jsdelivr.net/npm/dragula@3.7.3/dist/dragula.css' rel='stylesheet' /> +<script src='https://cdn.jsdelivr.net/npm/dragula@3.7.3/dist/dragula.js'></script> +<script src='../../bundle/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + + /* initialize the external events + -----------------------------------------------------------------*/ + + var containerEl = document.getElementById('external-events-list'); + + var drake = dragula({ + containers: [ containerEl ], + copy: true + }); + + new FullCalendar.ThirdPartyDraggable(containerEl, { + itemSelector: '.fc-event', + mirrorSelector: '.gu-mirror', + eventData: function(eventEl) { + return { + title: eventEl.innerText.trim() + } + } + }) + + //// the individual way to do it + // var eventEls = Array.prototype.slice.call( + // containerEl.querySelectorAll('.fc-event') + // ); + // eventEls.forEach(function(eventEl) { + // eventEl.setAttribute('data-event', JSON.stringify({ + // title: eventEl.innerText.trim() + // })); + // }); + // new FullCalendar.ThirdPartyDraggable({ + // mirrorSelector: '.gu-mirror' + // }) + + /* initialize the calendar + -----------------------------------------------------------------*/ + + var calendarEl = document.getElementById('calendar'); + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + editable: true, + droppable: true, // this allows things to be dropped onto the calendar + drop: function(arg) { + // is the "remove after drop" checkbox checked? + if (document.getElementById('drop-remove').checked) { + // if so, remove the element from the "Draggable Events" list + arg.draggedEl.parentNode.removeChild(arg.draggedEl); + } + } + }); + calendar.render(); + + }); + +</script> +<style> + + body { + margin-top: 40px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + } + + #wrap { + width: 1100px; + margin: 0 auto; + } + + #external-events { + float: left; + width: 150px; + padding: 0 10px; + border: 1px solid #ccc; + background: #eee; + text-align: left; + } + + #external-events h4 { + font-size: 16px; + margin-top: 0; + padding-top: 1em; + } + + #external-events .fc-event { + margin: 10px 0; + cursor: pointer; + } + + #external-events p { + margin: 1.5em 0; + font-size: 11px; + color: #666; + } + + #external-events p input { + margin: 0; + vertical-align: middle; + } + + #calendar { + float: right; + width: 900px; + } + +</style> +</head> +<body> + <div id='wrap'> + + <div id='external-events'> + <h4>Draggable Events</h4> + + <div id='external-events-list'> + <div class='fc-event'>My Event 1</div> + <div class='fc-event'>My Event 2</div> + <div class='fc-event'>My Event 3</div> + <div class='fc-event'>My Event 4</div> + <div class='fc-event'>My Event 5</div> + </div> + + <p> + <input type='checkbox' id='drop-remove' /> + <label for='drop-remove'>remove after drop</label> + </p> + </div> + + <div id='calendar'></div> + + <div style='clear:both'></div> + + </div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/external-dragging-jqueryui.html b/fullcalendar-main/tests/manual/external-dragging-jqueryui.html new file mode 100644 index 0000000..816c9d9 --- /dev/null +++ b/fullcalendar-main/tests/manual/external-dragging-jqueryui.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.js'></script> +<script src='https://cdn.jsdelivr.net/npm/components-jqueryui@1.12.1/jquery-ui.js'></script> +<script src='../../bundle/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + + new FullCalendar.ThirdPartyDraggable({ + mirrorSelector: '.ui-draggable-dragging' + }) + + /* initialize the external events + -----------------------------------------------------------------*/ + + $('#external-events .fc-event').each(function() { + + // store data so the calendar knows to render an event upon drop + $(this).attr('data-event', JSON.stringify({ + title: $.trim($(this).text()), // use the element's text as the event title + stick: true // maintain when user navigates (see docs on the renderEvent method) + })); + + // make the event draggable using jQuery UI + $(this).draggable({ + zIndex: 999, + revert: true, // will cause the event to go back to its + revertDuration: 0 // original position after the drag + }); + + }); + + /* initialize the calendar + -----------------------------------------------------------------*/ + + var calendarEl = document.getElementById('calendar'); + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + editable: true, + droppable: true, // this allows things to be dropped onto the calendar + drop: function(arg) { + // is the "remove after drop" checkbox checked? + if ($('#drop-remove').is(':checked')) { + // if so, remove the element from the "Draggable Events" list + $(arg.draggedEl).remove(); + } + } + }); + calendar.render(); + + }); + +</script> +<style> + + body { + margin-top: 40px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + } + + #wrap { + width: 1100px; + margin: 0 auto; + } + + #external-events { + float: left; + width: 150px; + padding: 0 10px; + border: 1px solid #ccc; + background: #eee; + text-align: left; + } + + #external-events h4 { + font-size: 16px; + margin-top: 0; + padding-top: 1em; + } + + #external-events .fc-event { + margin: 10px 0; + cursor: pointer; + } + + #external-events p { + margin: 1.5em 0; + font-size: 11px; + color: #666; + } + + #external-events p input { + margin: 0; + vertical-align: middle; + } + + #calendar { + float: right; + width: 900px; + } + +</style> +</head> +<body> + <div id='wrap'> + + <div id='external-events'> + <h4>Draggable Events</h4> + <div class='fc-event'>My Event 1</div> + <div class='fc-event'>My Event 2</div> + <div class='fc-event'>My Event 3</div> + <div class='fc-event'>My Event 4</div> + <div class='fc-event'>My Event 5</div> + <p> + <input type='checkbox' id='drop-remove' /> + <label for='drop-remove'>remove after drop</label> + </p> + </div> + + <div id='calendar'></div> + + <div style='clear:both'></div> + + </div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/google-calendar.html b/fullcalendar-main/tests/manual/google-calendar.html new file mode 100644 index 0000000..700768e --- /dev/null +++ b/fullcalendar-main/tests/manual/google-calendar.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/google-calendar/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,listYear' + }, + + displayEventTime: false, // don't show the time column in list view + + // THIS KEY WON'T WORK IN PRODUCTION!!! + // To make your own Google API key, follow the directions here: + // https://fullcalendar.io/docs/google-calendar/ + googleCalendarApiKey: 'AIzaSyDcnW6WejpTOCffshGDDb4neIrXVUA1EAE', + + // US Holidays + events: 'en.usa#holiday@group.v.calendar.google.com', + + eventClick: function(arg) { + // opens events in a popup window + window.open(arg.event.url, 'google-calendar-event', 'width=700,height=600'); + + arg.jsEvent.preventDefault() // don't navigate in main tab + }, + + loading: function(bool) { + document.getElementById('loading').style.display = + bool ? 'block' : 'none'; + } + + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #loading { + display: none; + position: absolute; + top: 10px; + right: 10px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='loading'>loading...</div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/icalendar.html b/fullcalendar-main/tests/manual/icalendar.html new file mode 100644 index 0000000..6086d59 --- /dev/null +++ b/fullcalendar-main/tests/manual/icalendar.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://github.com/mozilla-comm/ical.js/releases/download/v1.4.0/ical.js'></script> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/icalendar/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + displayEventTime: false, + initialDate: '2019-04-01', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,listYear' + }, + events: { + url: 'ics/feed.ics', + format: 'ics', + failure: function() { + document.getElementById('script-warning').style.display = 'block'; + } + }, + loading: function(bool) { + document.getElementById('loading').style.display = + bool ? 'block' : 'none'; + } + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #script-warning { + display: none; + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: red; + } + + #loading { + display: none; + position: absolute; + top: 10px; + right: 10px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='script-warning'> + <code>ics/feed.ics</code> must be servable + </div> + + <div id='loading'>loading...</div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/ics/feed.ics b/fullcalendar-main/tests/manual/ics/feed.ics new file mode 100644 index 0000000..6233d3a --- /dev/null +++ b/fullcalendar-main/tests/manual/ics/feed.ics @@ -0,0 +1,65 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190410 +DTEND;VALUE=DATE:20190413 +DTSTAMP:20201006T124223Z +UID:1234578 +CREATED:20190408T110429Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:First conference +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190416 +DTEND;VALUE=DATE:20190417 +DTSTAMP:20201008T153019Z +UID:1234578 +DTSTAMP:20201008T153019Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Second conference +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20190415T093000Z +DTEND:20190415T103000Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Hour long meeting +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20190415T141500Z +DTEND:20190415T154500Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:90 minute meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/fullcalendar-main/tests/manual/importmap.html b/fullcalendar-main/tests/manual/importmap.html new file mode 100644 index 0000000..00ecf7f --- /dev/null +++ b/fullcalendar-main/tests/manual/importmap.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<html> + <head> + <script type='importmap'> + { + "imports": { + "preact": "../../packages/core/node_modules/preact/dist/preact.mjs", + "preact/compat": "../../packages/core/node_modules/preact/compat/dist/compat.mjs", + "preact/hooks": "../../packages/core/node_modules/preact/hooks/dist/hooks.mjs", + "@fullcalendar/core": "../../packages/core/dist/index.js", + "@fullcalendar/core/index.js": "../../packages/core/dist/index.js", + "@fullcalendar/core/internal.js": "../../packages/core/dist/internal.js", + "@fullcalendar/core/preact.js": "../../packages/core/dist/preact.js", + "@fullcalendar/daygrid": "../../packages/daygrid/dist/index.js", + "@fullcalendar/daygrid/internal.js": "../../packages/daygrid/dist/internal.js", + "@fullcalendar/timegrid": "../../packages/timegrid/dist/index.js", + "@fullcalendar/timegrid/internal.js": "../../packages/timegrid/dist/internal.js", + "@fullcalendar/list": "../../packages/list/dist/index.js", + "@fullcalendar/list/internal.js": "../../packages/list/dist/internal.js" + } + } + </script> + <script type='module'> + import { Calendar } from '@fullcalendar/core' + import dayGridPlugin from '@fullcalendar/daygrid' + import timeGridPlugin from '@fullcalendar/timegrid' + import listPlugin from '@fullcalendar/list' + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new Calendar(calendarEl, { + plugins: [dayGridPlugin, timeGridPlugin, listPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: '2020-09-12', + editable: true, + navLinks: true, // can click day/week names to navigate views + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01' + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + }); + </script> + <style> + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #script-warning { + display: none; + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: red; + } + + #loading { + display: none; + position: absolute; + top: 10px; + right: 10px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + </style> + </head> + <body> + <div id='calendar'></div> + </body> +</html> diff --git a/fullcalendar-main/tests/manual/importmap.public.html b/fullcalendar-main/tests/manual/importmap.public.html new file mode 100644 index 0000000..a4430e5 --- /dev/null +++ b/fullcalendar-main/tests/manual/importmap.public.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<html> + <head> + <script type='importmap'> + { + "imports": { + "preact": "https://cdn.jsdelivr.net/npm/preact@10.19.5/dist/preact.mjs", + "preact/compat": "https://cdn.jsdelivr.net/npm/preact@10.19.5/compat/dist/compat.mjs", + "preact/hooks": "https://cdn.jsdelivr.net/npm/preact@10.19.5/hooks/dist/hooks.mjs", + "@fullcalendar/core": "https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/index.js", + "@fullcalendar/core/index.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/index.js", + "@fullcalendar/core/internal.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/internal.js", + "@fullcalendar/core/preact.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/preact.js", + "@fullcalendar/daygrid": "https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@6.1.10/index.js", + "@fullcalendar/daygrid/internal.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@6.1.10/internal.js", + "@fullcalendar/timegrid": "https://cdn.jsdelivr.net/npm/@fullcalendar/timegrid@6.1.10/index.js", + "@fullcalendar/timegrid/internal.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/timegrid@6.1.10/internal.js", + "@fullcalendar/list": "https://cdn.jsdelivr.net/npm/@fullcalendar/list@6.1.10/index.js", + "@fullcalendar/list/internal.js": "https://cdn.jsdelivr.net/npm/@fullcalendar/list@6.1.10/internal.js" + } + } + </script> + <script type='module'> + import { Calendar } from '@fullcalendar/core' + import dayGridPlugin from '@fullcalendar/daygrid' + import timeGridPlugin from '@fullcalendar/timegrid' + import listPlugin from '@fullcalendar/list' + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new Calendar(calendarEl, { + plugins: [dayGridPlugin, timeGridPlugin, listPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: '2020-09-12', + editable: true, + navLinks: true, // can click day/week names to navigate views + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01' + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + }); + </script> + <style> + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #script-warning { + display: none; + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: red; + } + + #loading { + display: none; + position: absolute; + top: 10px; + right: 10px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + </style> + </head> + <body> + <div id='calendar'></div> + </body> +</html> diff --git a/fullcalendar-main/tests/manual/jquery-connector.html b/fullcalendar-main/tests/manual/jquery-connector.html new file mode 100644 index 0000000..00eef61 --- /dev/null +++ b/fullcalendar-main/tests/manual/jquery-connector.html @@ -0,0 +1,183 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../bundle/dist/index.global.js'></script> +<script src='https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.js'></script> +<script> + + // The jQuery Connector + // ---------------------------------------------------------------------------------------------------- + + $.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1) // for a possible method call + var res = this // what this function will return (the current jQuery object by default) + + this.each(function(i, el) { // loop each DOM element involved + var $el = $(el) + var calendar = $el.data('fullCalendar') // get the existing calendar object (if any) + var singleRes // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + + if (options === 'getCalendar') { + + if (!i) { // first element only + res = calendar + } + + } else if (options === 'destroy') { // don't warn if no calendar object + + if (calendar) { + calendar.destroy() + $el.removeData('fullCalendar') + } + + } else if (!calendar) { + + console.warn('Attempting to call a FullCalendar method on an element with no calendar.') + + } else if ($.isFunction(calendar[options])) { + + singleRes = calendar[options].apply(calendar, args) + + if (!i) { + res = singleRes // record the first method call result + } + + } else { + console.warn("'" + options + "' is an unknown FullCalendar method.") + } + + // an initialization + } else { + + if (calendar) { + console.warn('Can\'t initialize another calendar on the same element.') + } else { + calendar = new FullCalendar.Calendar(el, options) + $el.data('fullCalendar', calendar) + calendar.render() + } + } + }) + + return res + } + + + // The Actual Example + // ---------------------------------------------------------------------------------------------------- + + $(document).ready(function() { + var $calendar = $('#calendar') + + $calendar.fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: '2020-09-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01', + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }) + + $('#prev-button').on('click', function() { + $calendar.fullCalendar('prev') + }) + + $('#next-button').on('click', function() { + $calendar.fullCalendar('next') + }) + + $('#gotoDate-button').on('click', function() { + $calendar.fullCalendar('gotoDate', '2020-09-01') + }) + }) + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <button id='prev-button'>prev</button> + <button id='next-button'>next</button> + <button id='gotoDate-button'>goto year 2000</button> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/js/theme-chooser.js b/fullcalendar-main/tests/manual/js/theme-chooser.js new file mode 100644 index 0000000..c967c2e --- /dev/null +++ b/fullcalendar-main/tests/manual/js/theme-chooser.js @@ -0,0 +1,149 @@ + +function initThemeChooser(settings) { + var isInitialized = false + var currentThemeSystem // don't set this directly. use setThemeSystem + var currentStylesheetEl + var loadingEl = document.getElementById('loading') + var systemSelectEl = document.querySelector('#theme-system-selector select') + var themeSelectWrapEls = Array.prototype.slice.call( // convert to real array + document.querySelectorAll('.selector[data-theme-system]'), + ) + + systemSelectEl.addEventListener('change', function() { + setThemeSystem(this.value) + }) + + setThemeSystem(systemSelectEl.value) + + themeSelectWrapEls.forEach(function(themeSelectWrapEl) { + var themeSelectEl = themeSelectWrapEl.querySelector('select') + + themeSelectWrapEl.addEventListener('change', function() { + setTheme( + currentThemeSystem, + themeSelectEl.options[themeSelectEl.selectedIndex].value, + ) + }) + }) + + + function setThemeSystem(themeSystem) { + var selectedTheme + + currentThemeSystem = themeSystem + + themeSelectWrapEls.forEach(function(themeSelectWrapEl) { + var themeSelectEl = themeSelectWrapEl.querySelector('select') + var themeSystems = (themeSelectWrapEl.getAttribute('data-theme-system') || '').split(',') + + if (themeSystems.includes(themeSystem)) { + selectedTheme = themeSelectEl.options[themeSelectEl.selectedIndex].value + themeSelectWrapEl.style.display = 'inline-block' + } else { + themeSelectWrapEl.style.display = 'none' + } + }) + + setTheme(themeSystem, selectedTheme) + } + + + function setTheme(themeSystem, themeName) { + var stylesheetUrl = generateStylesheetUrl(themeSystem, themeName) + var stylesheetEl + + function done() { + if (!isInitialized) { + isInitialized = true + settings.init(themeSystem) + } + else { + settings.change(themeSystem) + } + + showCredits(themeSystem, themeName) + } + + if (stylesheetUrl) { + stylesheetEl = document.createElement('link') + stylesheetEl.setAttribute('rel', 'stylesheet') + stylesheetEl.setAttribute('href', stylesheetUrl) + document.querySelector('head').appendChild(stylesheetEl) + + loadingEl.style.display = 'inline' + + whenStylesheetLoaded(stylesheetEl, function() { + if (currentStylesheetEl) { + currentStylesheetEl.parentNode.removeChild(currentStylesheetEl) + } + currentStylesheetEl = stylesheetEl + loadingEl.style.display = 'none' + done() + }) + } else { + if (currentStylesheetEl) { + currentStylesheetEl.parentNode.removeChild(currentStylesheetEl) + currentStylesheetEl = null + } + done() + } + } + + + function generateStylesheetUrl(themeSystem, themeName) { + if (themeSystem === 'bootstrap') { + if (themeName) { + return 'https://bootswatch.com/4/' + themeName + '/bootstrap.min.css' + } + else { // the default bootstrap theme + return 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' + } + } else if (themeSystem === 'bootstrap5') { + if (themeName) { + return 'https://bootswatch.com/5/' + themeName + '/bootstrap.min.css' + } + else { // the default bootstrap theme + return 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css' + } + } + } + + + function showCredits(themeSystem, themeName) { + var creditId + + if (themeSystem.match('bootstrap')) { + if (themeName) { + creditId = 'bootstrap-custom' + } + else { + creditId = 'bootstrap-standard' + } + } + + Array.prototype.slice.call( // convert to real array + document.querySelectorAll('.credits'), + ).forEach(function(creditEl) { + if (creditEl.getAttribute('data-credit-id') === creditId) { + creditEl.style.display = 'block' + } else { + creditEl.style.display = 'none' + } + }) + } + + + function whenStylesheetLoaded(linkNode, callback) { + var isReady = false + + function ready() { + if (!isReady) { // avoid double-call + isReady = true + callback() + } + } + + linkNode.onload = ready // does not work cross-browser + setTimeout(ready, 2000) // max wait. also handles browsers that don't support onload + } +} diff --git a/fullcalendar-main/tests/manual/json.html b/fullcalendar-main/tests/manual/json.html new file mode 100644 index 0000000..cbaa419 --- /dev/null +++ b/fullcalendar-main/tests/manual/json.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../bundle/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: '2020-09-12', + editable: true, + navLinks: true, // can click day/week names to navigate views + dayMaxEvents: true, // allow "more" link when too many events + events: { + url: 'php/get-events.php', + failure: function() { + document.getElementById('script-warning').style.display = 'block' + } + }, + loading: function(bool) { + document.getElementById('loading').style.display = + bool ? 'block' : 'none'; + } + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #script-warning { + display: none; + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: red; + } + + #loading { + display: none; + position: absolute; + top: 10px; + right: 10px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='script-warning'> + <code>php/get-events.php</code> must be running. + </div> + + <div id='loading'>loading...</div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/json/events.json b/fullcalendar-main/tests/manual/json/events.json new file mode 100644 index 0000000..29da886 --- /dev/null +++ b/fullcalendar-main/tests/manual/json/events.json @@ -0,0 +1,56 @@ +[ + { + "title": "All Day Event", + "start": "2020-09-01" + }, + { + "title": "Long Event", + "start": "2020-09-07", + "end": "2020-09-10" + }, + { + "id": "999", + "title": "Repeating Event", + "start": "2020-09-09T16:00:00-05:00" + }, + { + "id": "999", + "title": "Repeating Event", + "start": "2020-09-16T16:00:00-05:00" + }, + { + "title": "Conference", + "start": "2020-09-11", + "end": "2020-09-13" + }, + { + "title": "Meeting", + "start": "2020-09-12T10:30:00-05:00", + "end": "2020-09-12T12:30:00-05:00" + }, + { + "title": "Lunch", + "start": "2020-09-12T12:00:00-05:00" + }, + { + "title": "Meeting", + "start": "2020-09-12T14:30:00-05:00" + }, + { + "title": "Happy Hour", + "start": "2020-09-12T17:30:00-05:00" + }, + { + "title": "Dinner", + "start": "2020-09-12T20:00:00" + }, + { + "title": "Birthday Party", + "start": "2020-09-13T07:00:00-05:00" + }, + { + "title": "Click for Google", + "url": "http://google.com/", + "start": "2020-09-28" + } +] diff --git a/fullcalendar-main/tests/manual/locales-es.html b/fullcalendar-main/tests/manual/locales-es.html new file mode 100644 index 0000000..1d0c3db --- /dev/null +++ b/fullcalendar-main/tests/manual/locales-es.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/core/dist/locales/es.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + initialDate: '2020-09-12', + locale: 'es', + buttonIcons: false, // show the prev/next text + weekNumbers: true, + navLinks: true, // can click day/week names to navigate views + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01' + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/locales.html b/fullcalendar-main/tests/manual/locales.html new file mode 100644 index 0000000..b0b9709 --- /dev/null +++ b/fullcalendar-main/tests/manual/locales.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/core/dist/locales-all.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/multimonth/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var initialLocaleCode = 'en'; + var localeSelectorEl = document.getElementById('locale-selector'); + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth,multiMonthYear' + }, + initialDate: '2020-09-12', + locale: initialLocaleCode, + buttonIcons: false, // show the prev/next text + weekNumbers: true, + navLinks: true, // can click day/week names to navigate views + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01' + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + + // build the locale selector's options + calendar.getAvailableLocaleCodes().forEach(function(localeCode) { + var optionEl = document.createElement('option'); + optionEl.value = localeCode; + optionEl.selected = localeCode == initialLocaleCode; + optionEl.innerText = localeCode; + localeSelectorEl.appendChild(optionEl); + }); + + // when the selected option changes, dynamically change the calendar option + localeSelectorEl.addEventListener('change', function() { + if (this.value) { + calendar.setOption('locale', this.value); + } + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #top { + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + font-size: 12px; + } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='top'> + + Locales: + <select id='locale-selector'></select> + + </div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/blank.html b/fullcalendar-main/tests/manual/old/blank.html new file mode 100644 index 0000000..cfe2fa9 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/blank.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<script src='../../dist/fullcalendar.js'></script> +<script> + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/clipped-height-interactions.html b/fullcalendar-main/tests/manual/old/clipped-height-interactions.html new file mode 100644 index 0000000..75c51b3 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/clipped-height-interactions.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<script src='../../dist/fullcalendar.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,day' + }, + initialDate: '2018-12-12', + navLinks: true, // can click day/week names to navigate views + selectable: true, + dateClick: function(info) { + console.log('dateClick', info.dateStr) + } + }); + + calendar.render(); + }); + +</script> +<style> + + /* + for + https://github.com/fullcalendar/fullcalendar/issues/3615 + */ + + html, body { + height: 100%; + } + + body { + border: 1px solid red; + overflow-x: hidden; + } + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/data_as_a_function.html b/fullcalendar-main/tests/manual/old/data_as_a_function.html new file mode 100644 index 0000000..b7014cf --- /dev/null +++ b/fullcalendar-main/tests/manual/old/data_as_a_function.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + year: 2010, + month: 0, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: { + url: "many_events_json.txt", + data: function() { + var custom_data = { q: 'custom data value' }; // should see this in Networking + console.log('setting custom_data as part of the eventSource fetch'); + return custom_data; + } + } + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/date_util_test.html b/fullcalendar-main/tests/manual/old/date_util_test.html new file mode 100644 index 0000000..7180bbf --- /dev/null +++ b/fullcalendar-main/tests/manual/old/date_util_test.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/moment/min/locales.min.js'></script> +<script>FC = {}</script> +<script src='../src/util.js'></script> +<script src='../src/moment-ext.js'></script> +<script src='../src/date-formatting.js'></script> +</head> +<body> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/day_render.html b/fullcalendar-main/tests/manual/old/day_render.html new file mode 100644 index 0000000..90905a6 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/day_render.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + + dayRender: function(date, td) { + //console.log(date, td); + td.find('.fc-day-content').prepend('this is the ' + date.getDate()); + }, + + dayClick: function(date, allDay) { + console.log(date, allDay); + }, + weekNumbers: true, + //selectable: true, + //direction: 'rtl', + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/dbclick.html b/fullcalendar-main/tests/manual/old/dbclick.html new file mode 100644 index 0000000..8118310 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/dbclick.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + eventRender: function(event, element) { + element.bind('dblclick', function() { + alert('double click!'); + }); + // alert shows up in linux chrome, but messes up draggable + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/dnd.html b/fullcalendar-main/tests/manual/old/dnd.html new file mode 100644 index 0000000..d032c52 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/dnd.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../dist/fullcalendar.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + + // var PointerDragging = FullCalendar.PointerDragging; + + // var pointer = new PointerDragging(document.getElementById('box')) + // pointer.on('down', function(ev) { + // console.log('down', ev) + // pointer.cancelTouchScroll() + // }) + // pointer.on('move', function(ev) { + // console.log('move', ev) + // }) + // pointer.on('up', function(ev) { + // console.log('up', ev) + // }) + + // TODO: need to expose + // var FeaturefulDragging = FullCalendar.FeaturefulDragging; + + // var listener = new FeaturefulDragging({ + // containerEl: document.getElementById('box'), + // selector: 'a', + // touchDelay: 1000, + // mouseMinDistance: 20, + // touchScrollAllowed: false + // }) + // listener.on('pointerdown', function(ev) { + // console.log('pointerdown', ev) + // }) + // listener.on('dragstart', function(ev) { + // console.log('dragstart', ev) + // }) + // listener.on('dragmove', function(ev) { + // console.log('dragmove', ev) + // }) + // listener.on('dragend', function(ev) { + // console.log('dragend', ev) + // }) + // listener.on('pointerup', function(ev) { + // console.log('pointerup', ev) + // }) + }); + +</script> +</head> +<body> + + <div style='width:400px; height:600px; margin:0 auto; overflow:auto; background:yellow'> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra. + </p> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra. + </p> + <div id='box' style='width: 100px; height: 100px; background: red'> + <a>stuff in here</a> + </div> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra. + </p> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra. + </p> + </div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/droppable.html b/fullcalendar-main/tests/manual/old/droppable.html new file mode 100644 index 0000000..74b82e9 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/droppable.html @@ -0,0 +1,175 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/components-jqueryui/jquery-ui.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + + editable: true, + droppable: true, + drop: function(date, allDay, ev) { + console.log('drop', date, allDay, ev); + }, + //initialView: 'week', + + //firstDay: 1, + //hiddenDays: [ 4, 6 ], // hide thursday and saturday + //direction: 'rtl', + //slotMinTime: '6:30am', + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + $('.external-event').draggable({ + revert: true, + revertDuration: 0, + zIndex: 999 + }); + + $('#sortable-events').sortable(); + + $('#calendar2').fullCalendar({ + //direction: 'rtl', + droppable: true, + dropAccept: '.for-calendar2', + /* + dropAccept: function(e) { + console.log(e); + console.log(this); + return e.text() == 'Draggable 1'; + }, + */ + drop: function(date, allDay) { + console.log('drop 2nd calendar', date, allDay); + }, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + } + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + float: left; + } + + #external-events, #sortable-events { + position: relative; + left: 50px; + text-align: left; + float: left; + width: 140px; + padding: 10px; + border: 1px solid #aaa; + background: #ccc; + } + + .external-event, .sortable-event { + height: 20px; + line-height: 20px; + color: #fff; + background: blue; + margin-bottom: 10px; + padding-left: 5px; + cursor: pointer; + } + + #calendar2 { + width: 900px; + margin-top: 50px; + } + +</style> +</head> +<body> +<div id='calendar'></div> +<div id='external-events'> + <div class='external-event'>Draggable 1</div> + <div class='external-event'>Draggable 2</div> + <div class='external-event for-calendar2'>Draggable 3</div> +</div> +<div id='sortable-events'> + <div class='sortable-event'>Sortable 1</div> + <div class='sortable-event'>Sortable 2</div> + <div class='sortable-event for-calendar2'>Sortable 3</div> +</div> +<div style='clear:both'></div> +<div id='calendar2'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/dynamic-options.html b/fullcalendar-main/tests/manual/old/dynamic-options.html new file mode 100644 index 0000000..bbe6440 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/dynamic-options.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + var direction = 'ltr'; + var businessHours = false; + + $('#change-direction-button').on('click', function() { + direction = direction === 'ltr' ? 'rtl' : 'ltr'; + $('#calendar').fullCalendar('option', 'direction', direction); + }); + + $('#change-businessHours-button').on('click', function() { + businessHours = !businessHours; + $('#calendar').fullCalendar('option', 'businessHours', businessHours); + }); + + $('#change-all-button').on('click', function() { + direction = direction === 'ltr' ? 'rtl' : 'ltr'; + businessHours = !businessHours; + $('#calendar').fullCalendar('option', { + direction: direction, + businessHours: businessHours + }); + }); + + $('#calendar').fullCalendar({ + direction: direction, + businessHours: businessHours, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,day' + }, + initialDate: '2014-06-12', + editable: true, + events: [ + { + title: 'All Day Event', + start: '2014-06-01' + }, + { + title: 'Long Event', + start: '2014-06-07', + end: '2014-06-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-06-12T10:30:00', + end: '2014-06-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-06-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-06-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-06-28' + } + ] + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + <button id='change-direction-button'>change direction</button> + <button id='change-businessHours-button'>change businessHours</button> + <button id='change-all-button'>change all</button> + + <div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/event_data_transform.html b/fullcalendar-main/tests/manual/old/event_data_transform.html new file mode 100644 index 0000000..8a5364d --- /dev/null +++ b/fullcalendar-main/tests/manual/old/event_data_transform.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + + eventDataTransform: function(event) { + var copy = $.extend({}, event); + copy.title += "*"; + return copy; + }, + events: { + eventDataTransform: function(event) { + var copy = $.extend({}, event); + copy.title += "!"; + return copy; + }, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + } + + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/event_stress_test.html b/fullcalendar-main/tests/manual/old/event_stress_test.html new file mode 100644 index 0000000..f75ae1e --- /dev/null +++ b/fullcalendar-main/tests/manual/old/event_stress_test.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var events = [], + id = 1, + start = null, + end = null; + for (var d = 1; d < 31; d++) { + for (var t = 0; t < 50; t++) { + start = new Date(2014, 7, d, 8, 0, 0, 0); + start.setTime(start.getTime() + (t * 5 * 60 * 1000)); + end = start; + end.setTime(start.getTime() + (60 * 60 * 1000)); + events.push({ + id: id, + start: start, + end: end, + title: "Event #" + id, + name: "Full event name on " + start.toLocaleString() + }); + id++; + } + } + console.log("Events " + events.length); + var runStart = new Date(); + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,day' + }, + initialDate: '2014-08-12', + editable: true, + events: events + }); + var runSetup = new Date(); + console.log("Milliseconds to setup: " + (runSetup.getTime() - runStart.getTime())); + $('#calendar').fullCalendar("removeEventSource", events); + var runRemove = new Date(); + console.log("Remove source: " + (runRemove.getTime() - runSetup.getTime())); + $('#calendar').fullCalendar("addEventSource", events); + var runAdd = new Date(); + console.log("Add source: " + (runAdd.getTime() - runRemove.getTime())); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/external-dragging-touch-punch.html b/fullcalendar-main/tests/manual/old/external-dragging-touch-punch.html new file mode 100644 index 0000000..1862603 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/external-dragging-touch-punch.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/components-jqueryui/jquery-ui.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js'></script> +<script> + + $(document).ready(function() { + + + /* initialize the external events + -----------------------------------------------------------------*/ + + $('#external-events .fc-event').each(function() { + + // store data so the calendar knows to render an event upon drop + $(this).data('event', { + title: $.trim($(this).text()), // use the element's text as the event title + stick: true // maintain when user navigates (see docs on the renderEvent method) + }); + + // make the event draggable using jQuery UI + $(this).draggable({ + zIndex: 999, + revert: true, // will cause the event to go back to its + revertDuration: 0 // original position after the drag + }); + + }); + + + /* initialize the calendar + -----------------------------------------------------------------*/ + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,day' + }, + editable: true, + droppable: true, // this allows things to be dropped onto the calendar + drop: function() { + // is the "remove after drop" checkbox checked? + if ($('#drop-remove').is(':checked')) { + // if so, remove the element from the "Draggable Events" list + $(this).remove(); + } + } + }); + + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 14px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #wrap { + width: 1100px; + margin: 0 auto; + } + + #external-events { + float: left; + width: 150px; + padding: 0 10px; + border: 1px solid #ccc; + background: #eee; + text-align: left; + } + + #external-events h4 { + font-size: 16px; + margin-top: 0; + padding-top: 1em; + } + + #external-events .fc-event { + margin: 10px 0; + cursor: pointer; + } + + #external-events p { + margin: 1.5em 0; + font-size: 11px; + color: #666; + } + + #external-events p input { + margin: 0; + vertical-align: middle; + } + + #calendar { + float: right; + width: 900px; + } + +</style> +</head> +<body> + <div id='wrap'> + + <div id='external-events'> + <h4>Draggable Events</h4> + <div class='fc-event'>My Event 1</div> + <div class='fc-event'>My Event 2</div> + <div class='fc-event'>My Event 3</div> + <div class='fc-event'>My Event 4</div> + <div class='fc-event'>My Event 5</div> + <p> + <input type='checkbox' id='drop-remove' /> + <label for='drop-remove'>remove after drop</label> + </p> + </div> + + <div id='calendar'></div> + + <div style='clear:both'></div> + + </div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/fullheight.html b/fullcalendar-main/tests/manual/old/fullheight.html new file mode 100644 index 0000000..0d7858e --- /dev/null +++ b/fullcalendar-main/tests/manual/old/fullheight.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + footerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ], + fixedWeekCount: false, + height: '100%' + }); + + }); + +</script> +<style> + + html, body { + margin: 0; + padding: 0; + height: 100%; + } + + body { + position: relative; + font-size: 14px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar-wrap { + position: absolute; + top: 20px; + right: 20px; + bottom: 20px; + left: 20px; + } + +</style> +</head> +<body> + + <div id='calendar-wrap'> + <div id='calendar'></div> + </div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/gcal.html b/fullcalendar-main/tests/manual/old/gcal.html new file mode 100644 index 0000000..88b107c --- /dev/null +++ b/fullcalendar-main/tests/manual/old/gcal.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + $(document).ready(function() { + $('#calendar').fullCalendar({ + weekends: false, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,week,dayGridWeek,day,dayGridDay' + }, + //editable: true, + eventSources: [ + { + url: "http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic", + editable: true, + className: 'holiday' + }, + /* + FullCalendar.gcalFeed( + "http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic", + { + editable: true, + className: 'holiday' + } + ), + */ + { + url: "https://www.google.com/calendar/feeds/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic", + currentTimezone: 'America/Edmonton', // 'America/Los_Angeles' 'America/Los Angeles' + editable: true + } + ], + eventClick: function(event) { + console.log(event.start); + console.log(event.end); + return false; + } + }); + }); + +</script> +<style> + + .holiday * { + color: yellow !important; + } + +</style> +</head> +<body style='font-size:12px'> +<div id='calendar' style='width:900px;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/hiddenDays.html b/fullcalendar-main/tests/manual/old/hiddenDays.html new file mode 100644 index 0000000..b4872cf --- /dev/null +++ b/fullcalendar-main/tests/manual/old/hiddenDays.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + + year: 2013, + month: 6, // july + + //weekNumbers: true, + direction: 'rtl', + //selectable: true, + //firstDay: 2, + //weekends: false, + hiddenDays: [ 4 ], // thursday + + events: [ + { + title: '1 Day', + start: new Date(2013, 6, 17) + }, + { + title: '2 Day', + start: new Date(2013, 6, 17), + end: new Date(2013, 6, 18) + }, + { + title: '3 Day', + start: new Date(2013, 6, 17), + end: new Date(2013, 6, 19) + }, + { + title: '1 Day IN GAP', + start: new Date(2013, 6, 18) // won't be visible + } + ] + + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/icons.html b/fullcalendar-main/tests/manual/old/icons.html new file mode 100644 index 0000000..5e1dd7f --- /dev/null +++ b/fullcalendar-main/tests/manual/old/icons.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../src/timeline/timeline.css' rel='stylesheet' /> +<style> + + .fc-icon { + clear: left; + float: left; + margin-bottom: .1em; + border: 5px solid #fff; + color: #000; + background: #fff; + } + + .inverse .fc-icon { + color: #fff; + background: #000; + } + + .bordered .fc-icon { + border-color: #666; + } + + td { + vertical-align: top; + padding: 10px 25px; + } + +</style> +<script> + + $(function() { + var fontSizes = [ '50px', '26px', '16px', '14px' ]; + var td = $('td'); + var i; + + for (i = 1; i < fontSizes.length; i++) { + td.after(td.clone()); + } + + $('td').each(function(i) { + $(this).css('font-size', fontSizes[i]); + }); + + $(document) + .on('click', function() { + $('body').toggleClass('inverse'); + }) + .on('mousedown', function() { + return false; // prevent native text selection + }); + + $('td').on('click', function() { + $(this).toggleClass('bordered'); + return false; // prevent native text selection + }); + }); + +</script> +</head> +<body> +<table><tr><td> + + <span class='fc-icon fc-icon-chevron-left'></span> + <span class='fc-icon fc-icon-chevron-right'></span> + <span class='fc-icon fc-icon-chevrons-left'></span> + <span class='fc-icon fc-icon-chevrons-right'></span> + <span class='fc-icon fc-icon-plus-square'></span> + <span class='fc-icon fc-icon-minus-square'></span> + <span class='fc-icon fc-icon-x'></span> + +</td></tr></table> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_1035.html b/fullcalendar-main/tests/manual/old/issue_1035.html new file mode 100644 index 0000000..4addeff --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_1035.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + initialView: 'week', + selectable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + + .fc-timeGrid-slots td div { + height: 10px; + } + +</style> +</head> +<body> + +<p>The selection box for selecting timegrid slot cells should align to the grid.</p> +<p>(even though <code>.fc-timeGrid-slots td div</code> has been set to 10px tall)</p> +<p>(even though <code>.fc-timeGrid-slots td div</code> has been set to 10px tall)</p> + +<div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_2048.html b/fullcalendar-main/tests/manual/old/issue_2048.html new file mode 100644 index 0000000..352ec29 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_2048.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + initialView: 'week', + initialDate: '2013-08-01', + scrollTime: '00:00', + editable: true, + events: [ + { + title: 'All Day Event', + start: '2013-08-01' + } + ], + eventDrop: function(event, delta, revertFunc) { + if (!confirm('Move event?')) { + revertFunc(); + } + else { + alert( + 'delta (days/hours/minutes) is ' + + Math.floor(delta.asDays()) + '/' + + delta.hours() + '/' + + delta.minutes() + ); + alert('allDay is ' + event.allDay); + } + } + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_206_parseDate_dst.html b/fullcalendar-main/tests/manual/old/issue_206_parseDate_dst.html new file mode 100644 index 0000000..4c8fcdd --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_206_parseDate_dst.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + // set your time to Tehran time (GMT+03:30) + // (recreated on a Windows XP machine, after restarting) + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + year: 2010, + month: 2, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'Yay Tehran!', + start: '2010-03-21' // should NOT show up on the 20th + //allDay: false // if uncommented, will show 1am + + // HOWEVER, when set to 2010-03-21T00:30:00, this ends up being 2010-03-21T01:30:00 + // should it be 2010-03-21T01:00:00 instead!!?? + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_2154.html b/fullcalendar-main/tests/manual/old/issue_2154.html new file mode 100644 index 0000000..3b77d76 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_2154.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/moment-timezone/moment-timezone.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + //moment-timezone-data + moment.tz.add({ + "zones": { + "CET": [ + "1 C-Eur CE%sT" + ], + "Etc/GMT": [ + "0 - GMT" + ], + "Etc/UTC": [ + "0 - UTC" + ], + "Europe/Berlin": [ + "0:53:28 - LMT 1893_3 0:53:28", + "1 C-Eur CE%sT 1945_4_24_2 2", + "1 SovietZone CE%sT 1946 1", + "1 Germany CE%sT 1980 1", + "1 EU CE%sT" + ] + }, + "rules": { + "C-Eur": [ + "1916 1916 3 30 7 23 0 1 S", + "1916 1916 9 1 7 1 0 0", + "1917 1918 3 15 1 2 2 1 S", + "1917 1918 8 15 1 2 2 0", + "1940 1940 3 1 7 2 2 1 S", + "1942 1942 10 2 7 2 2 0", + "1943 1943 2 29 7 2 2 1 S", + "1943 1943 9 4 7 2 2 0", + "1944 1945 3 1 1 2 2 1 S", + "1944 1944 9 2 7 2 2 0", + "1945 1945 8 16 7 2 2 0", + "1977 1980 3 1 0 2 2 1 S", + "1977 1977 8 0 8 2 2 0", + "1978 1978 9 1 7 2 2 0", + "1979 1995 8 0 8 2 2 0", + "1981 9999 2 0 8 2 2 1 S", + "1996 9999 9 0 8 2 2 0" + ], + "SovietZone": [ + "1945 1945 4 24 7 2 0 2 M", + "1945 1945 8 24 7 3 0 1 S", + "1945 1945 10 18 7 2 2 0" + ], + "Germany": [ + "1946 1946 3 14 7 2 2 1 S", + "1946 1946 9 7 7 2 2 0", + "1947 1949 9 1 0 2 2 0", + "1947 1947 3 6 7 3 2 1 S", + "1947 1947 4 11 7 2 2 2 M", + "1947 1947 5 29 7 3 0 1 S", + "1948 1948 3 18 7 2 2 1 S", + "1949 1949 3 10 7 2 2 1 S" + ], + "EU": [ + "1977 1980 3 1 0 1 1 1 S", + "1977 1977 8 0 8 1 1 0", + "1978 1978 9 1 7 1 1 0", + "1979 1995 8 0 8 1 1 0", + "1981 9999 2 0 8 1 1 1 S", + "1996 9999 9 0 8 1 1 0" + ] + }, + "links": {} + }); + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + editable: true, + timeZone: 'Europe/Berlin', + events: [ + { + title: "event1", + start: moment.parseZone('2014-05-30T10:00:00+02:00').tz('Europe/Berlin') + } + ], + eventRender: function(event, el) { + // render the timezone offset below the event title + if (event.start.hasZone()) { + el.find('.fc-event-title').after( + $('<div class="tzo"/>').text(event.start.format('Z')) + ); + } + } + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_220_buttons_ie6.html b/fullcalendar-main/tests/manual/old/issue_220_buttons_ie6.html new file mode 100644 index 0000000..6e974e7 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_220_buttons_ie6.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + } + }); + + }); + +</script> +<style> + + body { + margin: 0; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + +</style> +</head> +<body> +<div style='height:108px;position:relative'></div> +<div style='position:relative;margin-top:-50px;width:900px;z-index:2'> +<div style='position:relative;padding:3px'> + +<div style='margin: 10px 20px 0'>Nav</div> + +<div style='margin:20px 0 0;padding:0 20px'> + +<p> +this is a paragraph +</p> + +<div id='calendar' style='margin:3em 0;direction:ltr'></div> + +</div> + +</div> +</div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_221_quick_remove_source.html b/fullcalendar-main/tests/manual/old/issue_221_quick_remove_source.html new file mode 100644 index 0000000..b6b0390 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_221_quick_remove_source.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + var gcalFeed = FullCalendar.gcalFeed("http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic"); + + $(document).ready(function() { + + var cal = $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + loading: function(bool) { + if (bool) { + $('#loading').show(); + }else{ + $('#loading').hide(); + } + } + }); + + cal.fullCalendar('addEventSource', gcalFeed); + cal.fullCalendar('removeEventSource', gcalFeed); + + // events should not be rendered when jsonp returns! + + }); + +</script> +<style> + + .red-event a { + background: red; + } + + .yellow-event a { + background: yellow; + } + + .black-text-event a { + color: #000; + } + + button { + font-size: 11px; + } + +</style> +</head> +<body style='font-size:12px'> +<div id='loading' style='position:absolute;top:0;left:0;display:none'>loading...</div> +<div id='calendar' style='width:900px;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_230_height_json_events.html b/fullcalendar-main/tests/manual/old/issue_230_height_json_events.html new file mode 100644 index 0000000..bbba02c --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_230_height_json_events.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + //year: 2009, + //month: 10, + //date: 22, + //initialView: 'week', // error also occured with month view + editable: true, + events: "../demos/json-events.php" + }); + + $('#calendar').fullCalendar('option', 'height', $(window).height()-80); + // shouldn't throw an error + + }); + + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_2396_Sydney.html b/fullcalendar-main/tests/manual/old/issue_2396_Sydney.html new file mode 100644 index 0000000..17b0525 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_2396_Sydney.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + initialDate: '2014-04-05', + initialView: 'day', + selectable: true, + timeZone: 'local', + dayClick: function(date) { + console.log('dayClick', date.format()); + }, + select: function(start, end) { + console.log('select', start.format(), end.format(), start.isBefore(end)); + var title = prompt('Event Title:'); + var eventData; + if (title) { + eventData = { + title: title, + start: start, + end: end + }; + $('#calendar').fullCalendar('renderEvent', eventData, true); // stick? = true + } + $('#calendar').fullCalendar('unselect'); + } + }); + + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <p style='text-align:center;margin-bottom: 3em'> + To recreate, set computer's timezone to Australia/Sydney, open in Safari, and try to select any time after 3pm. + </p> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_244_aspectRatio_0.html b/fullcalendar-main/tests/manual/old/issue_244_aspectRatio_0.html new file mode 100644 index 0000000..ea69a96 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_244_aspectRatio_0.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + initialView: 'week', // month view also looked scrunched + aspectRatio: 0 + }); + + // shouldnt allow aspectRatios to *actually* go under .5 + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_251_empty_end_date.html b/fullcalendar-main/tests/manual/old/issue_251_empty_end_date.html new file mode 100644 index 0000000..540f3a3 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_251_empty_end_date.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1), + end: '' // shouldn't choke on empty string + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_333_blinking.html b/fullcalendar-main/tests/manual/old/issue_333_blinking.html new file mode 100644 index 0000000..1949ff3 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_333_blinking.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + + initialView: 'day', + slotMinTime: 0, + slotMaxTime: 24, + firstDay: 1, + allDayContent: "dieną", + + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ], + + headerToolbar: { + left: 'prev', + center: 'title', + right: 'next next next' + }, + allDaySlot: true, + editable: true, + titleFormat: + { + month: 'MMMM yyyy', // September 2009 + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", // Sep 7 - 13 2009 + day: 'dddd' // Tuesday, Sep 8, 2009 + }, + + slotLabelFormat: 'HH(:mm)', + timeFormat: { timeGrid: 'HH:mm{ - HH:mm}' }, + weekends: true + }); + +}); + + +</script> + +<body> +<div id="calendar"> </div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_417_refetchEvents.html b/fullcalendar-main/tests/manual/old/issue_417_refetchEvents.html new file mode 100644 index 0000000..9eb3aab --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_417_refetchEvents.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + /* + - click on day button + - click refetchEvents + - click on month button + event shouldn't disappear!!! + */ + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + year: 2010, + month: 9, + date: 31, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,day' + }, + editable: true, + events: FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic') + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick="$('#calendar').fullCalendar('refetchEvents')">refetchEvents</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_429_gotoDate.html b/fullcalendar-main/tests/manual/old/issue_429_gotoDate.html new file mode 100644 index 0000000..1a49538 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_429_gotoDate.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + year: 2015, + month: 0, + date: 31 + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick="$('#calendar').fullCalendar('gotoDate', 2015, 1)">gotoDate - feb 2015</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_477_event_width.html b/fullcalendar-main/tests/manual/old/issue_477_event_width.html new file mode 100644 index 0000000..9e9397d --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_477_event_width.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + // need to change to GMT+2 to recreate this bug!!!! + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + year: 2010, + month: 4, + firstDay: 1, + events: [ + {"id":"1","title":"testcase 1 BAD","start":"1272822975","end":"1272837609","allDay":false} + //{"id":"2","title":"testcase 2 GOOD","start":"1272822975","end":"1272837809","allDay":false}, + //{"id":"3","title":"testcase 3 GOOD too","start":"1272822975","end":"1272837609","allDay":true} + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_517_js_error_ie.html b/fullcalendar-main/tests/manual/old/issue_517_js_error_ie.html new file mode 100644 index 0000000..00aa3e8 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_517_js_error_ie.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + initialView: 'week', + events: [ + { + title: 'Event1', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event2', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event3', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event4', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event5', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event6', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event7', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event8', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event9', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event10', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event11', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event12', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event13', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event14', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event15', + start: new Date(y, m, d, 6, 0), + allDay : false + }, + { + title: 'Event16', + start: new Date(y, m, d, 6, 0), + allDay : false + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 400px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_554.html b/fullcalendar-main/tests/manual/old/issue_554.html new file mode 100644 index 0000000..a9a976a --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_554.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + year: 2010, + month: 9, + date: 31, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,day' + }, + events: FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic') + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick="$('#calendar').fullCalendar('gotoDate',2010,10,11);$('#calendar').fullCalendar('changeView','day')">gotoDate + changeView</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_586_refetchEvents.html b/fullcalendar-main/tests/manual/old/issue_586_refetchEvents.html new file mode 100644 index 0000000..09de3e1 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_586_refetchEvents.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic') + }); + + }); + + function doit() { + var calendar = $('#calendar'); + calendar.fullCalendar('removeEvents'); + calendar.fullCalendar('refetchEvents'); + calendar.fullCalendar('refetchEvents'); + calendar.fullCalendar('refetchEvents'); + calendar.fullCalendar('refetchEvents'); + } + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick='doit()'>do it</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_616.html b/fullcalendar-main/tests/manual/old/issue_616.html new file mode 100644 index 0000000..280bb37 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_616.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true + }); + + }); + + + function doit() { + var calendar = $('#calendar'); + var google-calendar = FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic'); + calendar.fullCalendar('addEventSource', google-calendar); + calendar.fullCalendar('refetchEvents'); + } + + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick='doit()'>do it</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_679.html b/fullcalendar-main/tests/manual/old/issue_679.html new file mode 100644 index 0000000..93bf8ea --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_679.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true + }); + + }); + + + function doit() { + var calendar = $('#calendar'); + var holidays = FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic'); + var moon = FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic'); + var australia = FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/en.australian%23holiday%40group.v.calendar.google.com/public/basic'); + calendar.fullCalendar('addEventSource', holidays); + calendar.fullCalendar('addEventSource', moon); + calendar.fullCalendar('addEventSource', australia); + } + + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick='doit()'>do it</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_688_parseInt.html b/fullcalendar-main/tests/manual/old/issue_688_parseInt.html new file mode 100644 index 0000000..5c25ce0 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_688_parseInt.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + initialView: 'week', + slotMinTime: '09:30', + slotMaxTime: '15:30', + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_740_event_resizing.html b/fullcalendar-main/tests/manual/old/issue_740_event_resizing.html new file mode 100644 index 0000000..cfc0a5d --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_740_event_resizing.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + weekends: false, + //firstDay: 1, + //direction: 'rtl', + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_750.html b/fullcalendar-main/tests/manual/old/issue_750.html new file mode 100644 index 0000000..ae25929 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_750.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + year: 2011, + month: 6, // august + ignoreTimezone: false, + events: [ + { + title: 'All Day Event', + start: '2011-07-05T12:00:00Z', // august + allDay: false + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/issue_757_removeEvents.html b/fullcalendar-main/tests/manual/old/issue_757_removeEvents.html new file mode 100644 index 0000000..2a4a1af --- /dev/null +++ b/fullcalendar-main/tests/manual/old/issue_757_removeEvents.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + //direction: 'rtl', + year: 2011, + month: 0, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<button onclick="$('#calendar').fullCalendar('removeEvents')">removeEvents</button> +<button onclick="$('#calendar').fullCalendar('renderEvent', { title:'hey', start:'2011-01-09' }, true)">addEvent (with stick)</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/liquidwidth.html b/fullcalendar-main/tests/manual/old/liquidwidth.html new file mode 100644 index 0000000..fe4bdbd --- /dev/null +++ b/fullcalendar-main/tests/manual/old/liquidwidth.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + //initialView: 'week', + + slotMinTime: '5:30am', + slotMaxTime: '5:30pm', + + //handleWindowResize: false, + + // TODO: there is a bug switching into week from month with a 1280x1024 (with firebug showing) + + headerToolbar: { + left: 'prev,next today', + center: 'prev title next', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + + }); + + }); + +</script> +<style> + + html { + overflow: auto; + } + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 80%; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/list-view.html b/fullcalendar-main/tests/manual/old/list-view.html new file mode 100644 index 0000000..0815c48 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/list-view.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'listMonth,month,week' + }, + initialView: 'listMonth', + initialDate: '2014-06-12', + editable: true, + eventSources: [ + { + color: 'green', + events: [ + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-06-01' + }, + { + id: 'goog-window-1', + title: 'Click for Google, new window 1', + url: 'http://google.com/', + start: '2014-06-01' + }, + { + id: 'goog-window-2', + title: 'Click for Google, new window 2', + url: 'http://google.com/', + start: '2014-06-01' + } + ] + }, + { + events: [ + { + title: 'Event With Color', + color: 'red', + start: '2014-06-02' + }, + { + title: 'All Day Event', + start: '2014-06-02' + }, + { + title: 'Long Event', + start: '2014-06-07', + end: '2014-06-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-06-12T10:30:00', + end: '2014-06-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-06-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-06-13T07:00:00' + } + ] + } + ], + eventClick: function(event, jsEvent) { + console.log('eventClick', event.title); + + if (event.id === 'goog-window-1') { + window.open(event.url); + return false; + } + else if (event.id === 'goog-window-2') { + jsEvent.preventDefault(); + window.open(event.url); + } + }, + eventMouseEnter: function(arg) { + console.log('eventMouseEnter', arg.event.title); + }, + eventMouseLeave: function(arg) { + console.log('eventMouseLeave', arg.event.title); + } + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/locale.html b/fullcalendar-main/tests/manual/old/locale.html new file mode 100644 index 0000000..da47adc --- /dev/null +++ b/fullcalendar-main/tests/manual/old/locale.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<html> +<head> +<style> + + /* http://code.google.com/p/fullcalendar/issues/detail?id=193 */ + .fc .fc-sat, .fc .fc-sun { background-color:red } + +</style> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $(document).ready(function() { + $('#calendar').fullCalendar({ + + headerToolbar: { + left: 'nextYear,next,prev,prevYear today', + center: 'month,week,dayGridWeek,day,dayGridDay', + right: 'title prev,next' + }, + + editable: true, + + direction: 'rtl', + firstDay: 1, + //weekends: false, + //hiddenDays: [ 4 ], // hide thursday + + //weekNumbers: true, + + slotMinTime: '8am', + slotMaxTime: '11:30pm', + firstHour: 9, + + monthNames: ["januari", "februari", "maart", "april", "mei", "juni","juli", "augustus", "september", "oktober", "november", "december"], + monthNamesShort: ["jan", "feb", "maa", "apr", "mei", "jun", "jul", "aug","sep", "okt", "nov", "dec"], + dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag','donderdag', 'vrijdag', 'zaterdag'], + dayNamesShort: ["zo", "ma", "di", "wo", "do", "vr", "za", "zo"], + + //timeFormat: 'H(:mm)t', + //timeFormat: 'MMMM,dddd', + + dayClick: function(dayDate, allDay, ev, view) { + console.log('dayClick - ' + dayDate + ' - allDay:' + allDay + ' --- ' + view.title); + }, + + eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view) { + console.log('DROP ' + event.title); + console.log(dayDelta + ' days'); + console.log(minuteDelta + ' minutes'); + console.log('allDay: ' + allDay); + console.log(event.start); + //console.log(minuteDelta + ' minutes'); + }, + + eventResize: function(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view) { + console.log('RESIZE!! ' + event.title); + console.log(dayDelta + ' days'); + console.log(minuteDelta + ' minutes'); + console.log(event.end); + //console.log(minuteDelta + ' minutes'); + }, + + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + }); + +</script> +</head> +<body style='font-size:12px'> +<div id='calendar' style='width:900px;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/long_event_titles.html b/fullcalendar-main/tests/manual/old/long_event_titles.html new file mode 100644 index 0000000..f5e7501 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/long_event_titles.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'Allllllllllllllllll Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Longggggggggggggggg Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeatinggggggggggggggggg Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Eventtttttttttttttttttttttttttttttttttttt', + start: new Date(y, m, d+4, 16, 0), + allDay: false + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + + .fc-event { + color: red; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/manual_gh_3137_popover_scroll.html b/fullcalendar-main/tests/manual/old/manual_gh_3137_popover_scroll.html new file mode 100644 index 0000000..577a98b --- /dev/null +++ b/fullcalendar-main/tests/manual/old/manual_gh_3137_popover_scroll.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + dayMaxEvents: true, + initialDate: '2014-06-12', + editable: true, + events: [ + { + title: 'All Day Event', + start: '2014-07-12' + }, + { + title: 'Long Event', + start: '2014-07-11', + end: '2014-07-13' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-07-05T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-07-12T16:00:00' + }, + { + title: 'Meeting', + start: '2014-07-12T10:30:00', + end: '2014-07-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-07-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-07-12T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + { + title: 'Event', + start: '2014-07-12' + }, + ] + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + + <p style='text-align:center'>try a variety of window sizes. cut off the +more popover.</p> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/manual_gh_3152.html b/fullcalendar-main/tests/manual/old/manual_gh_3152.html new file mode 100644 index 0000000..08d67d9 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/manual_gh_3152.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + initialDate: '2014-06-12', + initialView: 'week', + editable: true, + dayClick: function() { + alert('dayClick'); + } + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + +<p> + On a touch device, begin to scroll on the timeslots.<br /> + If <em>dayClick</em> pops up, the test has <strong>FAILED</strong>. +</p> + +<div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/manual_gh_3160.html b/fullcalendar-main/tests/manual/old/manual_gh_3160.html new file mode 100644 index 0000000..278624c --- /dev/null +++ b/fullcalendar-main/tests/manual/old/manual_gh_3160.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + initialDate: '2014-06-12', + initialView: 'week', + editable: true, + height: 'auto', + events: [ + { + title: 'All Day Event', + start: '2014-06-01' + }, + { + title: 'Long Event', + start: '2014-06-07', + end: '2014-06-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-06-12T10:30:00', + end: '2014-06-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-06-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-06-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-06-28' + } + ] + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + +<p> + Make sure there is limited vertical space so the window scrolls.<br /> + Then touch and hold on an event and begin scrolling. Hold down for 3+ seconds.<br /> + If the event becomes selected and draggable, the test has <strong>FAILED</strong>. +</p> + +<div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/many_events.html b/fullcalendar-main/tests/manual/old/many_events.html new file mode 100644 index 0000000..70b16e1 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/many_events.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + initialDate: '2010-01-01', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: "many_events_json.txt" + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/many_events_json.txt b/fullcalendar-main/tests/manual/old/many_events_json.txt new file mode 100644 index 0000000..cdb72c9 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/many_events_json.txt @@ -0,0 +1 @@ +[{"id":830,"start":1262279460000,"end":1262281260000,"title":"this is a long event isnt that right?","body":"","multi":0,"allDay":false,"extension_id":2},{"id":831,"start":1262282052000,"end":1262283852000,"title":"830","body":"","multi":0,"allDay":false,"extension_id":2},{"id":832,"start":1262284644000,"end":1262286444000,"title":"831","body":"","multi":0,"allDay":false,"extension_id":2},{"id":833,"start":1262287236000,"end":1262289036000,"title":"832","body":"","multi":0,"allDay":false,"extension_id":2},{"id":834,"start":1262289828000,"end":1262291628000,"title":"833","body":"","multi":0,"allDay":false,"extension_id":2},{"id":835,"start":1262292420000,"end":1262294220000,"title":"834","body":"","multi":0,"allDay":false,"extension_id":2},{"id":836,"start":1262295012000,"end":1262296812000,"title":"835","body":"","multi":0,"allDay":false,"extension_id":2},{"id":837,"start":1262297604000,"end":1262299404000,"title":"836","body":"","multi":0,"allDay":false,"extension_id":2},{"id":838,"start":1262300196000,"end":1262301996000,"title":"837","body":"","multi":0,"allDay":false,"extension_id":2},{"id":839,"start":1262302788000,"end":1262304588000,"title":"838","body":"","multi":0,"allDay":false,"extension_id":2},{"id":840,"start":1262305380000,"end":1262307180000,"title":"839","body":"","multi":0,"allDay":false,"extension_id":2},{"id":841,"start":1262307972000,"end":1262309772000,"title":"840","body":"","multi":0,"allDay":false,"extension_id":2},{"id":842,"start":1262310564000,"end":1262312364000,"title":"841","body":"","multi":0,"allDay":false,"extension_id":2},{"id":843,"start":1262313156000,"end":1262314956000,"title":"842","body":"","multi":0,"allDay":false,"extension_id":2},{"id":844,"start":1262315748000,"end":1262317548000,"title":"843","body":"","multi":0,"allDay":false,"extension_id":2},{"id":845,"start":1262318340000,"end":1262320140000,"title":"844","body":"","multi":0,"allDay":false,"extension_id":2},{"id":846,"start":1262320932000,"end":1262322732000,"title":"845","body":"","multi":0,"allDay":false,"extension_id":2},{"id":847,"start":1262323524000,"end":1262325324000,"title":"846","body":"","multi":0,"allDay":false,"extension_id":2},{"id":848,"start":1262326116000,"end":1262327916000,"title":"847","body":"","multi":0,"allDay":false,"extension_id":2},{"id":849,"start":1262328708000,"end":1262330508000,"title":"848","body":"","multi":0,"allDay":false,"extension_id":2},{"id":850,"start":1262331300000,"end":1262333100000,"title":"849","body":"","multi":0,"allDay":false,"extension_id":2},{"id":851,"start":1262333892000,"end":1262335692000,"title":"850","body":"","multi":0,"allDay":false,"extension_id":2},{"id":852,"start":1262336484000,"end":1262338284000,"title":"851","body":"","multi":0,"allDay":false,"extension_id":2},{"id":853,"start":1262339076000,"end":1262340876000,"title":"852","body":"","multi":0,"allDay":false,"extension_id":2},{"id":854,"start":1262341668000,"end":1262343468000,"title":"853","body":"","multi":0,"allDay":false,"extension_id":2},{"id":855,"start":1262344260000,"end":1262346060000,"title":"854","body":"","multi":0,"allDay":false,"extension_id":2},{"id":856,"start":1262346852000,"end":1262348652000,"title":"855","body":"","multi":0,"allDay":false,"extension_id":2},{"id":857,"start":1262349444000,"end":1262351244000,"title":"856","body":"","multi":0,"allDay":false,"extension_id":2},{"id":858,"start":1262352036000,"end":1262353836000,"title":"857","body":"","multi":0,"allDay":false,"extension_id":2},{"id":859,"start":1262354628000,"end":1262356428000,"title":"858","body":"","multi":0,"allDay":false,"extension_id":2},{"id":860,"start":1262357220000,"end":1262359020000,"title":"859","body":"","multi":0,"allDay":false,"extension_id":2},{"id":861,"start":1262359812000,"end":1262361612000,"title":"860","body":"","multi":0,"allDay":false,"extension_id":2},{"id":862,"start":1262362404000,"end":1262364204000,"title":"861","body":"","multi":0,"allDay":false,"extension_id":2},{"id":863,"start":1262364996000,"end":1262366796000,"title":"862","body":"","multi":0,"allDay":false,"extension_id":2},{"id":864,"start":1262367588000,"end":1262369388000,"title":"863","body":"","multi":0,"allDay":false,"extension_id":2},{"id":865,"start":1262370180000,"end":1262371980000,"title":"864","body":"","multi":0,"allDay":false,"extension_id":2},{"id":866,"start":1262372772000,"end":1262374572000,"title":"865","body":"","multi":0,"allDay":false,"extension_id":2},{"id":867,"start":1262375364000,"end":1262377164000,"title":"866","body":"","multi":0,"allDay":false,"extension_id":2},{"id":868,"start":1262377956000,"end":1262379756000,"title":"867","body":"","multi":0,"allDay":false,"extension_id":2},{"id":869,"start":1262380548000,"end":1262382348000,"title":"868","body":"","multi":0,"allDay":false,"extension_id":2},{"id":870,"start":1262383140000,"end":1262384940000,"title":"869","body":"","multi":0,"allDay":false,"extension_id":2},{"id":871,"start":1262385732000,"end":1262387532000,"title":"870","body":"","multi":0,"allDay":false,"extension_id":2},{"id":872,"start":1262388324000,"end":1262390124000,"title":"871","body":"","multi":0,"allDay":false,"extension_id":2},{"id":873,"start":1262390916000,"end":1262392716000,"title":"872","body":"","multi":0,"allDay":false,"extension_id":2},{"id":874,"start":1262393508000,"end":1262395308000,"title":"873","body":"","multi":0,"allDay":false,"extension_id":2},{"id":875,"start":1262396100000,"end":1262397900000,"title":"874","body":"","multi":0,"allDay":false,"extension_id":2},{"id":876,"start":1262398692000,"end":1262400492000,"title":"875","body":"","multi":0,"allDay":false,"extension_id":2},{"id":877,"start":1262401284000,"end":1262403084000,"title":"876","body":"","multi":0,"allDay":false,"extension_id":2},{"id":878,"start":1262403876000,"end":1262405676000,"title":"877","body":"","multi":0,"allDay":false,"extension_id":2},{"id":879,"start":1262406468000,"end":1262408268000,"title":"878","body":"","multi":0,"allDay":false,"extension_id":2},{"id":880,"start":1262409060000,"end":1262410860000,"title":"879","body":"","multi":0,"allDay":false,"extension_id":2},{"id":881,"start":1262411652000,"end":1262413452000,"title":"880","body":"","multi":0,"allDay":false,"extension_id":2},{"id":882,"start":1262414244000,"end":1262416044000,"title":"881","body":"","multi":0,"allDay":false,"extension_id":2},{"id":883,"start":1262416836000,"end":1262418636000,"title":"882","body":"","multi":0,"allDay":false,"extension_id":2},{"id":884,"start":1262419428000,"end":1262421228000,"title":"883","body":"","multi":0,"allDay":false,"extension_id":2},{"id":885,"start":1262422020000,"end":1262423820000,"title":"884","body":"","multi":0,"allDay":false,"extension_id":2},{"id":886,"start":1262424612000,"end":1262426412000,"title":"885","body":"","multi":0,"allDay":false,"extension_id":2},{"id":887,"start":1262427204000,"end":1262429004000,"title":"886","body":"","multi":0,"allDay":false,"extension_id":2},{"id":888,"start":1262429796000,"end":1262431596000,"title":"887","body":"","multi":0,"allDay":false,"extension_id":2},{"id":889,"start":1262432388000,"end":1262434188000,"title":"888","body":"","multi":0,"allDay":false,"extension_id":2},{"id":890,"start":1262434980000,"end":1262436780000,"title":"889","body":"","multi":0,"allDay":false,"extension_id":2},{"id":891,"start":1262437572000,"end":1262439372000,"title":"890","body":"","multi":0,"allDay":false,"extension_id":2},{"id":892,"start":1262440164000,"end":1262441964000,"title":"891","body":"","multi":0,"allDay":false,"extension_id":2},{"id":893,"start":1262442756000,"end":1262444556000,"title":"892","body":"","multi":0,"allDay":false,"extension_id":2},{"id":894,"start":1262445348000,"end":1262447148000,"title":"893","body":"","multi":0,"allDay":false,"extension_id":2},{"id":895,"start":1262447940000,"end":1262449740000,"title":"894","body":"","multi":0,"allDay":false,"extension_id":2},{"id":896,"start":1262450532000,"end":1262452332000,"title":"895","body":"","multi":0,"allDay":false,"extension_id":2},{"id":897,"start":1262453124000,"end":1262454924000,"title":"896","body":"","multi":0,"allDay":false,"extension_id":2},{"id":898,"start":1262455716000,"end":1262457516000,"title":"897","body":"","multi":0,"allDay":false,"extension_id":2},{"id":899,"start":1262458308000,"end":1262460108000,"title":"898","body":"","multi":0,"allDay":false,"extension_id":2},{"id":900,"start":1262460900000,"end":1262462700000,"title":"899","body":"","multi":0,"allDay":false,"extension_id":2},{"id":901,"start":1262463492000,"end":1262465292000,"title":"900","body":"","multi":0,"allDay":false,"extension_id":2},{"id":902,"start":1262466084000,"end":1262467884000,"title":"901","body":"","multi":0,"allDay":false,"extension_id":2},{"id":903,"start":1262468676000,"end":1262470476000,"title":"902","body":"","multi":0,"allDay":false,"extension_id":2},{"id":904,"start":1262471268000,"end":1262473068000,"title":"903","body":"","multi":0,"allDay":false,"extension_id":2},{"id":905,"start":1262473860000,"end":1262475660000,"title":"904","body":"","multi":0,"allDay":false,"extension_id":2},{"id":906,"start":1262476452000,"end":1262478252000,"title":"905","body":"","multi":0,"allDay":false,"extension_id":2},{"id":907,"start":1262479044000,"end":1262480844000,"title":"906","body":"","multi":0,"allDay":false,"extension_id":2},{"id":908,"start":1262481636000,"end":1262483436000,"title":"907","body":"","multi":0,"allDay":false,"extension_id":2},{"id":909,"start":1262484228000,"end":1262486028000,"title":"908","body":"","multi":0,"allDay":false,"extension_id":2},{"id":910,"start":1262486820000,"end":1262488620000,"title":"909","body":"","multi":0,"allDay":false,"extension_id":2},{"id":911,"start":1262489412000,"end":1262491212000,"title":"910","body":"","multi":0,"allDay":false,"extension_id":2},{"id":912,"start":1262492004000,"end":1262493804000,"title":"911","body":"","multi":0,"allDay":false,"extension_id":2},{"id":913,"start":1262494596000,"end":1262496396000,"title":"912","body":"","multi":0,"allDay":false,"extension_id":2},{"id":914,"start":1262497188000,"end":1262498988000,"title":"913","body":"","multi":0,"allDay":false,"extension_id":2},{"id":915,"start":1262499780000,"end":1262501580000,"title":"914","body":"","multi":0,"allDay":false,"extension_id":2},{"id":916,"start":1262502372000,"end":1262504172000,"title":"915","body":"","multi":0,"allDay":false,"extension_id":2},{"id":917,"start":1262504964000,"end":1262506764000,"title":"916","body":"","multi":0,"allDay":false,"extension_id":2},{"id":918,"start":1262507556000,"end":1262509356000,"title":"917","body":"","multi":0,"allDay":false,"extension_id":2},{"id":919,"start":1262510148000,"end":1262511948000,"title":"918","body":"","multi":0,"allDay":false,"extension_id":2},{"id":920,"start":1262512740000,"end":1262514540000,"title":"919","body":"","multi":0,"allDay":false,"extension_id":2},{"id":921,"start":1262515332000,"end":1262517132000,"title":"920","body":"","multi":0,"allDay":false,"extension_id":2},{"id":922,"start":1262517924000,"end":1262519724000,"title":"921","body":"","multi":0,"allDay":false,"extension_id":2},{"id":923,"start":1262520516000,"end":1262522316000,"title":"922","body":"","multi":0,"allDay":false,"extension_id":2},{"id":924,"start":1262523108000,"end":1262524908000,"title":"923","body":"","multi":0,"allDay":false,"extension_id":2},{"id":925,"start":1262525700000,"end":1262527500000,"title":"924","body":"","multi":0,"allDay":false,"extension_id":2},{"id":926,"start":1262528292000,"end":1262530092000,"title":"925","body":"","multi":0,"allDay":false,"extension_id":2},{"id":927,"start":1262530884000,"end":1262532684000,"title":"926","body":"","multi":0,"allDay":false,"extension_id":2},{"id":928,"start":1262533476000,"end":1262535276000,"title":"927","body":"","multi":0,"allDay":false,"extension_id":2},{"id":929,"start":1262536068000,"end":1262537868000,"title":"928","body":"","multi":0,"allDay":false,"extension_id":2},{"id":930,"start":1262538660000,"end":1262540460000,"title":"929","body":"","multi":0,"allDay":false,"extension_id":2},{"id":931,"start":1262541252000,"end":1262543052000,"title":"930","body":"","multi":0,"allDay":false,"extension_id":2},{"id":932,"start":1262543844000,"end":1262545644000,"title":"931","body":"","multi":0,"allDay":false,"extension_id":2},{"id":933,"start":1262546436000,"end":1262548236000,"title":"932","body":"","multi":0,"allDay":false,"extension_id":2},{"id":934,"start":1262549028000,"end":1262550828000,"title":"933","body":"","multi":0,"allDay":false,"extension_id":2},{"id":935,"start":1262551620000,"end":1262553420000,"title":"934","body":"","multi":0,"allDay":false,"extension_id":2},{"id":936,"start":1262554212000,"end":1262556012000,"title":"935","body":"","multi":0,"allDay":false,"extension_id":2},{"id":937,"start":1262556804000,"end":1262558604000,"title":"936","body":"","multi":0,"allDay":false,"extension_id":2},{"id":938,"start":1262559396000,"end":1262561196000,"title":"937","body":"","multi":0,"allDay":false,"extension_id":2},{"id":939,"start":1262561988000,"end":1262563788000,"title":"938","body":"","multi":0,"allDay":false,"extension_id":2},{"id":940,"start":1262564580000,"end":1262566380000,"title":"939","body":"","multi":0,"allDay":false,"extension_id":2},{"id":941,"start":1262567172000,"end":1262568972000,"title":"940","body":"","multi":0,"allDay":false,"extension_id":2},{"id":942,"start":1262569764000,"end":1262571564000,"title":"941","body":"","multi":0,"allDay":false,"extension_id":2},{"id":943,"start":1262572356000,"end":1262574156000,"title":"942","body":"","multi":0,"allDay":false,"extension_id":2},{"id":944,"start":1262574948000,"end":1262576748000,"title":"943","body":"","multi":0,"allDay":false,"extension_id":2},{"id":945,"start":1262577540000,"end":1262579340000,"title":"944","body":"","multi":0,"allDay":false,"extension_id":2},{"id":946,"start":1262580132000,"end":1262581932000,"title":"945","body":"","multi":0,"allDay":false,"extension_id":2},{"id":947,"start":1262582724000,"end":1262584524000,"title":"946","body":"","multi":0,"allDay":false,"extension_id":2},{"id":948,"start":1262585316000,"end":1262587116000,"title":"947","body":"","multi":0,"allDay":false,"extension_id":2},{"id":949,"start":1262587908000,"end":1262589708000,"title":"948","body":"","multi":0,"allDay":false,"extension_id":2},{"id":950,"start":1262590500000,"end":1262592300000,"title":"949","body":"","multi":0,"allDay":false,"extension_id":2},{"id":951,"start":1262593092000,"end":1262594892000,"title":"950","body":"","multi":0,"allDay":false,"extension_id":2},{"id":952,"start":1262595684000,"end":1262597484000,"title":"951","body":"","multi":0,"allDay":false,"extension_id":2},{"id":953,"start":1262598276000,"end":1262600076000,"title":"952","body":"","multi":0,"allDay":false,"extension_id":2},{"id":954,"start":1262600868000,"end":1262602668000,"title":"953","body":"","multi":0,"allDay":false,"extension_id":2},{"id":955,"start":1262603460000,"end":1262605260000,"title":"954","body":"","multi":0,"allDay":false,"extension_id":2},{"id":956,"start":1262606052000,"end":1262607852000,"title":"955","body":"","multi":0,"allDay":false,"extension_id":2},{"id":957,"start":1262608644000,"end":1262610444000,"title":"956","body":"","multi":0,"allDay":false,"extension_id":2},{"id":958,"start":1262611236000,"end":1262613036000,"title":"957","body":"","multi":0,"allDay":false,"extension_id":2},{"id":959,"start":1262613828000,"end":1262615628000,"title":"958","body":"","multi":0,"allDay":false,"extension_id":2},{"id":960,"start":1262616420000,"end":1262618220000,"title":"959","body":"","multi":0,"allDay":false,"extension_id":2},{"id":961,"start":1262619012000,"end":1262620812000,"title":"960","body":"","multi":0,"allDay":false,"extension_id":2},{"id":962,"start":1262621604000,"end":1262623404000,"title":"961","body":"","multi":0,"allDay":false,"extension_id":2},{"id":963,"start":1262624196000,"end":1262625996000,"title":"962","body":"","multi":0,"allDay":false,"extension_id":2},{"id":964,"start":1262626788000,"end":1262628588000,"title":"963","body":"","multi":0,"allDay":false,"extension_id":2},{"id":965,"start":1262629380000,"end":1262631180000,"title":"964","body":"","multi":0,"allDay":false,"extension_id":2},{"id":966,"start":1262631972000,"end":1262633772000,"title":"965","body":"","multi":0,"allDay":false,"extension_id":2},{"id":967,"start":1262634564000,"end":1262636364000,"title":"966","body":"","multi":0,"allDay":false,"extension_id":2},{"id":968,"start":1262637156000,"end":1262638956000,"title":"967","body":"","multi":0,"allDay":false,"extension_id":2},{"id":969,"start":1262639748000,"end":1262641548000,"title":"968","body":"","multi":0,"allDay":false,"extension_id":2},{"id":970,"start":1262642340000,"end":1262644140000,"title":"969","body":"","multi":0,"allDay":false,"extension_id":2},{"id":971,"start":1262644932000,"end":1262646732000,"title":"970","body":"","multi":0,"allDay":false,"extension_id":2},{"id":972,"start":1262647524000,"end":1262649324000,"title":"971","body":"","multi":0,"allDay":false,"extension_id":2},{"id":973,"start":1262650116000,"end":1262651916000,"title":"972","body":"","multi":0,"allDay":false,"extension_id":2},{"id":974,"start":1262652708000,"end":1262654508000,"title":"973","body":"","multi":0,"allDay":false,"extension_id":2},{"id":975,"start":1262655300000,"end":1262657100000,"title":"974","body":"","multi":0,"allDay":false,"extension_id":2},{"id":976,"start":1262657892000,"end":1262659692000,"title":"975","body":"","multi":0,"allDay":false,"extension_id":2},{"id":977,"start":1262660484000,"end":1262662284000,"title":"976","body":"","multi":0,"allDay":false,"extension_id":2},{"id":978,"start":1262663076000,"end":1262664876000,"title":"977","body":"","multi":0,"allDay":false,"extension_id":2},{"id":979,"start":1262665668000,"end":1262667468000,"title":"978","body":"","multi":0,"allDay":false,"extension_id":2},{"id":980,"start":1262668260000,"end":1262670060000,"title":"979","body":"","multi":0,"allDay":false,"extension_id":2},{"id":981,"start":1262670852000,"end":1262672652000,"title":"980","body":"","multi":0,"allDay":false,"extension_id":2},{"id":982,"start":1262673444000,"end":1262675244000,"title":"981","body":"","multi":0,"allDay":false,"extension_id":2},{"id":983,"start":1262676036000,"end":1262677836000,"title":"982","body":"","multi":0,"allDay":false,"extension_id":2},{"id":984,"start":1262678628000,"end":1262680428000,"title":"983","body":"","multi":0,"allDay":false,"extension_id":2},{"id":985,"start":1262681220000,"end":1262683020000,"title":"984","body":"","multi":0,"allDay":false,"extension_id":2},{"id":986,"start":1262683812000,"end":1262685612000,"title":"985","body":"","multi":0,"allDay":false,"extension_id":2},{"id":987,"start":1262686404000,"end":1262688204000,"title":"986","body":"","multi":0,"allDay":false,"extension_id":2},{"id":988,"start":1262688996000,"end":1262690796000,"title":"987","body":"","multi":0,"allDay":false,"extension_id":2},{"id":989,"start":1262691588000,"end":1262693388000,"title":"988","body":"","multi":0,"allDay":false,"extension_id":2},{"id":990,"start":1262694180000,"end":1262695980000,"title":"989","body":"","multi":0,"allDay":false,"extension_id":2},{"id":991,"start":1262696772000,"end":1262698572000,"title":"990","body":"","multi":0,"allDay":false,"extension_id":2},{"id":992,"start":1262699364000,"end":1262701164000,"title":"991","body":"","multi":0,"allDay":false,"extension_id":2},{"id":993,"start":1262701956000,"end":1262703756000,"title":"992","body":"","multi":0,"allDay":false,"extension_id":2},{"id":994,"start":1262704548000,"end":1262706348000,"title":"993","body":"","multi":0,"allDay":false,"extension_id":2},{"id":995,"start":1262707140000,"end":1262708940000,"title":"994","body":"","multi":0,"allDay":false,"extension_id":2},{"id":996,"start":1262709732000,"end":1262711532000,"title":"995","body":"","multi":0,"allDay":false,"extension_id":2},{"id":997,"start":1262712324000,"end":1262714124000,"title":"996","body":"","multi":0,"allDay":false,"extension_id":2},{"id":998,"start":1262714916000,"end":1262716716000,"title":"997","body":"","multi":0,"allDay":false,"extension_id":2},{"id":999,"start":1262717508000,"end":1262719308000,"title":"998","body":"","multi":0,"allDay":false,"extension_id":2},{"id":1000,"start":1262720100000,"end":1262721900000,"title":"999","body":"","multi":0,"allDay":false,"extension_id":2},{"id":1,"start":1264425660000,"end":1264427460000,"title":"0","body":"","multi":0,"allDay":false,"extension_id":2},{"id":2,"start":1264428252000,"end":1264430052000,"title":"1","body":"","multi":0,"allDay":false,"extension_id":2},{"id":3,"start":1264430844000,"end":1264432644000,"title":"2","body":"","multi":0,"allDay":false,"extension_id":2},{"id":4,"start":1264433436000,"end":1264435236000,"title":"3","body":"","multi":0,"allDay":false,"extension_id":2},{"id":5,"start":1264436028000,"end":1264437828000,"title":"4","body":"","multi":0,"allDay":false,"extension_id":2},{"id":6,"start":1264438620000,"end":1264440420000,"title":"5","body":"","multi":0,"allDay":false,"extension_id":2},{"id":7,"start":1264441212000,"end":1264443012000,"title":"6","body":"","multi":0,"allDay":false,"extension_id":2},{"id":8,"start":1264443804000,"end":1264445604000,"title":"7","body":"","multi":0,"allDay":false,"extension_id":2},{"id":9,"start":1264446396000,"end":1264448196000,"title":"8","body":"","multi":0,"allDay":false,"extension_id":2},{"id":10,"start":1264448988000,"end":1264450788000,"title":"9","body":"","multi":0,"allDay":false,"extension_id":2},{"id":11,"start":1264451580000,"end":1264453380000,"title":"10","body":"","multi":0,"allDay":false,"extension_id":2},{"id":12,"start":1264454172000,"end":1264455972000,"title":"11","body":"","multi":0,"allDay":false,"extension_id":2},{"id":13,"start":1264456764000,"end":1264458564000,"title":"12","body":"","multi":0,"allDay":false,"extension_id":2},{"id":14,"start":1264459356000,"end":1264461156000,"title":"13","body":"","multi":0,"allDay":false,"extension_id":2},{"id":15,"start":1264461948000,"end":1264463748000,"title":"14","body":"","multi":0,"allDay":false,"extension_id":2},{"id":16,"start":1264464540000,"end":1264466340000,"title":"15","body":"","multi":0,"allDay":false,"extension_id":2},{"id":17,"start":1264467132000,"end":1264468932000,"title":"16","body":"","multi":0,"allDay":false,"extension_id":2},{"id":18,"start":1264469724000,"end":1264471524000,"title":"17","body":"","multi":0,"allDay":false,"extension_id":2},{"id":19,"start":1264472316000,"end":1264474116000,"title":"18","body":"","multi":0,"allDay":false,"extension_id":2},{"id":20,"start":1264474908000,"end":1264476708000,"title":"19","body":"","multi":0,"allDay":false,"extension_id":2},{"id":21,"start":1264477500000,"end":1264479300000,"title":"20","body":"","multi":0,"allDay":false,"extension_id":2},{"id":22,"start":1264480092000,"end":1264481892000,"title":"21","body":"","multi":0,"allDay":false,"extension_id":2},{"id":23,"start":1264482684000,"end":1264484484000,"title":"22","body":"","multi":0,"allDay":false,"extension_id":2},{"id":24,"start":1264485276000,"end":1264487076000,"title":"23","body":"","multi":0,"allDay":false,"extension_id":2},{"id":25,"start":1264487868000,"end":1264489668000,"title":"24","body":"","multi":0,"allDay":false,"extension_id":2},{"id":26,"start":1264490460000,"end":1264492260000,"title":"25","body":"","multi":0,"allDay":false,"extension_id":2},{"id":27,"start":1264493052000,"end":1264494852000,"title":"26","body":"","multi":0,"allDay":false,"extension_id":2},{"id":28,"start":1264495644000,"end":1264497444000,"title":"27","body":"","multi":0,"allDay":false,"extension_id":2},{"id":29,"start":1264498236000,"end":1264500036000,"title":"28","body":"","multi":0,"allDay":false,"extension_id":2},{"id":30,"start":1264500828000,"end":1264502628000,"title":"29","body":"","multi":0,"allDay":false,"extension_id":2},{"id":31,"start":1264503420000,"end":1264505220000,"title":"30","body":"","multi":0,"allDay":false,"extension_id":2},{"id":32,"start":1264506012000,"end":1264507812000,"title":"31","body":"","multi":0,"allDay":false,"extension_id":2},{"id":33,"start":1264508604000,"end":1264510404000,"title":"32","body":"","multi":0,"allDay":false,"extension_id":2},{"id":34,"start":1264511196000,"end":1264512996000,"title":"33","body":"","multi":0,"allDay":false,"extension_id":2},{"id":35,"start":1264513788000,"end":1264515588000,"title":"34","body":"","multi":0,"allDay":false,"extension_id":2},{"id":36,"start":1264516380000,"end":1264518180000,"title":"35","body":"","multi":0,"allDay":false,"extension_id":2},{"id":37,"start":1264518972000,"end":1264520772000,"title":"36","body":"","multi":0,"allDay":false,"extension_id":2},{"id":38,"start":1264521564000,"end":1264523364000,"title":"37","body":"","multi":0,"allDay":false,"extension_id":2},{"id":39,"start":1264524156000,"end":1264525956000,"title":"38","body":"","multi":0,"allDay":false,"extension_id":2},{"id":40,"start":1264526748000,"end":1264528548000,"title":"39","body":"","multi":0,"allDay":false,"extension_id":2},{"id":41,"start":1264529340000,"end":1264531140000,"title":"40","body":"","multi":0,"allDay":false,"extension_id":2},{"id":42,"start":1264531932000,"end":1264533732000,"title":"41","body":"","multi":0,"allDay":false,"extension_id":2},{"id":43,"start":1264534524000,"end":1264536324000,"title":"42","body":"","multi":0,"allDay":false,"extension_id":2},{"id":44,"start":1264537116000,"end":1264538916000,"title":"43","body":"","multi":0,"allDay":false,"extension_id":2},{"id":45,"start":1264539708000,"end":1264541508000,"title":"44","body":"","multi":0,"allDay":false,"extension_id":2},{"id":46,"start":1264542300000,"end":1264544100000,"title":"45","body":"","multi":0,"allDay":false,"extension_id":2},{"id":47,"start":1264544892000,"end":1264546692000,"title":"46","body":"","multi":0,"allDay":false,"extension_id":2},{"id":48,"start":1264547484000,"end":1264549284000,"title":"47","body":"","multi":0,"allDay":false,"extension_id":2},{"id":49,"start":1264550076000,"end":1264551876000,"title":"48","body":"","multi":0,"allDay":false,"extension_id":2},{"id":50,"start":1264552668000,"end":1264554468000,"title":"49","body":"","multi":0,"allDay":false,"extension_id":2},{"id":51,"start":1264555260000,"end":1264557060000,"title":"50","body":"","multi":0,"allDay":false,"extension_id":2},{"id":52,"start":1264557852000,"end":1264559652000,"title":"51","body":"","multi":0,"allDay":false,"extension_id":2},{"id":53,"start":1264560444000,"end":1264562244000,"title":"52","body":"","multi":0,"allDay":false,"extension_id":2},{"id":54,"start":1264563036000,"end":1264564836000,"title":"53","body":"","multi":0,"allDay":false,"extension_id":2},{"id":55,"start":1264565628000,"end":1264567428000,"title":"54","body":"","multi":0,"allDay":false,"extension_id":2},{"id":56,"start":1264568220000,"end":1264570020000,"title":"55","body":"","multi":0,"allDay":false,"extension_id":2},{"id":57,"start":1264570812000,"end":1264572612000,"title":"56","body":"","multi":0,"allDay":false,"extension_id":2},{"id":58,"start":1264573404000,"end":1264575204000,"title":"57","body":"","multi":0,"allDay":false,"extension_id":2},{"id":59,"start":1264575996000,"end":1264577796000,"title":"58","body":"","multi":0,"allDay":false,"extension_id":2},{"id":60,"start":1264578588000,"end":1264580388000,"title":"59","body":"","multi":0,"allDay":false,"extension_id":2},{"id":61,"start":1264581180000,"end":1264582980000,"title":"60","body":"","multi":0,"allDay":false,"extension_id":2},{"id":62,"start":1264583772000,"end":1264585572000,"title":"61","body":"","multi":0,"allDay":false,"extension_id":2},{"id":63,"start":1264586364000,"end":1264588164000,"title":"62","body":"","multi":0,"allDay":false,"extension_id":2},{"id":64,"start":1264588956000,"end":1264590756000,"title":"63","body":"","multi":0,"allDay":false,"extension_id":2},{"id":65,"start":1264591548000,"end":1264593348000,"title":"64","body":"","multi":0,"allDay":false,"extension_id":2},{"id":66,"start":1264594140000,"end":1264595940000,"title":"65","body":"","multi":0,"allDay":false,"extension_id":2},{"id":67,"start":1264596732000,"end":1264598532000,"title":"66","body":"","multi":0,"allDay":false,"extension_id":2},{"id":68,"start":1264599324000,"end":1264601124000,"title":"67","body":"","multi":0,"allDay":false,"extension_id":2},{"id":69,"start":1264601916000,"end":1264603716000,"title":"68","body":"","multi":0,"allDay":false,"extension_id":2},{"id":70,"start":1264604508000,"end":1264606308000,"title":"69","body":"","multi":0,"allDay":false,"extension_id":2},{"id":71,"start":1264607100000,"end":1264608900000,"title":"70","body":"","multi":0,"allDay":false,"extension_id":2},{"id":72,"start":1264609692000,"end":1264611492000,"title":"71","body":"","multi":0,"allDay":false,"extension_id":2},{"id":73,"start":1264612284000,"end":1264614084000,"title":"72","body":"","multi":0,"allDay":false,"extension_id":2},{"id":74,"start":1264614876000,"end":1264616676000,"title":"73","body":"","multi":0,"allDay":false,"extension_id":2},{"id":75,"start":1264617468000,"end":1264619268000,"title":"74","body":"","multi":0,"allDay":false,"extension_id":2},{"id":76,"start":1264620060000,"end":1264621860000,"title":"75","body":"","multi":0,"allDay":false,"extension_id":2},{"id":77,"start":1264622652000,"end":1264624452000,"title":"76","body":"","multi":0,"allDay":false,"extension_id":2},{"id":78,"start":1264625244000,"end":1264627044000,"title":"77","body":"","multi":0,"allDay":false,"extension_id":2},{"id":79,"start":1264627836000,"end":1264629636000,"title":"78","body":"","multi":0,"allDay":false,"extension_id":2},{"id":80,"start":1264630428000,"end":1264632228000,"title":"79","body":"","multi":0,"allDay":false,"extension_id":2},{"id":81,"start":1264633020000,"end":1264634820000,"title":"80","body":"","multi":0,"allDay":false,"extension_id":2},{"id":82,"start":1264635612000,"end":1264637412000,"title":"81","body":"","multi":0,"allDay":false,"extension_id":2},{"id":83,"start":1264638204000,"end":1264640004000,"title":"82","body":"","multi":0,"allDay":false,"extension_id":2},{"id":84,"start":1264640796000,"end":1264642596000,"title":"83","body":"","multi":0,"allDay":false,"extension_id":2},{"id":85,"start":1264643388000,"end":1264645188000,"title":"84","body":"","multi":0,"allDay":false,"extension_id":2},{"id":86,"start":1264645980000,"end":1264647780000,"title":"85","body":"","multi":0,"allDay":false,"extension_id":2},{"id":87,"start":1264648572000,"end":1264650372000,"title":"86","body":"","multi":0,"allDay":false,"extension_id":2},{"id":88,"start":1264651164000,"end":1264652964000,"title":"87","body":"","multi":0,"allDay":false,"extension_id":2},{"id":89,"start":1264653756000,"end":1264655556000,"title":"88","body":"","multi":0,"allDay":false,"extension_id":2},{"id":90,"start":1264656348000,"end":1264658148000,"title":"89","body":"","multi":0,"allDay":false,"extension_id":2},{"id":91,"start":1264658940000,"end":1264660740000,"title":"90","body":"","multi":0,"allDay":false,"extension_id":2},{"id":92,"start":1264661532000,"end":1264663332000,"title":"91","body":"","multi":0,"allDay":false,"extension_id":2},{"id":93,"start":1264664124000,"end":1264665924000,"title":"92","body":"","multi":0,"allDay":false,"extension_id":2},{"id":94,"start":1264666716000,"end":1264668516000,"title":"93","body":"","multi":0,"allDay":false,"extension_id":2},{"id":95,"start":1264669308000,"end":1264671108000,"title":"94","body":"","multi":0,"allDay":false,"extension_id":2},{"id":96,"start":1264671900000,"end":1264673700000,"title":"95","body":"","multi":0,"allDay":false,"extension_id":2},{"id":97,"start":1264674492000,"end":1264676292000,"title":"96","body":"","multi":0,"allDay":false,"extension_id":2},{"id":98,"start":1264677084000,"end":1264678884000,"title":"97","body":"","multi":0,"allDay":false,"extension_id":2},{"id":99,"start":1264679676000,"end":1264681476000,"title":"98","body":"","multi":0,"allDay":false,"extension_id":2},{"id":100,"start":1264682268000,"end":1264684068000,"title":"99","body":"","multi":0,"allDay":false,"extension_id":2},{"id":101,"start":1264684860000,"end":1264686660000,"title":"100","body":"","multi":0,"allDay":false,"extension_id":2},{"id":102,"start":1264687452000,"end":1264689252000,"title":"101","body":"","multi":0,"allDay":false,"extension_id":2},{"id":103,"start":1264690044000,"end":1264691844000,"title":"102","body":"","multi":0,"allDay":false,"extension_id":2},{"id":104,"start":1264692636000,"end":1264694436000,"title":"103","body":"","multi":0,"allDay":false,"extension_id":2},{"id":105,"start":1264695228000,"end":1264697028000,"title":"104","body":"","multi":0,"allDay":false,"extension_id":2},{"id":106,"start":1264697820000,"end":1264699620000,"title":"105","body":"","multi":0,"allDay":false,"extension_id":2},{"id":107,"start":1264700412000,"end":1264702212000,"title":"106","body":"","multi":0,"allDay":false,"extension_id":2},{"id":108,"start":1264703004000,"end":1264704804000,"title":"107","body":"","multi":0,"allDay":false,"extension_id":2},{"id":109,"start":1264705596000,"end":1264707396000,"title":"108","body":"","multi":0,"allDay":false,"extension_id":2},{"id":110,"start":1264708188000,"end":1264709988000,"title":"109","body":"","multi":0,"allDay":false,"extension_id":2},{"id":111,"start":1264710780000,"end":1264712580000,"title":"110","body":"","multi":0,"allDay":false,"extension_id":2},{"id":112,"start":1264713372000,"end":1264715172000,"title":"111","body":"","multi":0,"allDay":false,"extension_id":2},{"id":113,"start":1264715964000,"end":1264717764000,"title":"112","body":"","multi":0,"allDay":false,"extension_id":2},{"id":114,"start":1264718556000,"end":1264720356000,"title":"113","body":"","multi":0,"allDay":false,"extension_id":2},{"id":115,"start":1264721148000,"end":1264722948000,"title":"114","body":"","multi":0,"allDay":false,"extension_id":2},{"id":116,"start":1264723740000,"end":1264725540000,"title":"115","body":"","multi":0,"allDay":false,"extension_id":2},{"id":117,"start":1264726332000,"end":1264728132000,"title":"116","body":"","multi":0,"allDay":false,"extension_id":2},{"id":118,"start":1264728924000,"end":1264730724000,"title":"117","body":"","multi":0,"allDay":false,"extension_id":2},{"id":119,"start":1264731516000,"end":1264733316000,"title":"118","body":"","multi":0,"allDay":false,"extension_id":2},{"id":120,"start":1264734108000,"end":1264735908000,"title":"119","body":"","multi":0,"allDay":false,"extension_id":2},{"id":121,"start":1264736700000,"end":1264738500000,"title":"120","body":"","multi":0,"allDay":false,"extension_id":2},{"id":122,"start":1264739292000,"end":1264741092000,"title":"121","body":"","multi":0,"allDay":false,"extension_id":2},{"id":123,"start":1264741884000,"end":1264743684000,"title":"122","body":"","multi":0,"allDay":false,"extension_id":2},{"id":124,"start":1264744476000,"end":1264746276000,"title":"123","body":"","multi":0,"allDay":false,"extension_id":2},{"id":125,"start":1264747068000,"end":1264748868000,"title":"124","body":"","multi":0,"allDay":false,"extension_id":2},{"id":126,"start":1264749660000,"end":1264751460000,"title":"125","body":"","multi":0,"allDay":false,"extension_id":2},{"id":127,"start":1264752252000,"end":1264754052000,"title":"126","body":"","multi":0,"allDay":false,"extension_id":2},{"id":128,"start":1264754844000,"end":1264756644000,"title":"127","body":"","multi":0,"allDay":false,"extension_id":2},{"id":129,"start":1264757436000,"end":1264759236000,"title":"128","body":"","multi":0,"allDay":false,"extension_id":2},{"id":130,"start":1264760028000,"end":1264761828000,"title":"129","body":"","multi":0,"allDay":false,"extension_id":2},{"id":131,"start":1264762620000,"end":1264764420000,"title":"130","body":"","multi":0,"allDay":false,"extension_id":2},{"id":132,"start":1264765212000,"end":1264767012000,"title":"131","body":"","multi":0,"allDay":false,"extension_id":2},{"id":133,"start":1264767804000,"end":1264769604000,"title":"132","body":"","multi":0,"allDay":false,"extension_id":2},{"id":134,"start":1264770396000,"end":1264772196000,"title":"133","body":"","multi":0,"allDay":false,"extension_id":2},{"id":135,"start":1264772988000,"end":1264774788000,"title":"134","body":"","multi":0,"allDay":false,"extension_id":2},{"id":136,"start":1264775580000,"end":1264777380000,"title":"135","body":"","multi":0,"allDay":false,"extension_id":2},{"id":137,"start":1264778172000,"end":1264779972000,"title":"136","body":"","multi":0,"allDay":false,"extension_id":2},{"id":138,"start":1264780764000,"end":1264782564000,"title":"137","body":"","multi":0,"allDay":false,"extension_id":2},{"id":139,"start":1264783356000,"end":1264785156000,"title":"138","body":"","multi":0,"allDay":false,"extension_id":2},{"id":140,"start":1264785948000,"end":1264787748000,"title":"139","body":"","multi":0,"allDay":false,"extension_id":2},{"id":141,"start":1264788540000,"end":1264790340000,"title":"140","body":"","multi":0,"allDay":false,"extension_id":2},{"id":142,"start":1264791132000,"end":1264792932000,"title":"141","body":"","multi":0,"allDay":false,"extension_id":2},{"id":143,"start":1264793724000,"end":1264795524000,"title":"142","body":"","multi":0,"allDay":false,"extension_id":2},{"id":144,"start":1264796316000,"end":1264798116000,"title":"143","body":"","multi":0,"allDay":false,"extension_id":2},{"id":145,"start":1264798908000,"end":1264800708000,"title":"144","body":"","multi":0,"allDay":false,"extension_id":2},{"id":146,"start":1264801500000,"end":1264803300000,"title":"145","body":"","multi":0,"allDay":false,"extension_id":2},{"id":147,"start":1264804092000,"end":1264805892000,"title":"146","body":"","multi":0,"allDay":false,"extension_id":2},{"id":148,"start":1264806684000,"end":1264808484000,"title":"147","body":"","multi":0,"allDay":false,"extension_id":2},{"id":149,"start":1264809276000,"end":1264811076000,"title":"148","body":"","multi":0,"allDay":false,"extension_id":2},{"id":150,"start":1264811868000,"end":1264813668000,"title":"149","body":"","multi":0,"allDay":false,"extension_id":2},{"id":151,"start":1264814460000,"end":1264816260000,"title":"150","body":"","multi":0,"allDay":false,"extension_id":2},{"id":152,"start":1264817052000,"end":1264818852000,"title":"151","body":"","multi":0,"allDay":false,"extension_id":2},{"id":153,"start":1264819644000,"end":1264821444000,"title":"152","body":"","multi":0,"allDay":false,"extension_id":2},{"id":154,"start":1264822236000,"end":1264824036000,"title":"153","body":"","multi":0,"allDay":false,"extension_id":2},{"id":155,"start":1264824828000,"end":1264826628000,"title":"154","body":"","multi":0,"allDay":false,"extension_id":2},{"id":156,"start":1264827420000,"end":1264829220000,"title":"155","body":"","multi":0,"allDay":false,"extension_id":2},{"id":157,"start":1264830012000,"end":1264831812000,"title":"156","body":"","multi":0,"allDay":false,"extension_id":2},{"id":158,"start":1264832604000,"end":1264834404000,"title":"157","body":"","multi":0,"allDay":false,"extension_id":2},{"id":159,"start":1264835196000,"end":1264836996000,"title":"158","body":"","multi":0,"allDay":false,"extension_id":2},{"id":160,"start":1264837788000,"end":1264839588000,"title":"159","body":"","multi":0,"allDay":false,"extension_id":2},{"id":161,"start":1264840380000,"end":1264842180000,"title":"160","body":"","multi":0,"allDay":false,"extension_id":2},{"id":162,"start":1264842972000,"end":1264844772000,"title":"161","body":"","multi":0,"allDay":false,"extension_id":2},{"id":163,"start":1264845564000,"end":1264847364000,"title":"162","body":"","multi":0,"allDay":false,"extension_id":2},{"id":164,"start":1264848156000,"end":1264849956000,"title":"163","body":"","multi":0,"allDay":false,"extension_id":2},{"id":165,"start":1264850748000,"end":1264852548000,"title":"164","body":"","multi":0,"allDay":false,"extension_id":2},{"id":166,"start":1264853340000,"end":1264855140000,"title":"165","body":"","multi":0,"allDay":false,"extension_id":2},{"id":167,"start":1264855932000,"end":1264857732000,"title":"166","body":"","multi":0,"allDay":false,"extension_id":2},{"id":168,"start":1264858524000,"end":1264860324000,"title":"167","body":"","multi":0,"allDay":false,"extension_id":2},{"id":169,"start":1264861116000,"end":1264862916000,"title":"168","body":"","multi":0,"allDay":false,"extension_id":2},{"id":170,"start":1264863708000,"end":1264865508000,"title":"169","body":"","multi":0,"allDay":false,"extension_id":2},{"id":171,"start":1264866300000,"end":1264868100000,"title":"170","body":"","multi":0,"allDay":false,"extension_id":2},{"id":172,"start":1264868892000,"end":1264870692000,"title":"171","body":"","multi":0,"allDay":false,"extension_id":2},{"id":173,"start":1264871484000,"end":1264873284000,"title":"172","body":"","multi":0,"allDay":false,"extension_id":2},{"id":174,"start":1264874076000,"end":1264875876000,"title":"173","body":"","multi":0,"allDay":false,"extension_id":2},{"id":175,"start":1264876668000,"end":1264878468000,"title":"174","body":"","multi":0,"allDay":false,"extension_id":2},{"id":176,"start":1264879260000,"end":1264881060000,"title":"175","body":"","multi":0,"allDay":false,"extension_id":2},{"id":177,"start":1264881852000,"end":1264883652000,"title":"176","body":"","multi":0,"allDay":false,"extension_id":2},{"id":178,"start":1264884444000,"end":1264886244000,"title":"177","body":"","multi":0,"allDay":false,"extension_id":2},{"id":179,"start":1264887036000,"end":1264888836000,"title":"178","body":"","multi":0,"allDay":false,"extension_id":2},{"id":180,"start":1264889628000,"end":1264891428000,"title":"179","body":"","multi":0,"allDay":false,"extension_id":2},{"id":181,"start":1264892220000,"end":1264894020000,"title":"180","body":"","multi":0,"allDay":false,"extension_id":2},{"id":182,"start":1264894812000,"end":1264896612000,"title":"181","body":"","multi":0,"allDay":false,"extension_id":2},{"id":183,"start":1264897404000,"end":1264899204000,"title":"182","body":"","multi":0,"allDay":false,"extension_id":2},{"id":184,"start":1264899996000,"end":1264901796000,"title":"183","body":"","multi":0,"allDay":false,"extension_id":2},{"id":185,"start":1264902588000,"end":1264904388000,"title":"184","body":"","multi":0,"allDay":false,"extension_id":2},{"id":186,"start":1264905180000,"end":1264906980000,"title":"185","body":"","multi":0,"allDay":false,"extension_id":2},{"id":187,"start":1264907772000,"end":1264909572000,"title":"186","body":"","multi":0,"allDay":false,"extension_id":2},{"id":188,"start":1264910364000,"end":1264912164000,"title":"187","body":"","multi":0,"allDay":false,"extension_id":2},{"id":189,"start":1264912956000,"end":1264914756000,"title":"188","body":"","multi":0,"allDay":false,"extension_id":2},{"id":190,"start":1264915548000,"end":1264917348000,"title":"189","body":"","multi":0,"allDay":false,"extension_id":2},{"id":191,"start":1264918140000,"end":1264919940000,"title":"190","body":"","multi":0,"allDay":false,"extension_id":2},{"id":192,"start":1264920732000,"end":1264922532000,"title":"191","body":"","multi":0,"allDay":false,"extension_id":2},{"id":193,"start":1264923324000,"end":1264925124000,"title":"192","body":"","multi":0,"allDay":false,"extension_id":2},{"id":194,"start":1264925916000,"end":1264927716000,"title":"193","body":"","multi":0,"allDay":false,"extension_id":2},{"id":195,"start":1264928508000,"end":1264930308000,"title":"194","body":"","multi":0,"allDay":false,"extension_id":2},{"id":196,"start":1264931100000,"end":1264932900000,"title":"195","body":"","multi":0,"allDay":false,"extension_id":2},{"id":197,"start":1264933692000,"end":1264935492000,"title":"196","body":"","multi":0,"allDay":false,"extension_id":2},{"id":198,"start":1264936284000,"end":1264938084000,"title":"197","body":"","multi":0,"allDay":false,"extension_id":2},{"id":199,"start":1264938876000,"end":1264940676000,"title":"198","body":"","multi":0,"allDay":false,"extension_id":2},{"id":200,"start":1264941468000,"end":1264943268000,"title":"199","body":"","multi":0,"allDay":false,"extension_id":2},{"id":201,"start":1264944060000,"end":1264945860000,"title":"200","body":"","multi":0,"allDay":false,"extension_id":2},{"id":202,"start":1264946652000,"end":1264948452000,"title":"201","body":"","multi":0,"allDay":false,"extension_id":2},{"id":203,"start":1264949244000,"end":1264951044000,"title":"202","body":"","multi":0,"allDay":false,"extension_id":2},{"id":204,"start":1264951836000,"end":1264953636000,"title":"203","body":"","multi":0,"allDay":false,"extension_id":2},{"id":205,"start":1264954428000,"end":1264956228000,"title":"204","body":"","multi":0,"allDay":false,"extension_id":2},{"id":206,"start":1264957020000,"end":1264958820000,"title":"205","body":"","multi":0,"allDay":false,"extension_id":2},{"id":207,"start":1264959612000,"end":1264961412000,"title":"206","body":"","multi":0,"allDay":false,"extension_id":2},{"id":208,"start":1264962204000,"end":1264964004000,"title":"207","body":"","multi":0,"allDay":false,"extension_id":2},{"id":209,"start":1264964796000,"end":1264966596000,"title":"208","body":"","multi":0,"allDay":false,"extension_id":2},{"id":210,"start":1264967388000,"end":1264969188000,"title":"209","body":"","multi":0,"allDay":false,"extension_id":2},{"id":211,"start":1264969980000,"end":1264971780000,"title":"210","body":"","multi":0,"allDay":false,"extension_id":2},{"id":212,"start":1264972572000,"end":1264974372000,"title":"211","body":"","multi":0,"allDay":false,"extension_id":2},{"id":213,"start":1264975164000,"end":1264976964000,"title":"212","body":"","multi":0,"allDay":false,"extension_id":2},{"id":214,"start":1264977756000,"end":1264979556000,"title":"213","body":"","multi":0,"allDay":false,"extension_id":2},{"id":215,"start":1264980348000,"end":1264982148000,"title":"214","body":"","multi":0,"allDay":false,"extension_id":2},{"id":216,"start":1264982940000,"end":1264984740000,"title":"215","body":"","multi":0,"allDay":false,"extension_id":2},{"id":217,"start":1264985532000,"end":1264987332000,"title":"216","body":"","multi":0,"allDay":false,"extension_id":2},{"id":218,"start":1264988124000,"end":1264989924000,"title":"217","body":"","multi":0,"allDay":false,"extension_id":2},{"id":219,"start":1264990716000,"end":1264992516000,"title":"218","body":"","multi":0,"allDay":false,"extension_id":2},{"id":220,"start":1264993308000,"end":1264995108000,"title":"219","body":"","multi":0,"allDay":false,"extension_id":2},{"id":221,"start":1264995900000,"end":1264997700000,"title":"220","body":"","multi":0,"allDay":false,"extension_id":2},{"id":222,"start":1264998492000,"end":1265000292000,"title":"221","body":"","multi":0,"allDay":false,"extension_id":2},{"id":223,"start":1265001084000,"end":1265002884000,"title":"222","body":"","multi":0,"allDay":false,"extension_id":2},{"id":224,"start":1265003676000,"end":1265005476000,"title":"223","body":"","multi":0,"allDay":false,"extension_id":2},{"id":225,"start":1265006268000,"end":1265008068000,"title":"224","body":"","multi":0,"allDay":false,"extension_id":2},{"id":226,"start":1265008860000,"end":1265010660000,"title":"225","body":"","multi":0,"allDay":false,"extension_id":2},{"id":227,"start":1265011452000,"end":1265013252000,"title":"226","body":"","multi":0,"allDay":false,"extension_id":2},{"id":228,"start":1265014044000,"end":1265015844000,"title":"227","body":"","multi":0,"allDay":false,"extension_id":2},{"id":229,"start":1265016636000,"end":1265018436000,"title":"228","body":"","multi":0,"allDay":false,"extension_id":2},{"id":230,"start":1265019228000,"end":1265021028000,"title":"229","body":"","multi":0,"allDay":false,"extension_id":2},{"id":231,"start":1265021820000,"end":1265023620000,"title":"230","body":"","multi":0,"allDay":false,"extension_id":2},{"id":232,"start":1265024412000,"end":1265026212000,"title":"231","body":"","multi":0,"allDay":false,"extension_id":2},{"id":233,"start":1265027004000,"end":1265028804000,"title":"232","body":"","multi":0,"allDay":false,"extension_id":2},{"id":234,"start":1265029596000,"end":1265031396000,"title":"233","body":"","multi":0,"allDay":false,"extension_id":2},{"id":235,"start":1265032188000,"end":1265033988000,"title":"234","body":"","multi":0,"allDay":false,"extension_id":2},{"id":236,"start":1265034780000,"end":1265036580000,"title":"235","body":"","multi":0,"allDay":false,"extension_id":2},{"id":237,"start":1265037372000,"end":1265039172000,"title":"236","body":"","multi":0,"allDay":false,"extension_id":2},{"id":238,"start":1265039964000,"end":1265041764000,"title":"237","body":"","multi":0,"allDay":false,"extension_id":2},{"id":239,"start":1265042556000,"end":1265044356000,"title":"238","body":"","multi":0,"allDay":false,"extension_id":2},{"id":240,"start":1265045148000,"end":1265046948000,"title":"239","body":"","multi":0,"allDay":false,"extension_id":2},{"id":241,"start":1265047740000,"end":1265049540000,"title":"240","body":"","multi":0,"allDay":false,"extension_id":2},{"id":242,"start":1265050332000,"end":1265052132000,"title":"241","body":"","multi":0,"allDay":false,"extension_id":2},{"id":243,"start":1265052924000,"end":1265054724000,"title":"242","body":"","multi":0,"allDay":false,"extension_id":2},{"id":244,"start":1265055516000,"end":1265057316000,"title":"243","body":"","multi":0,"allDay":false,"extension_id":2},{"id":245,"start":1265058108000,"end":1265059908000,"title":"244","body":"","multi":0,"allDay":false,"extension_id":2},{"id":246,"start":1265060700000,"end":1265062500000,"title":"245","body":"","multi":0,"allDay":false,"extension_id":2},{"id":247,"start":1265063292000,"end":1265065092000,"title":"246","body":"","multi":0,"allDay":false,"extension_id":2},{"id":248,"start":1265065884000,"end":1265067684000,"title":"247","body":"","multi":0,"allDay":false,"extension_id":2},{"id":249,"start":1265068476000,"end":1265070276000,"title":"248","body":"","multi":0,"allDay":false,"extension_id":2},{"id":250,"start":1265071068000,"end":1265072868000,"title":"249","body":"","multi":0,"allDay":false,"extension_id":2},{"id":251,"start":1265073660000,"end":1265075460000,"title":"250","body":"","multi":0,"allDay":false,"extension_id":2},{"id":252,"start":1265076252000,"end":1265078052000,"title":"251","body":"","multi":0,"allDay":false,"extension_id":2},{"id":253,"start":1265078844000,"end":1265080644000,"title":"252","body":"","multi":0,"allDay":false,"extension_id":2},{"id":254,"start":1265081436000,"end":1265083236000,"title":"253","body":"","multi":0,"allDay":false,"extension_id":2},{"id":255,"start":1265084028000,"end":1265085828000,"title":"254","body":"","multi":0,"allDay":false,"extension_id":2},{"id":256,"start":1265086620000,"end":1265088420000,"title":"255","body":"","multi":0,"allDay":false,"extension_id":2},{"id":257,"start":1265089212000,"end":1265091012000,"title":"256","body":"","multi":0,"allDay":false,"extension_id":2},{"id":258,"start":1265091804000,"end":1265093604000,"title":"257","body":"","multi":0,"allDay":false,"extension_id":2},{"id":259,"start":1265094396000,"end":1265096196000,"title":"258","body":"","multi":0,"allDay":false,"extension_id":2},{"id":260,"start":1265096988000,"end":1265098788000,"title":"259","body":"","multi":0,"allDay":false,"extension_id":2},{"id":261,"start":1265099580000,"end":1265101380000,"title":"260","body":"","multi":0,"allDay":false,"extension_id":2},{"id":262,"start":1265102172000,"end":1265103972000,"title":"261","body":"","multi":0,"allDay":false,"extension_id":2},{"id":263,"start":1265104764000,"end":1265106564000,"title":"262","body":"","multi":0,"allDay":false,"extension_id":2},{"id":264,"start":1265107356000,"end":1265109156000,"title":"263","body":"","multi":0,"allDay":false,"extension_id":2},{"id":265,"start":1265109948000,"end":1265111748000,"title":"264","body":"","multi":0,"allDay":false,"extension_id":2},{"id":266,"start":1265112540000,"end":1265114340000,"title":"265","body":"","multi":0,"allDay":false,"extension_id":2},{"id":267,"start":1265115132000,"end":1265116932000,"title":"266","body":"","multi":0,"allDay":false,"extension_id":2},{"id":268,"start":1265117724000,"end":1265119524000,"title":"267","body":"","multi":0,"allDay":false,"extension_id":2},{"id":269,"start":1265120316000,"end":1265122116000,"title":"268","body":"","multi":0,"allDay":false,"extension_id":2},{"id":270,"start":1265122908000,"end":1265124708000,"title":"269","body":"","multi":0,"allDay":false,"extension_id":2},{"id":271,"start":1265125500000,"end":1265127300000,"title":"270","body":"","multi":0,"allDay":false,"extension_id":2},{"id":272,"start":1265128092000,"end":1265129892000,"title":"271","body":"","multi":0,"allDay":false,"extension_id":2},{"id":273,"start":1265130684000,"end":1265132484000,"title":"272","body":"","multi":0,"allDay":false,"extension_id":2},{"id":274,"start":1265133276000,"end":1265135076000,"title":"273","body":"","multi":0,"allDay":false,"extension_id":2},{"id":275,"start":1265135868000,"end":1265137668000,"title":"274","body":"","multi":0,"allDay":false,"extension_id":2},{"id":276,"start":1265138460000,"end":1265140260000,"title":"275","body":"","multi":0,"allDay":false,"extension_id":2},{"id":277,"start":1265141052000,"end":1265142852000,"title":"276","body":"","multi":0,"allDay":false,"extension_id":2},{"id":278,"start":1265143644000,"end":1265145444000,"title":"277","body":"","multi":0,"allDay":false,"extension_id":2},{"id":279,"start":1265146236000,"end":1265148036000,"title":"278","body":"","multi":0,"allDay":false,"extension_id":2},{"id":280,"start":1265148828000,"end":1265150628000,"title":"279","body":"","multi":0,"allDay":false,"extension_id":2},{"id":281,"start":1265151420000,"end":1265153220000,"title":"280","body":"","multi":0,"allDay":false,"extension_id":2},{"id":282,"start":1265154012000,"end":1265155812000,"title":"281","body":"","multi":0,"allDay":false,"extension_id":2},{"id":283,"start":1265156604000,"end":1265158404000,"title":"282","body":"","multi":0,"allDay":false,"extension_id":2},{"id":284,"start":1265159196000,"end":1265160996000,"title":"283","body":"","multi":0,"allDay":false,"extension_id":2},{"id":285,"start":1265161788000,"end":1265163588000,"title":"284","body":"","multi":0,"allDay":false,"extension_id":2},{"id":286,"start":1265164380000,"end":1265166180000,"title":"285","body":"","multi":0,"allDay":false,"extension_id":2},{"id":287,"start":1265166972000,"end":1265168772000,"title":"286","body":"","multi":0,"allDay":false,"extension_id":2},{"id":288,"start":1265169564000,"end":1265171364000,"title":"287","body":"","multi":0,"allDay":false,"extension_id":2},{"id":289,"start":1265172156000,"end":1265173956000,"title":"288","body":"","multi":0,"allDay":false,"extension_id":2},{"id":290,"start":1265174748000,"end":1265176548000,"title":"289","body":"","multi":0,"allDay":false,"extension_id":2},{"id":291,"start":1265177340000,"end":1265179140000,"title":"290","body":"","multi":0,"allDay":false,"extension_id":2},{"id":292,"start":1265179932000,"end":1265181732000,"title":"291","body":"","multi":0,"allDay":false,"extension_id":2},{"id":293,"start":1265182524000,"end":1265184324000,"title":"292","body":"","multi":0,"allDay":false,"extension_id":2},{"id":294,"start":1265185116000,"end":1265186916000,"title":"293","body":"","multi":0,"allDay":false,"extension_id":2},{"id":295,"start":1265187708000,"end":1265189508000,"title":"294","body":"","multi":0,"allDay":false,"extension_id":2},{"id":296,"start":1265190300000,"end":1265192100000,"title":"295","body":"","multi":0,"allDay":false,"extension_id":2},{"id":297,"start":1265192892000,"end":1265194692000,"title":"296","body":"","multi":0,"allDay":false,"extension_id":2},{"id":298,"start":1265195484000,"end":1265197284000,"title":"297","body":"","multi":0,"allDay":false,"extension_id":2},{"id":299,"start":1265198076000,"end":1265199876000,"title":"298","body":"","multi":0,"allDay":false,"extension_id":2},{"id":300,"start":1265200668000,"end":1265202468000,"title":"299","body":"","multi":0,"allDay":false,"extension_id":2},{"id":301,"start":1265203260000,"end":1265205060000,"title":"300","body":"","multi":0,"allDay":false,"extension_id":2},{"id":302,"start":1265205852000,"end":1265207652000,"title":"301","body":"","multi":0,"allDay":false,"extension_id":2},{"id":303,"start":1265208444000,"end":1265210244000,"title":"302","body":"","multi":0,"allDay":false,"extension_id":2},{"id":304,"start":1265211036000,"end":1265212836000,"title":"303","body":"","multi":0,"allDay":false,"extension_id":2},{"id":305,"start":1265213628000,"end":1265215428000,"title":"304","body":"","multi":0,"allDay":false,"extension_id":2},{"id":306,"start":1265216220000,"end":1265218020000,"title":"305","body":"","multi":0,"allDay":false,"extension_id":2},{"id":307,"start":1265218812000,"end":1265220612000,"title":"306","body":"","multi":0,"allDay":false,"extension_id":2},{"id":308,"start":1265221404000,"end":1265223204000,"title":"307","body":"","multi":0,"allDay":false,"extension_id":2},{"id":309,"start":1265223996000,"end":1265225796000,"title":"308","body":"","multi":0,"allDay":false,"extension_id":2},{"id":310,"start":1265226588000,"end":1265228388000,"title":"309","body":"","multi":0,"allDay":false,"extension_id":2},{"id":311,"start":1265229180000,"end":1265230980000,"title":"310","body":"","multi":0,"allDay":false,"extension_id":2},{"id":312,"start":1265231772000,"end":1265233572000,"title":"311","body":"","multi":0,"allDay":false,"extension_id":2},{"id":313,"start":1265234364000,"end":1265236164000,"title":"312","body":"","multi":0,"allDay":false,"extension_id":2},{"id":314,"start":1265236956000,"end":1265238756000,"title":"313","body":"","multi":0,"allDay":false,"extension_id":2},{"id":315,"start":1265239548000,"end":1265241348000,"title":"314","body":"","multi":0,"allDay":false,"extension_id":2},{"id":316,"start":1265242140000,"end":1265243940000,"title":"315","body":"","multi":0,"allDay":false,"extension_id":2},{"id":317,"start":1265244732000,"end":1265246532000,"title":"316","body":"","multi":0,"allDay":false,"extension_id":2},{"id":318,"start":1265247324000,"end":1265249124000,"title":"317","body":"","multi":0,"allDay":false,"extension_id":2},{"id":319,"start":1265249916000,"end":1265251716000,"title":"318","body":"","multi":0,"allDay":false,"extension_id":2},{"id":320,"start":1265252508000,"end":1265254308000,"title":"319","body":"","multi":0,"allDay":false,"extension_id":2},{"id":321,"start":1265255100000,"end":1265256900000,"title":"320","body":"","multi":0,"allDay":false,"extension_id":2},{"id":322,"start":1265257692000,"end":1265259492000,"title":"321","body":"","multi":0,"allDay":false,"extension_id":2},{"id":323,"start":1265260284000,"end":1265262084000,"title":"322","body":"","multi":0,"allDay":false,"extension_id":2},{"id":324,"start":1265262876000,"end":1265264676000,"title":"323","body":"","multi":0,"allDay":false,"extension_id":2},{"id":325,"start":1265265468000,"end":1265267268000,"title":"324","body":"","multi":0,"allDay":false,"extension_id":2},{"id":326,"start":1265268060000,"end":1265269860000,"title":"325","body":"","multi":0,"allDay":false,"extension_id":2},{"id":327,"start":1265270652000,"end":1265272452000,"title":"326","body":"","multi":0,"allDay":false,"extension_id":2},{"id":328,"start":1265273244000,"end":1265275044000,"title":"327","body":"","multi":0,"allDay":false,"extension_id":2},{"id":329,"start":1265275836000,"end":1265277636000,"title":"328","body":"","multi":0,"allDay":false,"extension_id":2},{"id":330,"start":1265278428000,"end":1265280228000,"title":"329","body":"","multi":0,"allDay":false,"extension_id":2},{"id":331,"start":1265281020000,"end":1265282820000,"title":"330","body":"","multi":0,"allDay":false,"extension_id":2},{"id":332,"start":1265283612000,"end":1265285412000,"title":"331","body":"","multi":0,"allDay":false,"extension_id":2},{"id":333,"start":1265286204000,"end":1265288004000,"title":"332","body":"","multi":0,"allDay":false,"extension_id":2},{"id":334,"start":1265288796000,"end":1265290596000,"title":"333","body":"","multi":0,"allDay":false,"extension_id":2},{"id":335,"start":1265291388000,"end":1265293188000,"title":"334","body":"","multi":0,"allDay":false,"extension_id":2},{"id":336,"start":1265293980000,"end":1265295780000,"title":"335","body":"","multi":0,"allDay":false,"extension_id":2},{"id":337,"start":1265296572000,"end":1265298372000,"title":"336","body":"","multi":0,"allDay":false,"extension_id":2},{"id":338,"start":1265299164000,"end":1265300964000,"title":"337","body":"","multi":0,"allDay":false,"extension_id":2},{"id":339,"start":1265301756000,"end":1265303556000,"title":"338","body":"","multi":0,"allDay":false,"extension_id":2},{"id":340,"start":1265304348000,"end":1265306148000,"title":"339","body":"","multi":0,"allDay":false,"extension_id":2},{"id":341,"start":1265306940000,"end":1265308740000,"title":"340","body":"","multi":0,"allDay":false,"extension_id":2},{"id":342,"start":1265309532000,"end":1265311332000,"title":"341","body":"","multi":0,"allDay":false,"extension_id":2},{"id":343,"start":1265312124000,"end":1265313924000,"title":"342","body":"","multi":0,"allDay":false,"extension_id":2},{"id":344,"start":1265314716000,"end":1265316516000,"title":"343","body":"","multi":0,"allDay":false,"extension_id":2},{"id":345,"start":1265317308000,"end":1265319108000,"title":"344","body":"","multi":0,"allDay":false,"extension_id":2},{"id":346,"start":1265319900000,"end":1265321700000,"title":"345","body":"","multi":0,"allDay":false,"extension_id":2},{"id":347,"start":1265322492000,"end":1265324292000,"title":"346","body":"","multi":0,"allDay":false,"extension_id":2},{"id":348,"start":1265325084000,"end":1265326884000,"title":"347","body":"","multi":0,"allDay":false,"extension_id":2},{"id":349,"start":1265327676000,"end":1265329476000,"title":"348","body":"","multi":0,"allDay":false,"extension_id":2},{"id":350,"start":1265330268000,"end":1265332068000,"title":"349","body":"","multi":0,"allDay":false,"extension_id":2},{"id":351,"start":1265332860000,"end":1265334660000,"title":"350","body":"","multi":0,"allDay":false,"extension_id":2},{"id":352,"start":1265335452000,"end":1265337252000,"title":"351","body":"","multi":0,"allDay":false,"extension_id":2},{"id":353,"start":1265338044000,"end":1265339844000,"title":"352","body":"","multi":0,"allDay":false,"extension_id":2},{"id":354,"start":1265340636000,"end":1265342436000,"title":"353","body":"","multi":0,"allDay":false,"extension_id":2},{"id":355,"start":1265343228000,"end":1265345028000,"title":"354","body":"","multi":0,"allDay":false,"extension_id":2},{"id":356,"start":1265345820000,"end":1265347620000,"title":"355","body":"","multi":0,"allDay":false,"extension_id":2},{"id":357,"start":1265348412000,"end":1265350212000,"title":"356","body":"","multi":0,"allDay":false,"extension_id":2},{"id":358,"start":1265351004000,"end":1265352804000,"title":"357","body":"","multi":0,"allDay":false,"extension_id":2},{"id":359,"start":1265353596000,"end":1265355396000,"title":"358","body":"","multi":0,"allDay":false,"extension_id":2},{"id":360,"start":1265356188000,"end":1265357988000,"title":"359","body":"","multi":0,"allDay":false,"extension_id":2},{"id":361,"start":1265358780000,"end":1265360580000,"title":"360","body":"","multi":0,"allDay":false,"extension_id":2},{"id":362,"start":1265361372000,"end":1265363172000,"title":"361","body":"","multi":0,"allDay":false,"extension_id":2},{"id":363,"start":1265363964000,"end":1265365764000,"title":"362","body":"","multi":0,"allDay":false,"extension_id":2},{"id":364,"start":1265366556000,"end":1265368356000,"title":"363","body":"","multi":0,"allDay":false,"extension_id":2},{"id":365,"start":1265369148000,"end":1265370948000,"title":"364","body":"","multi":0,"allDay":false,"extension_id":2},{"id":366,"start":1265371740000,"end":1265373540000,"title":"365","body":"","multi":0,"allDay":false,"extension_id":2},{"id":367,"start":1265374332000,"end":1265376132000,"title":"366","body":"","multi":0,"allDay":false,"extension_id":2},{"id":368,"start":1265376924000,"end":1265378724000,"title":"367","body":"","multi":0,"allDay":false,"extension_id":2},{"id":369,"start":1265379516000,"end":1265381316000,"title":"368","body":"","multi":0,"allDay":false,"extension_id":2},{"id":370,"start":1265382108000,"end":1265383908000,"title":"369","body":"","multi":0,"allDay":false,"extension_id":2},{"id":371,"start":1265384700000,"end":1265386500000,"title":"370","body":"","multi":0,"allDay":false,"extension_id":2},{"id":372,"start":1265387292000,"end":1265389092000,"title":"371","body":"","multi":0,"allDay":false,"extension_id":2},{"id":373,"start":1265389884000,"end":1265391684000,"title":"372","body":"","multi":0,"allDay":false,"extension_id":2},{"id":374,"start":1265392476000,"end":1265394276000,"title":"373","body":"","multi":0,"allDay":false,"extension_id":2},{"id":375,"start":1265395068000,"end":1265396868000,"title":"374","body":"","multi":0,"allDay":false,"extension_id":2},{"id":376,"start":1265397660000,"end":1265399460000,"title":"375","body":"","multi":0,"allDay":false,"extension_id":2},{"id":377,"start":1265400252000,"end":1265402052000,"title":"376","body":"","multi":0,"allDay":false,"extension_id":2},{"id":378,"start":1265402844000,"end":1265404644000,"title":"377","body":"","multi":0,"allDay":false,"extension_id":2},{"id":379,"start":1265405436000,"end":1265407236000,"title":"378","body":"","multi":0,"allDay":false,"extension_id":2},{"id":380,"start":1265408028000,"end":1265409828000,"title":"379","body":"","multi":0,"allDay":false,"extension_id":2},{"id":381,"start":1265410620000,"end":1265412420000,"title":"380","body":"","multi":0,"allDay":false,"extension_id":2},{"id":382,"start":1265413212000,"end":1265415012000,"title":"381","body":"","multi":0,"allDay":false,"extension_id":2},{"id":383,"start":1265415804000,"end":1265417604000,"title":"382","body":"","multi":0,"allDay":false,"extension_id":2},{"id":384,"start":1265418396000,"end":1265420196000,"title":"383","body":"","multi":0,"allDay":false,"extension_id":2},{"id":385,"start":1265420988000,"end":1265422788000,"title":"384","body":"","multi":0,"allDay":false,"extension_id":2},{"id":386,"start":1265423580000,"end":1265425380000,"title":"385","body":"","multi":0,"allDay":false,"extension_id":2},{"id":387,"start":1265426172000,"end":1265427972000,"title":"386","body":"","multi":0,"allDay":false,"extension_id":2},{"id":388,"start":1265428764000,"end":1265430564000,"title":"387","body":"","multi":0,"allDay":false,"extension_id":2},{"id":389,"start":1265431356000,"end":1265433156000,"title":"388","body":"","multi":0,"allDay":false,"extension_id":2},{"id":390,"start":1265433948000,"end":1265435748000,"title":"389","body":"","multi":0,"allDay":false,"extension_id":2},{"id":391,"start":1265436540000,"end":1265438340000,"title":"390","body":"","multi":0,"allDay":false,"extension_id":2},{"id":392,"start":1265439132000,"end":1265440932000,"title":"391","body":"","multi":0,"allDay":false,"extension_id":2},{"id":393,"start":1265441724000,"end":1265443524000,"title":"392","body":"","multi":0,"allDay":false,"extension_id":2},{"id":394,"start":1265444316000,"end":1265446116000,"title":"393","body":"","multi":0,"allDay":false,"extension_id":2},{"id":395,"start":1265446908000,"end":1265448708000,"title":"394","body":"","multi":0,"allDay":false,"extension_id":2},{"id":396,"start":1265449500000,"end":1265451300000,"title":"395","body":"","multi":0,"allDay":false,"extension_id":2},{"id":397,"start":1265452092000,"end":1265453892000,"title":"396","body":"","multi":0,"allDay":false,"extension_id":2},{"id":398,"start":1265454684000,"end":1265456484000,"title":"397","body":"","multi":0,"allDay":false,"extension_id":2},{"id":399,"start":1265457276000,"end":1265459076000,"title":"398","body":"","multi":0,"allDay":false,"extension_id":2},{"id":400,"start":1265459868000,"end":1265461668000,"title":"399","body":"","multi":0,"allDay":false,"extension_id":2},{"id":401,"start":1265462460000,"end":1265464260000,"title":"400","body":"","multi":0,"allDay":false,"extension_id":2},{"id":402,"start":1265465052000,"end":1265466852000,"title":"401","body":"","multi":0,"allDay":false,"extension_id":2},{"id":403,"start":1265467644000,"end":1265469444000,"title":"402","body":"","multi":0,"allDay":false,"extension_id":2},{"id":404,"start":1265470236000,"end":1265472036000,"title":"403","body":"","multi":0,"allDay":false,"extension_id":2},{"id":405,"start":1265472828000,"end":1265474628000,"title":"404","body":"","multi":0,"allDay":false,"extension_id":2},{"id":406,"start":1265475420000,"end":1265477220000,"title":"405","body":"","multi":0,"allDay":false,"extension_id":2},{"id":407,"start":1265478012000,"end":1265479812000,"title":"406","body":"","multi":0,"allDay":false,"extension_id":2},{"id":408,"start":1265480604000,"end":1265482404000,"title":"407","body":"","multi":0,"allDay":false,"extension_id":2},{"id":409,"start":1265483196000,"end":1265484996000,"title":"408","body":"","multi":0,"allDay":false,"extension_id":2},{"id":410,"start":1265485788000,"end":1265487588000,"title":"409","body":"","multi":0,"allDay":false,"extension_id":2},{"id":411,"start":1265488380000,"end":1265490180000,"title":"410","body":"","multi":0,"allDay":false,"extension_id":2},{"id":412,"start":1265490972000,"end":1265492772000,"title":"411","body":"","multi":0,"allDay":false,"extension_id":2},{"id":413,"start":1265493564000,"end":1265495364000,"title":"412","body":"","multi":0,"allDay":false,"extension_id":2},{"id":414,"start":1265496156000,"end":1265497956000,"title":"413","body":"","multi":0,"allDay":false,"extension_id":2}] diff --git a/fullcalendar-main/tests/manual/old/many_timegrid_events.html b/fullcalendar-main/tests/manual/old/many_timegrid_events.html new file mode 100644 index 0000000..6a78a86 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/many_timegrid_events.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + initialView: 'week', + initialDate: '2009-12-16', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: 'many_timegrid_events_json.txt' + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/many_timegrid_events_json.txt b/fullcalendar-main/tests/manual/old/many_timegrid_events_json.txt new file mode 100644 index 0000000..f8deefe --- /dev/null +++ b/fullcalendar-main/tests/manual/old/many_timegrid_events_json.txt @@ -0,0 +1 @@ +[{"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13"}, {"title": "event", "start": "2009-12-13 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-13 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14"}, {"title": "event", "start": "2009-12-14 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-14 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15"}, {"title": "event", "start": "2009-12-15 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-15 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16"}, {"title": "event", "start": "2009-12-16 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-16 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17"}, {"title": "event", "start": "2009-12-17 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-17 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18"}, {"title": "event", "start": "2009-12-18 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-18 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19"}, {"title": "event", "start": "2009-12-19 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 23:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 00:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 01:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 02:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 03:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 04:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 05:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 06:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 07:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 08:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 09:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 10:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 11:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 12:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 13:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 14:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 15:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 16:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 17:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 18:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 19:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 20:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 21:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 22:00:00", "allDay": false}, {"title": "event", "start": "2009-12-19 23:00:00", "allDay": false}] diff --git a/fullcalendar-main/tests/manual/old/memory_leak.html b/fullcalendar-main/tests/manual/old/memory_leak.html new file mode 100644 index 0000000..1c5430c --- /dev/null +++ b/fullcalendar-main/tests/manual/old/memory_leak.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + var count = 0; + var intervalID; + + $('#start').on('click', start); + $('#stop').on('click', stop); + + function start() { + initCalendar(); + intervalID = setInterval(function() { + destroyCalendar(); + initCalendar(); + if (count > 100) { + stop(); + } + count++; + }, 200); + } + + function stop() { + if (intervalID) { + clearInterval(intervalID); + intervalID = null; + } + destroyCalendar(); + } + + function initCalendar() { + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + initialDate: '2014-06-12', + initialView: 'dayGridMonth', // week + editable: true, + selectable: true, + droppable: true, + events: [ + { + title: 'All Day Event', + start: '2014-06-01' + }, + { + title: 'Long Event', + start: '2014-06-07', + end: '2014-06-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-06-12T10:30:00', + end: '2014-06-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-06-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-06-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-06-28' + } + ] + }); + } + + function destroyCalendar() { + $('#calendar').fullCalendar('destroy'); + } + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> + + <button id='start'>START</button> + <button id='stop'>STOP</button> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/method_destroy.html b/fullcalendar-main/tests/manual/old/method_destroy.html new file mode 100644 index 0000000..5bd6130 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/method_destroy.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var cal = $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + } + }); + + console.log(cal.data('fullCalendar')); + + cal.fullCalendar('destroy'); + + console.log(cal.data('fullCalendar')); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'>some text in here</div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/methods.html b/fullcalendar-main/tests/manual/old/methods.html new file mode 100644 index 0000000..e14744b --- /dev/null +++ b/fullcalendar-main/tests/manual/old/methods.html @@ -0,0 +1,192 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + var cal, staticEvents; + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $(document).ready(function() { + cal = $('#calendar').fullCalendar({ + editable: true, + weekends: false, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + loading: function(bool) { + if (bool) { + $('#loading').show(); + }else{ + $('#loading').hide(); + } + }, + events: staticEvents = [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + id: 777, + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + }); + + function updateEventStart() { + var event = cal.fullCalendar('clientEvents', 777)[0]; + event.start = new Date(y, m, d, 13, 30); + event.end = new Date(y, m, d, 14, 50); + //event.start = new Date(y, m, 25, 10, 30); // move big days + //event.end = new Date(y, m, 26); + //event.allDay = true; + cal.fullCalendar('updateEvent', event); + } + + function updateRepeatingEvent() { + var event = cal.fullCalendar('clientEvents', 999)[0]; + event.start = new Date(y, m, 4, 13, 30); + event.end = new Date(y, m, 5, 2, 0); + event.allDay = true; + event.title = "repeat yo"; + //event.editable = false; + event.url = "http://google.com/"; + event.color = 'red'; + event.textColor = 'green'; + cal.fullCalendar('updateEvent', event); + //console.log(cal.fullCalendar('clientEvents', 2)); + } + + function renderEvent(stick) { + cal.fullCalendar('renderEvent', { + start: new Date(y, m, 17), + title: 'heyman' + }, stick); + } + + function getView() { + var view = cal.fullCalendar('getView'); + console.log(view.activeStart + ' --- ' + view.activeEnd + ' "' + view.title + '"'); + } + + function getDate() { + console.log(cal.fullCalendar('getDate')); + } + + function optionGetter() { + console.log(cal.fullCalendar('option', 'editable')); + } + + var gcalFeed = FullCalendar.gcalFeed("http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic"); + + var jsonFeed = "../demos/json-events.php"; + +</script> +<style> + + button { + font-size: 11px; + } + +</style> +</head> +<body style='font-size:12px'> +<p> + +<button onclick="cal.fullCalendar('prev')">prev</button> +<button onclick="cal.fullCalendar('next')">next</button> +<button onclick="cal.fullCalendar('today')">today</button> +<button onclick="cal.fullCalendar('gotoDate', 1999, 9, 31)">Oct 31 1999</button> +<button onclick="cal.fullCalendar('gotoDate', new Date(1999, 9, 30))">Oct 30 1999 (Date)</button> +<button onclick="cal.fullCalendar('incrementDate', 1, 1, 1)">+1 +1 +1</button> +<button onclick="cal.fullCalendar('incrementDate', -1, -1, -1)">-1 -1 -1</button> + +<button onclick="updateEventStart()">update event start</button> +<button onclick="updateRepeatingEvent()">update repeating event</button> +<button onclick="renderEvent(false)">render new event</button> +<button onclick="renderEvent(true)">render new sticky event</button> +<br /> + +<button onclick="cal.fullCalendar('removeEvents')">remove all</button> +<button onclick="cal.fullCalendar('removeEvents', 999)">remove repeating events</button> +<button onclick="cal.fullCalendar('removeEvents', function(e){return !e.allDay})">remove timed events</button> +<button onclick="console.log(cal.fullCalendar('clientEvents'))">log events</button> +<button onclick="console.log(cal.fullCalendar('clientEvents', '999'))">log repeating events</button> +<button onclick="console.log(cal.fullCalendar('clientEvents', function(e){return e.allDay}))">log all-day events</button> +<br /> + +<button onclick="cal.fullCalendar('addEventSource', staticEvents)">+ static events</button> +<button onclick="cal.fullCalendar('removeEventSource', staticEvents)">- static events</button> +<button onclick="cal.fullCalendar('addEventSource', gcalFeed)">+ google-calendar</button> +<button onclick="cal.fullCalendar('removeEventSource', gcalFeed)">- google-calendar</button> +<button onclick="cal.fullCalendar('addEventSource', jsonFeed)">+ json</button> +<button onclick="cal.fullCalendar('removeEventSource', jsonFeed)">- json</button> + +<button onclick="cal.fullCalendar('rerenderEvents')">rerender events</button> +<button onclick="cal.fullCalendar('refetchEvents')">refetch events</button> +<br /> + +<button onclick="cal.fullCalendar('changeView', 'dayGridMonth')">change to month</button> +<button onclick="cal.fullCalendar('changeView', 'dayGridWeek')">change to dayGridWeek</button> +<button onclick="cal.fullCalendar('changeView', 'dayGridDay')">change to dayGridDay</button> +<button onclick="getView()">getView</button> +<button onclick="getDate()">getDate</button> +<button onclick="optionGetter()">option getter</button> +<button onclick="cal.width(1100)">change width (passive)</button> +<button onclick="cal.fullCalendar('render')">render</button> +<button onclick="cal.fullCalendar('option', 'height', 1000)">change height</button> + +</p> +<div id='loading' style='position:absolute;display:none'>loading...</div> +<div id='calendar' style='width:70%;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/nav-links.html b/fullcalendar-main/tests/manual/old/nav-links.html new file mode 100644 index 0000000..b657e39 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/nav-links.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day,listWeek' + }, + initialDate: '2014-06-12', + weekNumberCalculation: 'ISO', + weekNumbersWithinDays: true, + weekNumbers: true, + + navLinks: true, + //navLinks: { + // day: 'day' + //}, + + dayClick: function() { + alert('dayClick!!!'); // should not fire + }, + + editable: true, + events: [ + { + title: 'All Day Event', + start: '2014-06-01' + }, + { + title: 'Long Event', + start: '2014-06-07', + end: '2014-06-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-06-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-06-12T10:30:00', + end: '2014-06-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-06-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-06-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-06-28' + } + ] + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/no_event_titles.html b/fullcalendar-main/tests/manual/old/no_event_titles.html new file mode 100644 index 0000000..5f7c683 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/no_event_titles.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: null, + start: new Date(y, m, 1) + }, + { + title: '', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + start: new Date(y, m, d, 10, 30), + allDay: false + } + ], + eventRender: function(event, el) { + el.find('.fc-event-title').text("something"); + } + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/options.html b/fullcalendar-main/tests/manual/old/options.html new file mode 100644 index 0000000..022dea3 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/options.html @@ -0,0 +1,143 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $(document).ready(function() { + $('#calendar').fullCalendar({ + + //year: 2011, + //month: 2, + //date: 1, + + /* + //previous bug, should NOT be march + year: 2009, + month: 1, + date: 15, + */ + + //initialView: 'dayGridMonth', + + selectable: true, + selectMirror: true, + + //weekends: false, + + height: 700, + //contentHeight: 500, + //aspectRatio: 2, + + headerToolbar: { + left: 'title', + center: 'month,week,dayGridWeek,day,dayGridDay', + right: 'today prevYear,prev,next,nextYear' + }, + + editable: true, + dragRevertDuration: 100, + + //allDaySlot: false, + allDayContent: 'ALLDAY', + firstHour: 10, + slotMinutes: 15, + defaultEventMinutes: 45, + + //defaultAllDay: false, + + /* + titleFormat: { + month: "'hey!'" + }, + */ + + dayHeaderFormat: { + month: "dddd" + //timeGrid: "ddd M/d!!!" // BUG: this wont work. timeGrid doesn't override our default for 'week' + }, + + timeFormat: "h(:mm)[T]{ - h(:mm)T}", + //timeFormat: { week: "'YO'" }, + + slotMinTime: 5, + slotMaxTime: '10:45pm', + //dayClick: function(date) { + // console.log(date); + //}, + //direction: 'rtl', + + eventColor: 'green', + eventTextColor: 'yellow', + eventBorderColor: 'black', + //eventBackgroundColor: 'red', + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1), + color: 'gray', + //backgroundColor: 'red', + textColor: 'white', + borderColor: '#000' + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + id: 777, + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +</head> +<body style='font-size:12px'> +<div id='calendar' style='width:70%;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/past_future_classNames.html b/fullcalendar-main/tests/manual/old/past_future_classNames.html new file mode 100644 index 0000000..9f0ee97 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/past_future_classNames.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + + .fc-past { + background: #fbc0fc; + } + + .fc-future { + background: #a4ffa4; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/plain.html b/fullcalendar-main/tests/manual/old/plain.html new file mode 100644 index 0000000..02075be --- /dev/null +++ b/fullcalendar-main/tests/manual/old/plain.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../dist/core/main.js'></script> +<script src='../../dist/interaction/main.js'></script> +<script src='../../dist/daygrid/main.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2019-02-12', + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2019-02-01' + }, + { + title: 'Long Event', + start: '2019-02-07', + end: '2019-02-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2019-02-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2019-02-16T16:00:00' + }, + { + title: 'Conference', + start: '2019-02-11', + end: '2019-02-13' + }, + { + title: 'Meeting', + start: '2019-02-12T10:30:00', + end: '2019-02-12T12:30:00' + }, + { + title: 'Lunch', + start: '2019-02-12T12:00:00' + }, + { + title: 'Meeting', + start: '2019-02-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2019-02-12T17:30:00' + }, + { + title: 'Dinner', + start: '2019-02-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2019-02-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2019-02-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/profiling.html b/fullcalendar-main/tests/manual/old/profiling.html new file mode 100644 index 0000000..4a8ed8a --- /dev/null +++ b/fullcalendar-main/tests/manual/old/profiling.html @@ -0,0 +1,242 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + var isOneTime = false; + + function initProfilingAction(containerEl, execFunc, teardownFunc) { + containerEl.find('.profiling-action__button').on('click', function() { + if (isOneTime) { + containerEl.find('.profiling-action__result') + .text(executeOneTime(execFunc) + 'ms'); + } + else { + executeTimes(execFunc, teardownFunc, 100).then(function(res) { + containerEl.find('.profiling-action__result') + .text(res + 'ms ave'); + }); + } + }); + } + + function executeOneTime(execFunc) { + var startMs; + var totalMs; + + execFunc(function() { + startMs = new Date().valueOf(); + }, function() { + totalMs = new Date().valueOf() - startMs; + }); + + return totalMs; + } + + function executeTimes(execFunc, teardownFunc, times) { + var deferred = $.Deferred(); + var totalTotalMs = 0; + var i = 0; + + function next() { + if (i < times) { + setTimeout(function() { + var startMs; + var totalMs; + + if (i && teardownFunc) { + teardownFunc(); + } + execFunc(function() { + startMs = new Date().valueOf(); + }, function() { + totalMs = new Date().valueOf() - startMs; + }); + totalTotalMs += totalMs; + + i++; + next(); + }, 0); + } + else { + deferred.resolve(totalTotalMs / times); + } + } + + next(); + + return deferred.promise(); + } + + function initCalendar() { + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day,listWeek' + }, + initialDate: '2017-07-01', + navLinks: true, // can click day/week names to navigate views + editable: true + }); + } + + function destroyCalendar() { + $('#calendar').fullCalendar('destroy'); + } + + initProfilingAction($('#init-calendar'), function(start, stop) { + start(); + initCalendar(); + stop(); + }, destroyCalendar); + + initProfilingAction($('#render-month-events'), function(start, stop) { + initCalendar(); + + var calendar = $('#calendar').fullCalendar('getCalendar'); + calendar.changeView('dayGridMonth'); + + var date = calendar.view.start.clone(); + var end = calendar.view.end.clone(); + var events = []; + + while (date.isBefore(end)) { + events.push({ + title: '3 day event', + start: date.clone(), + end: date.clone().add(3, 'days') + }, { + title: '2 day timed event', + start: date.clone().time('03:00'), + end: date.clone().add(1, 'day').time('20:00').format() + }, { + title: 'timed event', + start: date.clone().time('16:00') + }, { + title: 'timed event', + start: date.clone().time('16:00') + }, { + title: 'timed event', + start: date.clone().time('16:00') + }, { + title: 'timed event', + start: date.clone().time('16:00') + }, { + title: 'timed event', + start: date.clone().time('16:00') + }); + date.add(1, 'day'); + } + + start(); + calendar.renderEvents(events); + stop(); + //console.log('rendered ' + events.length + ' events'); + }, destroyCalendar); + + initProfilingAction($('#render-timegrid-events'), function(start, stop) { + initCalendar(); + + var calendar = $('#calendar').fullCalendar('getCalendar'); + calendar.changeView('week'); + + var date = calendar.view.start.clone(); + var end = calendar.view.end.clone(); + var events = []; + var time; + var calendar; + + while (date.isBefore(end)) { + time = moment.duration(0); + + while (time < moment.duration('24:00')) { + events.push({ + title: 'event', + start: date.clone().time(time) + }); + + time.add(30, 'minutes'); + } + + date.add(1, 'day'); + } + + start(); + calendar.renderEvents(events); + stop(); + //console.log('rendered ' + events.length + ' events'); + }, destroyCalendar); + + initProfilingAction($('#clear-events'), function(start, stop) { + var calendar = $('#calendar').fullCalendar('getCalendar'); + + start(); + calendar.removeEvents(); + stop(); + }); + + }); + +</script> +<style> + + body { + margin: 10px 10px; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + overflow: scroll; + } + + #profiling-area { + float: right; + text-align: right; + } + + #calendar { + max-width: 900px; + float: left; + } + +</style> +</head> +<body> + + <div id='profiling-area'> + + <div class='profiling-action' id='init-calendar'> + <span class='profiling-action__result'></span> + <button class='profiling-action__button'>init calendar</button> + </div> + + <div class='profiling-action' id='render-month-events'> + <span class='profiling-action__result'></span> + <button class='profiling-action__button'>render events for month</button> + </div> + + <div class='profiling-action' id='render-timegrid-events'> + <span class='profiling-action__result'></span> + <button class='profiling-action__button'>render events for timegrid</button> + </div> + + <div class='profiling-action' id='clear-events'> + <span class='profiling-action__result'></span> + <button class='profiling-action__button'>clear events</button> + </div> + + </div> + + <div id='calendar'></div> + + <div style='clear:both'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/profiling_results.txt b/fullcalendar-main/tests/manual/old/profiling_results.txt new file mode 100644 index 0000000..f3817ed --- /dev/null +++ b/fullcalendar-main/tests/manual/old/profiling_results.txt @@ -0,0 +1,30 @@ + +# v3.5.0 + +## Chrome + +27.71ms ave init calendar (4% improvement) +102.29ms ave render events for month (22% improvement) +112.77ms ave render events for timegrid (29% improvement) + +## IE11 + +72.79ms ave init calendar (10% improvement) +418.35ms ave render events for month (10% improvement) +393.8ms ave render events for timegrid (28% improvement) + + + +# v3.4.0 + +## Chrome + +28.9ms ave init calendar +131.7ms ave render events for month +157.91ms ave render events for timegrid + +## IE11 + +80.49ms ave init calendar +464.73ms ave render events for month +550.13ms ave render events for timegrid diff --git a/fullcalendar-main/tests/manual/old/selectable.html b/fullcalendar-main/tests/manual/old/selectable.html new file mode 100644 index 0000000..56c9c9f --- /dev/null +++ b/fullcalendar-main/tests/manual/old/selectable.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + // TODO: get rid of this!!! (used at the bottom too) + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + var calendar; + + $(document).ready(function() { + + calendar = $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + date: '2014-01-12', + initialView: 'dayGridMonth', + + //firstDay: 1, + //direction: 'rtl', + //slotMinTime: 7, + //weekends: false, + //allDaySlot: false, + //hiddenDays: [ 2, 4 ], // tuesdays and thursdays + + selectable: true, + + selectMirror: true, + /* + selectMirror: function(start, end) { + return $("<div style='background:red' />").text(start+' '+end); + }, + */ + + //unselectAuto: false, + //unselectCancel: '.fc', + + select: function(start, end, ev) { + console.log( + '---- selection ----\n' + + 'start: ' + start.format() + '\n' + + 'end: ' + end.format() + ); + if (ev) { + //console.log('select mouse: ' + ev.pageX + ', ' + ev.pageY); + } + }, + unselect: function(ev) { + console.log('unselect'); + if (ev) { + //console.log('unselect mouse: ' + ev.pageX + ', ' + ev.pageY); + } + }, + dayClick: function(date) { + console.log('DAYCLICK', date.format()); + console.log(this); + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: '2014-01-01' + }, + { + title: 'Long Event', + start: '2014-01-07', + end: '2014-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2014-01-16T16:00:00' + }, + { + title: 'Meeting', + start: '2014-01-12T10:30:00', + end: '2014-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2014-01-12T12:00:00' + }, + { + title: 'Birthday Party', + start: '2014-01-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2014-01-28' + } + ] + }); + + + }); + +</script> +<style> + + body { + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d-1), new Date(y, m, d-2), true)">1day, allday</button> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d-1))">1day, noend, noallday</button> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d-1), null, false)">1day, noend, allday=false</button> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d, 5, 15), new Date(y, m, d, 15, 30), false)">1day, timed</button> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d-3), new Date(y, m, d), true)">3day, allday</button> +<button onclick="calendar.fullCalendar('select', new Date(y, m, d-2, 5, 15), new Date(y, m, d+1, 15, 30), false)">3day, timed</button> +<button onclick="calendar.fullCalendar('unselect')">unselect</button> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/short_timegrid.html b/fullcalendar-main/tests/manual/old/short_timegrid.html new file mode 100644 index 0000000..28745f8 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/short_timegrid.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + initialView: 'week', + allDaySlot: false, + slotMinutes: 60, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 70%; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/skip-redraw-test.html b/fullcalendar-main/tests/manual/old/skip-redraw-test.html new file mode 100644 index 0000000..2752045 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/skip-redraw-test.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> +<head> +<style> + + .activeDay { + background:#eee !important; + } + +</style> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + var prevDay = null; + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + dayClick: function(date, allDay, jsEvent, view) { + + $('#calendar').fullCalendar( 'gotoDate', date); + + }, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <h1>gotoDate test page</h1> + <p> + This page is to test refactoring of gotoDate to check whether or not to call render() based on view.visStart and view.visEnd. + </p> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/slotEventOverlap-demo.html b/fullcalendar-main/tests/manual/old/slotEventOverlap-demo.html new file mode 100644 index 0000000..4042c88 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/slotEventOverlap-demo.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate() + 1; + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,day' + }, + editable: true, + initialView: 'day', + allDaySlot: false, + year: y, + month: m, + date: d, + slotEventOverlap: false, + events: [ + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 17, 0), + allDay: false + }, + { + title: 'Another Meeting', + start: new Date(y, m, d, 12, 15), + end: new Date(y, m, d, 14, 30), + allDay: false + }, + { + title: 'Party', + start: new Date(y, m, d, 9, 0), + end: new Date(y, m, d, 16, 0), + allDay: false + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 14px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + + .fc-event { + box-shadow: 0 2px 10px rgba(0, 0, 0, .25); + border-radius: 5px !important; + } + + .fc-event-bg { + opacity: .3 !important; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/slot_event_overlap.html b/fullcalendar-main/tests/manual/old/slot_event_overlap.html new file mode 100644 index 0000000..57cfd74 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/slot_event_overlap.html @@ -0,0 +1,215 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + initialView: 'day', + firstHour: 0, + year: 2013, + month: 6, // July + date: 31, + events: 'slot_event_overlap.json', + _eventsPositioned: function() { + testOverlap($('#calendar'), true, false); + } + }); + + $('#calendar-nooverlap').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + initialView: 'day', + firstHour: 0, + year: 2013, + month: 6, // July + date: 31, + events: 'slot_event_overlap.json', + slotEventOverlap: false, + _eventsPositioned: function() { + testOverlap($('#calendar-nooverlap'), false, false); + } + }); + + $('#calendar-rtl').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + initialView: 'day', + firstHour: 0, + year: 2013, + month: 6, // July + date: 31, + events: 'slot_event_overlap.json', + direction: 'rtl', + _eventsPositioned: function() { + testOverlap($('#calendar-rtl'), true, true); + } + }); + + $('#calendar-rtl-nooverlap').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + initialView: 'day', + firstHour: 0, + year: 2013, + month: 6, // July + date: 31, + events: 'slot_event_overlap.json', + direction: 'rtl', + slotEventOverlap: false, + _eventsPositioned: function() { + testOverlap($('#calendar-rtl-nooverlap'), false, true); + } + }); + + }); + + function testOverlap(el, allowOverlap, isRtl) { + if (_testOverlap(el, allowOverlap, isRtl)) { + el.prev('h2').find('span').css('color', 'green').text('passed'); + } + else { + el.prev('h2').find('span').css('color', 'red').text('failed'); + } + } + + function _testOverlap(el, allowOverlap, isRtl) { + + var events = el.find('.fc-event'); + + var cell = el.find('.fc-slot0 td'); + var cellLeft = Math.round(cell.offset().left); + var cellWidth = Math.round(cell.outerWidth()); + + if (!events.length) { // json events probably couldn't load + console.log('need to run this from a real web server'); + return false; + } + + for (var i=0; i<events.length; i++) { + + var event = $(events[i]); + var offset = event.offset(); + var top = Math.ceil(offset.top); + var left = Math.ceil(offset.left); + var width = Math.floor(event.outerWidth()); + var height = Math.floor(event.outerHeight()); + + if (left < cellLeft || left + width > cellLeft + cellWidth) { + console.log('event is out of bounds', event[0]); + return false; + } + + if (width < 10 || height < 10) { + console.log('event is suprisingly small', event[0]); + return false; + } + + if (allowOverlap) { + width /= 2; // make sure nothing overlaps the first half of the event + if (isRtl) { + left += width; + } + } + + for (var j=i+1; j<events.length; j++) { + // only test again events that are ahead in the DOM, meaning they have a higher + // implicit z-index and an top of the current event + + var otherEvent = $(events[j]); + var otherOffset = otherEvent.offset(); + var otherTop = Math.ceil(otherOffset.top); + var otherLeft = Math.ceil(otherOffset.left); + var otherWidth = Math.floor(otherEvent.outerWidth()); + var otherHeight = Math.floor(otherEvent.outerHeight()); + + if ( + top < otherTop + otherHeight && + top + height > otherTop && + left < otherLeft + otherWidth && + left + width > otherLeft + ) { + console.log('failed on', event[0], otherEvent[0]); + return false; // a collision + } + } + } + + if (!allowOverlap) { + // we know of certain events that should have the same width + // because they share a liquid area... + + var equalwidth1 = events.filter('.equalwidth1'); + if (equalwidth1.eq(0).outerWidth() - equalwidth1.eq(1).outerWidth() > 1) { + console.log('not equal width', equalwidth1.toArray()); + return false; + } + + var equalwidth2 = events.filter('.equalwidth2'); + if (equalwidth2.eq(0).outerWidth() - equalwidth2.eq(1).outerWidth() > 1) { + console.log('not equal width', equalwidth2.toArray()); + return false; + } + + } + + return true; + } + + // TODO: add tests for viewing events in week-mode also + +</script> +<style> + + body { + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + .calendar { + width: 900px; + margin: 50px auto; + } + +</style> +</head> +<body> + +<h2>Default: <span></span></h2> +<div id='calendar' class='calendar'></div> + +<h2>No overlap: <span></span></h2> +<div id='calendar-nooverlap' class='calendar'></div> + +<h2>RTL: <span></span></h2> +<div id='calendar-rtl' class='calendar'></div> + +<h2>RTL, no overlap: <span></span></h2> +<div id='calendar-rtl-nooverlap' class='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/slot_event_overlap.json b/fullcalendar-main/tests/manual/old/slot_event_overlap.json new file mode 100644 index 0000000..af99810 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/slot_event_overlap.json @@ -0,0 +1,102 @@ +[ + { + "title": "1", + "start": "2013-07-31T00:30", + "end": "2013-07-31T02:30", + "allDay": false + }, + { + "title": "2", + "start": "2013-07-31T00:30", + "end": "2013-07-31T02:30", + "allDay": false + }, + { + "title": "3", + "start": "2013-07-31T02:30", + "end": "2013-07-31T04:30", + "allDay": false + }, + { + "title": "4", + "start": "2013-07-31T02:30", + "end": "2013-07-31T04:30", + "allDay": false + }, + { + "title": "5", + "start": "2013-07-31T02:30", + "end": "2013-07-31T04:30", + "allDay": false + }, + { + "title": "6", + "start": "2013-07-31T01:00", + "end": "2013-07-31T03:00", + "allDay": false + }, + { + "title": "7", + "start": "2013-07-31T01:30", + "end": "2013-07-31T03:30", + "allDay": false + }, + { + "title": "8", + "start": "2013-07-31T02:50", + "end": "2013-07-31T04:50", + "allDay": false + }, + { + "title": "9", + "start": "2013-07-31T04:30", + "end": "2013-07-31T09:00", + "allDay": false + }, + { + "title": "10", + "start": "2013-07-31T02:00", + "end": "2013-07-31T04:05", + "allDay": false + }, + { + "title": "11", + "start": "2013-07-31T02:50", + "end": "2013-07-31T04:50", + "allDay": false + }, + { + "title": "12", + "start": "2013-07-31T02:50", + "end": "2013-07-31T04:50", + "allDay": false + }, + { + "title": "13", + "start": "2013-07-31T04:00", + "end": "2013-07-31T06:00", + "allDay": false, + "className": "equalwidth1" + }, + { + "title": "14", + "start": "2013-07-31T04:10", + "end": "2013-07-31T06:09", + "allDay": false, + "className": "equalwidth1" + }, + { + "title": "15", + "start": "2013-07-31T06:30", + "end": "2013-07-31T08:30", + "allDay": false, + "className": "equalwidth2" + }, + { + "title": "16", + "start": "2013-07-31T06:30", + "end": "2013-07-31T08:30", + "allDay": false, + "className": "equalwidth2" + } +] \ No newline at end of file diff --git a/fullcalendar-main/tests/manual/old/snap.html b/fullcalendar-main/tests/manual/old/snap.html new file mode 100644 index 0000000..01194ba --- /dev/null +++ b/fullcalendar-main/tests/manual/old/snap.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + + //slotMinutes: 30, // 20 + snapMinutes: 15, // 5 + + selectable: true, + selectMirror: true, + select: function(start, end) { + console.log(start, end); + }, + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/sources.html b/fullcalendar-main/tests/manual/old/sources.html new file mode 100644 index 0000000..9b81f0b --- /dev/null +++ b/fullcalendar-main/tests/manual/old/sources.html @@ -0,0 +1,175 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + var cal; + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + var gcalFeed = FullCalendar.gcalFeed("http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic"); + + var jsonFeed = "../demos/json-events.php"; + + var staticEvents = [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + title: 'Event with \n newline', + start: new Date(y, m, d), + end: new Date(y, m, d) + }, + { + title: 'T event', + start: y + '-06-06T10:20:00', + allDay: false + }, + { + title: 'No T event', + start: y + '-06-06 11:30:00', + allDay: false + }, + { + title: 'O event', + start: y + '-06-06T10:20:00-02:00', + allDay: false + }, + { + title: 'U event', + start: y + '-06-06T14:30:00Z', + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + id: 777, + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false, + //className: 'yellow-event black-text-event' + className: ['yellow-event', 'black-text-event'] + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + }, + { + title: 'Float String Timestamp Event', + start: '1295078400.0' + } + ]; + + var customSource = function(start, end, callback) { + callback([ + { + title: 'FIRST', + start: start + }, + { + title: 'LAST', + start: new Date(end - 1) + } + ]); + }; + + $(document).ready(function() { + cal = $('#calendar').fullCalendar({ + ignoreTimezone: false, + //lazyFetching: false, + editable: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + //events: staticEvents, + eventSources: [ + staticEvents, + jsonFeed, + gcalFeed, + customSource + ], + loading: function(bool) { + if (bool) { + $('#loading').show(); + }else{ + $('#loading').hide(); + } + } + /* + , + startParam: 'mystart', + endParam: 'myend' + */ + }); + }); + +</script> +<style> + + .red-event { + background: red !important; + } + + .yellow-event { + background: yellow !important; + } + + .black-text-event { + color: #000 !important; + } + + button { + font-size: 11px; + } + +</style> +</head> +<body style='font-size:12px'> +<div id='loading' style='position:absolute;top:0;left:0;display:none'>loading...</div> +<p> +<button onclick="cal.fullCalendar('refetchEvents')">refetch</button> +</p> +<div id='calendar' style='width:900px;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/sources_new.html b/fullcalendar-main/tests/manual/old/sources_new.html new file mode 100644 index 0000000..2a3fba7 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/sources_new.html @@ -0,0 +1,191 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + +/* + +(main options) +startParam +endParam +cacheParam +ignoreTimezone +defaultAllDay +editable +eventColor +eventTextColor +eventBorderColor +eventBackgroundColor + +(event source) +startParam +endParam +cacheParam +ignoreTimezone +defaultAllDay +className +editable +color +textColor +borderColor +backgroundColor + +(event) +className +editable +color +textColor +borderColor +backgroundColor + +*/ + +$(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + + editable: true, + //eventStartEditable: false, + //eventDurationEditable: false, + + selectable: true, + selectMirror: true, + eventSources: [ + + { + url: 'http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic', + color: 'orange', + className: 'google-calendar', + success: function(events) { + console.log('successfully loaded google-calendar event data!', events); + }, + editable: true + }, + + /* + FullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic', { + color: 'orange', + className: 'google-calendar' + }), + */ + + { + url: "../demos/json-events.php", + //editable: false, + color: 'red', + data: { + something: 'cool' + }, + success: function() { + console.log('json-events.php is done!!!', arguments); + } + }, + + { + color: 'purple', + //editable: false, + //startEditable: false, + //durationEditable: false, + events: [ + { + title: 'All Day Event', + //startEditable: false, + //durationEditable: false, + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + } + ] + }, + + { + events: [ + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + } + + ], + eventClick: function(event) { + if (event.url) { + window.open(event.url); + } + return false; + } + }); + +}); + +</script> +<style> + +body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + +#calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/stacking.html b/fullcalendar-main/tests/manual/old/stacking.html new file mode 100644 index 0000000..2162429 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/stacking.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + + year: 2010, + month: 10, + date: 14, + + editable: true, + + //direction: 'rtl', + //hiddenDays: [ 3 ], + + /* + // check to make sure there is no empty space where event should be + eventRender: function(event) { + if (event.title == 'event4') { + return false; + } + }, + */ + + /* + // KNOWN BUG: there shoulnd't be an empty space + eventRender: function(event) { + if (event.title == 'timed event 1') { + return false; + } + }, + */ + + events: [ + { + title: 'event1', + start: '2010-11-15', + end: '2010-11-19' + }, + { + title: 'event2 with a really long title that wraps', + start: '2010-11-15' + }, + { + title: 'event3', + start: '2010-11-17' + }, + { + title: 'event4', + start: '2010-11-18', + end: '2010-11-19' + }, + { + title: 'event5', + start: '2010-11-18' + }, + { + title: 'event6 with a long title dude', + start: '2010-11-25' + }, + { + title: 'event7', + start: '2010-11-26' + }, + { + title: 'timed event 1', + start: '2010-11-16T08:30:00', + end: '2010-11-16T16:00:00', + allDay: false + }, + { + title: 'timed event 2', + start: '2010-11-16T09:30:00', + end: '2010-11-16T11:30:00', + allDay: false + }, + { + title: 'Long Event', // this allday event should be above... + start: '2010-11-22', + end: '2010-11-22' + }, + { + title: 'Birthday Party', // ...this timed event + start: '2010-11-22T19:00:00', + end: '2010-11-22T22:30:00', + allDay: false + }, + { + title: 'An event', // this should be below the allday event + start: '2010-11-22T00:00:00', + allDay: false + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/touch-firing.html b/fullcalendar-main/tests/manual/old/touch-firing.html new file mode 100644 index 0000000..ea22ca5 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/touch-firing.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script> + + $(function() { + + $(document) + .on('mousedown', function(ev) { + console.log('mousedown'); //, ev.target); + }) + .on('mouseup', function(ev) { + console.log('mouseup'); //, ev.target); + }) + .on('mousemove', function(ev) { + console.log('mousemove'); //, ev.target); + }) + .on('mouseover', function(ev) { + console.log('mouseover'); //, ev.target); + }) + .on('mouseout', function(ev) { + console.log('mouseout'); //, ev.target); + }) + .on('click', function(ev) { + console.log('click'); //, ev.target); + }) + .on('touchstart', function(ev) { + console.log('touchstart'); //, ev.target); + }) + .on('touchend', function(ev) { + console.log('touchend'); //, ev.target); + }) + .on('touchmove', function(ev) { + console.log('touchmove'); //, ev.target); + //ev.preventDefault(); + }); + + $('#scroll') + .on('scroll', function(ev) { + console.log('scroll'); //, ev.target); + }); + + /* + + tap: + +touchstart + (delay) + +touchend + (delay) + +mousemove + +mousedown + +mouseup + +click + + */ + + }); + +</script> +<style> + +</style> +</head> +<body> + + <div id='scroll' style='width:400px;height:400px;overflow:auto;border:1px solid #000;float:left'> + <div style='width:800px;height:800px'> + test + </div> + </div> + + <a href='#' style='float:left;margin-left:1em'>this is a link</a> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/touch.html b/fullcalendar-main/tests/manual/old/touch.html new file mode 100644 index 0000000..0f9e687 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/touch.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + $('#calendar').fullCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek' + }, + height: 500, + selectable: true, + selectMirror: true, + initialView: 'week', + initialDate: '2016-01-12', + editable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2016-01-01' + }, + { + title: 'Long Event', + start: '2016-01-07', + end: '2016-01-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2016-01-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2016-01-16T16:00:00' + }, + { + title: 'Conference', + start: '2016-01-11', + end: '2016-01-13' + }, + { + title: 'Meeting', + start: '2016-01-12T10:30:00', + end: '2016-01-12T12:30:00' + }, + { + title: 'Lunch', + start: '2016-01-12T12:00:00' + }, + { + title: 'Meeting', + start: '2016-01-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2016-01-12T17:30:00' + }, + { + title: 'Dinner', + start: '2016-01-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2016-01-13T07:00:00', + end: '2016-01-14T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2016-01-28' + } + ] + }); + + $(document) + .on('touchstart', function() { + console.log('touchstart'); + }) + .on('touchend', function() { + console.log('touchend'); + }) + .on('mousemove', function() { + console.log('mousemove'); + }) + .on('mousedown', function() { + console.log('mousedown'); + }) + .on('mouseup', function() { + console.log('mouseup'); + }) + .on('click', function() { + console.log('click'); + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 13px; + } + + #calendar { + width: 900px; + margin: 40px auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/triggers.html b/fullcalendar-main/tests/manual/old/triggers.html new file mode 100644 index 0000000..cd69247 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/triggers.html @@ -0,0 +1,221 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + /* + TODO: + this file demonstrates a bug, unrelated to triggers, where starting off in a small window, + changing to week, maximizing the window, switching to month causes the last column of + events to be misaligned (don't stretch all the way right) + this happens due to week being taller than month before the switch back + */ + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $(document).ready(function() { + $('#calendar').fullCalendar({ + //weekends: false, + //initialView: 'week', + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + //direction: 'rtl', + + viewDisplay: function(view) { + console.log('viewDisplay'); + console.log(view.activeStart + ' - ' + view.activeEnd); + console.log(view.currentStart + ' - ' + view.currentEnd); + //console.log(view); + //console.log(this); + }, + + _eventsPositioned: function(view) { + console.log('all rendered', view); + }, + + //loading: // see sources.html + + windowResize: function(view) { + console.log('windowResize - ' + view.title); + //console.log(this); + }, + + slotMinTime: '5', + slotMaxTime: '21:30', + dayClick: function(dayDate, allDay, ev, view) { + //alert(dayDate); + console.log('dayClick - ' + dayDate + ', allDay:' + allDay + ' - ' + view.title); + //console.log(ev); + console.log(this); + }, + + selectable: true, + select: function(start, end, allDay) { + console.log('select', start, end, allDay); + }, + unselect: function() { + console.log('unselect'); + }, + + eventRender: function(event, element, view) { + if (event.id == 888) { + return false; + } + else if (event.id == 777) { + return $("<div style='background:green'>").text(event.title); + } + else if (event.id == 999) { + element.css('border-color', 'red'); + //console.log('renderEvent (' + event.title + ') - ' + view.title); + } + }, + eventPositioned: function(event, element, view) { + //console.log('after render for "' + event.title + '":'); + //console.log(element); + }, + + eventClick: function(event, jsEvent, view) { + console.log('EVENT CLICK ' + event.title); + //console.log(jsEvent); + //console.log(view); + //console.log(this); + //return false; + }, + + + eventMouseover: function(event, jsEvent, view) { + console.log('MOUSEOVER ' + event.title); + //console.log(jsEvent); + //console.log(view); + //console.log(this); + }, + eventMouseout: function(event, jsEvent, view) { + console.log('MOUSEOUT ' + event.title); + //console.log(jsEvent); + //console.log(view); + //console.log(this); + }, + + + eventDragStart: function(event, jsEvent, ui, view) { + console.log('DRAG START ' + event.title); + console.log(this); + }, + eventDragStop: function(event, jsEvent, ui, view) { + console.log('DRAG STOP ' + event.title); + console.log(this); + }, + eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view) { + console.log('DROP ' + event.title); + console.log(dayDelta + ' days'); + console.log(minuteDelta + ' minutes'); + console.log('allday: ' + allDay); + //setTimeout(function() { + // revertFunc(); + //}, 2000); + //console.log(jsEvent); + //console.log(ui); + //console.log(view.title); + //console.log(this); + }, + + eventResizeStart: function(event, jsEvent, ui, view) { + console.log('RESIZE START ' + event.title); + //console.log(this); + }, + eventResizeStop: function(event, jsEvent, ui, view) { + console.log('RESIZE STOP ' + event.title); + //console.log(this); + }, + eventResize: function(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view) { + console.log('RESIZE!! ' + event.title); + console.log(dayDelta + ' days'); + console.log(minuteDelta + ' minutes'); + //setTimeout(function() { + // revertFunc(); + //}, 2000); + //console.log(jsEvent); + //console.log(ui); + //console.log(view.title); + //console.log(this); + }, + + /* for testing _eventsPositioned + events: { + url: "http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic", + editable: true, + className: 'holiday' + }, + */ + + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + id: 888, + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + id: 777, + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + }); + +</script> +</head> +<body style='font-size:12px'> +<div id='calendar' style='width:75%;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/triggers_view.html b/fullcalendar-main/tests/manual/old/triggers_view.html new file mode 100644 index 0000000..6d4b52d --- /dev/null +++ b/fullcalendar-main/tests/manual/old/triggers_view.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script src='../../dist/plugins/google-calendar.js'></script> +<script> + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + var verbose = false; + + $(document).ready(function() { + $('#calendar').fullCalendar({ + + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + editable: true, + selectable: true, + + eventDestroy: function(event, element) { + console.log('eventDestroy'); + //alert('eventDestroy'); + if (verbose) { + console.log(' this', this); + console.log(' event', event); + console.log(' element', element); + } + }, + + eventSources: [ + { + url: "http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic", + editable: true, + className: 'holiday' + }, + { + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + id: 888, + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + id: 777, + title: 'Lunch', + start: new Date(y, m, d, 12, 0), + end: new Date(y, m, d, 14, 0), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + } + ] + }); + }); + + + function hyperJump() {console.log('hyperJump'); + $('#calendar').fullCalendar('changeView', 'week'); + //alert('pause'); + $('#calendar').fullCalendar('gotoDate', new Date(y, m+2, d)); + } + +</script> +</head> +<body style='font-size:14px'> +<button onclick='hyperJump()'>changeView+gotoDate</button> +<div id='calendar' style='width:75%;margin:20px auto 0;font-family:arial'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/week_numbers.html b/fullcalendar-main/tests/manual/old/week_numbers.html new file mode 100644 index 0000000..c30ce35 --- /dev/null +++ b/fullcalendar-main/tests/manual/old/week_numbers.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html> +<head> +<link href='../../dist/fullcalendar.css' rel='stylesheet' /> +<link href='../../dist/fullcalendar.print.css' rel='stylesheet' media='print' /> +<script src='../../node_modules/jquery/dist/jquery.js'></script> +<script src='../../node_modules/moment/moment.js'></script> +<script src='../../dist/fullcalendar.js'></script> +<script> + + $(document).ready(function() { + + var date = new Date(); + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + + $('#calendar').fullCalendar({ + + weekNumbers: true, + weekText: 'Wk', + + //weekNumberCalculation: function(date) { + // return date.getMonth(); // inappropriate. but just for testing + //}, + + firstDay: 1, + selectable: true, + //direction: 'rtl', + + editable: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'month,week,dayGridWeek,day,dayGridDay' + }, + events: [ + { + title: 'All Day Event', + start: new Date(y, m, 1) + }, + { + title: 'Long Event', + start: new Date(y, m, d-5), + end: new Date(y, m, d-2) + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d-3, 16, 0), + allDay: false + }, + { + groupId: 999, + title: 'Repeating Event', + start: new Date(y, m, d+4, 16, 0), + allDay: false + }, + { + title: 'Meeting', + start: new Date(y, m, d, 10, 30), + allDay: false + }, + { + title: 'Lunch', + start: new Date(y, m, d, 12, 5), + end: new Date(y, m, d, 14, 43), + allDay: false + }, + { + title: 'Birthday Party', + start: new Date(y, m, d+1, 19, 0), + end: new Date(y, m, d+1, 22, 30), + allDay: false + }, + { + title: 'Click for Google', + start: new Date(y, m, 28), + end: new Date(y, m, 29), + url: 'http://google.com/' + } + ] + }); + + }); + +</script> +<style> + + body { + margin-top: 40px; + text-align: center; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + } + + #calendar { + width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> +<div id='calendar'></div> +</body> +</html> diff --git a/fullcalendar-main/tests/manual/old/window-resize-dynamic-aspectRatio.html b/fullcalendar-main/tests/manual/old/window-resize-dynamic-aspectRatio.html new file mode 100644 index 0000000..08e854d --- /dev/null +++ b/fullcalendar-main/tests/manual/old/window-resize-dynamic-aspectRatio.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../dist/core/main.js'></script> +<script src='../../dist/interaction/main.js'></script> +<script src='../../dist/daygrid/main.js'></script> +<script> + + // from https://github.com/fullcalendar/fullcalendar/issues/4506 + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar') + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth' + }, + windowResize: function() { + var ratio = calendarRatio() + console.log('set ratio', ratio) + calendar.setOption('aspectRatio', ratio) + }, + aspectRatio: calendarRatio() + }) + + calendar.render() + + function calendarRatio() { + return Math.random() * 10 + } + + }) + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/php/get-events.php b/fullcalendar-main/tests/manual/php/get-events.php new file mode 100644 index 0000000..e4d7654 --- /dev/null +++ b/fullcalendar-main/tests/manual/php/get-events.php @@ -0,0 +1,50 @@ +<?php + +//-------------------------------------------------------------------------------------------------- +// This script reads event data from a JSON file and outputs those events which are within the range +// supplied by the "start" and "end" GET parameters. +// +// An optional "timeZone" GET parameter will force all ISO8601 date stings to a given timeZone. +// +// Requires PHP 5.2.0 or higher. +//-------------------------------------------------------------------------------------------------- + +// Require our Event class and datetime utilities +require dirname(__FILE__) . '/utils.php'; + +// Short-circuit if the client did not give us a date range. +if (!isset($_GET['start']) || !isset($_GET['end'])) { + die("Please provide a date range."); +} + +// Parse the start/end parameters. +// These are assumed to be ISO8601 strings with no time nor timeZone, like "2013-12-29". +// Since no timeZone will be present, they will parsed as UTC. +$range_start = parseDateTime($_GET['start']); +$range_end = parseDateTime($_GET['end']); + +// Parse the timeZone parameter if it is present. +$time_zone = null; +if (isset($_GET['timeZone'])) { + $time_zone = new DateTimeZone($_GET['timeZone']); +} + +// Read and parse our events JSON file into an array of event data arrays. +$json = file_get_contents(dirname(__FILE__) . '/../json/events.json'); +$input_arrays = json_decode($json, true); + +// Accumulate an output array of event data arrays. +$output_arrays = array(); +foreach ($input_arrays as $array) { + + // Convert the input array into a useful Event object + $event = new Event($array, $time_zone); + + // If the event is in-bounds, add it to the output + if ($event->isWithinDayRange($range_start, $range_end)) { + $output_arrays[] = $event->toArray(); + } +} + +// Send JSON to the client. +echo json_encode($output_arrays); diff --git a/fullcalendar-main/tests/manual/php/get-time-zones.php b/fullcalendar-main/tests/manual/php/get-time-zones.php new file mode 100644 index 0000000..241e1bd --- /dev/null +++ b/fullcalendar-main/tests/manual/php/get-time-zones.php @@ -0,0 +1,9 @@ +<?php + +//-------------------------------------------------------------------------------------------------- +// This script outputs a JSON array of all timezones (like "America/Chicago") that PHP supports. +// +// Requires PHP 5.2.0 or higher. +//-------------------------------------------------------------------------------------------------- + +echo json_encode(DateTimeZone::listIdentifiers()); \ No newline at end of file diff --git a/fullcalendar-main/tests/manual/php/utils.php b/fullcalendar-main/tests/manual/php/utils.php new file mode 100644 index 0000000..aa67cda --- /dev/null +++ b/fullcalendar-main/tests/manual/php/utils.php @@ -0,0 +1,130 @@ +<?php + +//-------------------------------------------------------------------------------------------------- +// Utilities for our event-fetching scripts. +// +// Requires PHP 5.2.0 or higher. +//-------------------------------------------------------------------------------------------------- + +// PHP will fatal error if we attempt to use the DateTime class without this being set. +date_default_timezone_set('UTC'); + + +class Event { + + // Tests whether the given ISO8601 string has a time-of-day or not + const ALL_DAY_REGEX = '/^\d{4}-\d\d-\d\d$/'; // matches strings like "2013-12-29" + + public $title; + public $allDay; // a boolean + public $start; // a DateTime + public $end; // a DateTime, or null + public $properties = array(); // an array of other misc properties + + + // Constructs an Event object from the given array of key=>values. + // You can optionally force the timeZone of the parsed dates. + public function __construct($array, $timeZone=null) { + + $this->title = $array['title']; + + if (isset($array['allDay'])) { + // allDay has been explicitly specified + $this->allDay = (bool)$array['allDay']; + } + else { + // Guess allDay based off of ISO8601 date strings + $this->allDay = preg_match(self::ALL_DAY_REGEX, $array['start']) && + (!isset($array['end']) || preg_match(self::ALL_DAY_REGEX, $array['end'])); + } + + if ($this->allDay) { + // If dates are allDay, we want to parse them in UTC to avoid DST issues. + $timeZone = null; + } + + // Parse dates + $this->start = parseDateTime($array['start'], $timeZone); + $this->end = isset($array['end']) ? parseDateTime($array['end'], $timeZone) : null; + + // Record misc properties + foreach ($array as $name => $value) { + if (!in_array($name, array('title', 'allDay', 'start', 'end'))) { + $this->properties[$name] = $value; + } + } + } + + + // Returns whether the date range of our event intersects with the given all-day range. + // $rangeStart and $rangeEnd are assumed to be dates in UTC with 00:00:00 time. + public function isWithinDayRange($rangeStart, $rangeEnd) { + + // Normalize our event's dates for comparison with the all-day range. + $eventStart = stripTime($this->start); + + if (isset($this->end)) { + $eventEnd = stripTime($this->end); // normalize + } + else { + $eventEnd = $eventStart; // consider this a zero-duration event + } + + // Check if the two whole-day ranges intersect. + return $eventStart < $rangeEnd && $eventEnd >= $rangeStart; + } + + + // Converts this Event object back to a plain data array, to be used for generating JSON + public function toArray() { + + // Start with the misc properties (don't worry, PHP won't affect the original array) + $array = $this->properties; + + $array['title'] = $this->title; + + // Figure out the date format. This essentially encodes allDay into the date string. + if ($this->allDay) { + $format = 'Y-m-d'; // output like "2013-12-29" + } + else { + $format = 'c'; // full ISO8601 output, like "2013-12-29T09:00:00+08:00" + } + + // Serialize dates into strings + $array['start'] = $this->start->format($format); + if (isset($this->end)) { + $array['end'] = $this->end->format($format); + } + + return $array; + } + +} + + +// Date Utilities +//---------------------------------------------------------------------------------------------- + + +// Parses a string into a DateTime object, optionally forced into the given timeZone. +function parseDateTime($string, $timeZone=null) { + $date = new DateTime( + $string, + $timeZone ? $timeZone : new DateTimeZone('UTC') + // Used only when the string is ambiguous. + // Ignored if string has a timeZone offset in it. + ); + if ($timeZone) { + // If our timeZone was ignored above, force it. + $date->setTimezone($timeZone); + } + return $date; +} + + +// Takes the year/month/date values of the given DateTime and converts them to a new DateTime, +// but in UTC. +function stripTime($datetime) { + return new DateTime($datetime->format('Y-m-d')); +} diff --git a/fullcalendar-main/tests/manual/rrule.html b/fullcalendar-main/tests/manual/rrule.html new file mode 100644 index 0000000..06e4d9c --- /dev/null +++ b/fullcalendar-main/tests/manual/rrule.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://cdn.jsdelivr.net/npm/rrule@2.6.6/dist/es5/rrule.js'></script> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/interaction/dist/index.global.js'></script> +<script src='../../packages/rrule/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2020-09-12', + editable: true, + events: [ + { + title: 'rrule event', + rrule: { + dtstart: '2020-09-09T13:00:00', + // until: '2020-09-01', + freq: 'weekly' + }, + duration: '02:00' + } + ], + eventClick: function(arg) { + if (confirm('delete event?')) { + arg.event.remove() + } + } + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/theming.html b/fullcalendar-main/tests/manual/theming.html new file mode 100644 index 0000000..710ecb3 --- /dev/null +++ b/fullcalendar-main/tests/manual/theming.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> + +<!-- for Bootstrap 5 --> +<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet"> + +<!-- for Bootstrap 4 --> +<link href='https://use.fontawesome.com/releases/v5.0.6/css/all.css' rel='stylesheet'> + +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/interaction/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<script src='../../packages/bootstrap5/dist/index.global.js'></script> + +<script src='js/theme-chooser.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + var calendar; + + initThemeChooser({ + + init: function(themeSystem) { + calendar = new FullCalendar.Calendar(calendarEl, { + themeSystem: themeSystem, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth' + }, + initialDate: '2020-09-12', + weekNumbers: true, + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + nowIndicator: true, + dayMaxEvents: true, // allow "more" link when too many events + // showNonCurrentDates: false, + events: [ + { + title: 'All Day Event', + start: '2020-09-01' + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + calendar.render(); + }, + + change: function(themeSystem) { + calendar.setOption('themeSystem', themeSystem); + } + + }); + + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-size: 14px; + } + + #top, + #calendar.fc-theme-standard { + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + } + + #calendar.fc-theme-bootstrap { + font-size: 14px; + } + + #top { + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + font-size: 12px; + color: #000; + } + + #top .selector { + display: inline-block; + margin-right: 10px; + } + + #top select { + font: inherit; /* mock what Boostrap does, don't compete */ + } + + .left { float: left } + .right { float: right } + .clear { clear: both } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='top'> + + <div class='left'> + + <div id='theme-system-selector' class='selector'> + Theme System: + <select> + <option value='bootstrap5' selected>Bootstrap 5</option> + <option value='bootstrap'>Bootstrap 4</option> + <option value='standard'>unthemed</option> + </select> + </div> + + <div data-theme-system="bootstrap,bootstrap5" class='selector' style='display:none'> + Theme Name: + <select> + <option value='' selected>Default</option> + <option value='cerulean'>Cerulean</option> + <option value='cosmo'>Cosmo</option> + <option value='cyborg'>Cyborg</option> + <option value='darkly'>Darkly</option> + <option value='flatly'>Flatly</option> + <option value='journal'>Journal</option> + <option value='litera'>Litera</option> + <option value='lumen'>Lumen</option> + <option value='lux'>Lux</option> + <option value='materia'>Materia</option> + <option value='minty'>Minty</option> + <option value='pulse'>Pulse</option> + <option value='sandstone'>Sandstone</option> + <option value='simplex'>Simplex</option> + <option value='sketchy'>Sketchy</option> + <option value='slate'>Slate</option> + <option value='solar'>Solar</option> + <option value='spacelab'>Spacelab</option> + <option value='superhero'>Superhero</option> + <option value='united'>United</option> + <option value='yeti'>Yeti</option> + </select> + </div> + + <span id='loading' style='display:none'>loading theme...</span> + + </div> + + <div class='right'> + <span class='credits' data-credit-id='bootstrap-standard' style='display:none'> + <a href='https://getbootstrap.com/docs/3.3/' target='_blank'>Theme by Bootstrap</a> + </span> + <span class='credits' data-credit-id='bootstrap-custom' style='display:none'> + <a href='https://bootswatch.com/' target='_blank'>Theme by Bootswatch</a> + </span> + </div> + + <div class='clear'></div> + </div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/time-zones.html b/fullcalendar-main/tests/manual/time-zones.html new file mode 100644 index 0000000..b594c28 --- /dev/null +++ b/fullcalendar-main/tests/manual/time-zones.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../bundle/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var initialTimeZone = 'local'; + var timeZoneSelectorEl = document.getElementById('time-zone-selector'); + var loadingEl = document.getElementById('loading'); + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + timeZone: initialTimeZone, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: '2020-09-12', + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + dayMaxEvents: true, // allow "more" link when too many events + events: { + url: 'php/get-events.php', + failure: function() { + document.getElementById('script-warning').style.display = 'inline'; // show + } + }, + loading: function(bool) { + if (bool) { + loadingEl.style.display = 'inline'; // show + } else { + loadingEl.style.display = 'none'; // hide + } + }, + + eventTimeFormat: { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }, + + dateClick: function(arg) { + console.log('dateClick', calendar.formatIso(arg.date)); + }, + select: function(arg) { + console.log('select', calendar.formatIso(arg.start), calendar.formatIso(arg.end)); + } + }); + + calendar.render(); + + // load the list of available timezones, build the <select> options + // it's HIGHLY recommended to use a different library for network requests, not this internal util func + FullCalendar.requestJson('GET', 'php/get-time-zones.php', {}, function(timeZones) { + + timeZones.forEach(function(timeZone) { + var optionEl; + + if (timeZone !== 'UTC') { // UTC is already in the list + optionEl = document.createElement('option'); + optionEl.value = timeZone; + optionEl.innerText = timeZone; + timeZoneSelectorEl.appendChild(optionEl); + } + }); + }, function() { + // TODO: handle error + }); + + // when the timezone selector changes, dynamically change the calendar option + timeZoneSelectorEl.addEventListener('change', function() { + calendar.setOption('timeZone', this.value); + }); + }); + +</script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #top { + background: #eee; + border-bottom: 1px solid #ddd; + padding: 0 10px; + line-height: 40px; + font-size: 12px; + } + .left { float: left } + .right { float: right } + .clear { clear: both } + + #script-warning, #loading { display: none } + #script-warning { font-weight: bold; color: red } + + #calendar { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + + .tzo { + color: #000; + } + +</style> +</head> +<body> + + <div id='top'> + + <div class='left'> + Timezone: + <select id='time-zone-selector'> + <option value='local' selected>local</option> + <option value='UTC'>UTC</option> + </select> + </div> + + <div class='right'> + <span id='loading'>loading...</span> + <span id='script-warning'><code>php/get-events.php</code> must be running.</span> + </div> + + <div class='clear'></div> + + </div> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/timegrid-chrome91-bug.html b/fullcalendar-main/tests/manual/timegrid-chrome91-bug.html new file mode 100644 index 0000000..b2942ce --- /dev/null +++ b/fullcalendar-main/tests/manual/timegrid-chrome91-bug.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../bundle/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialView: 'timeGridWeek', + allDaySlot: false, + slotDuration: '00:10', + slotLabelInterval: '00:30', + + timeZone: 'UTC', + editable: true, + initialDate: '2021-06-02', + events: [ + { + resourceId: 'a', + title: 'Timed Event', + start: '2021-06-02T16:00:00+00:00' + }, + { + resourceId: 'b', + title: 'Conference', + start: '2021-06-02' + }, + { + resourceId: 'c', + title: 'Meeting', + start: '2021-06-02T10:30:00+00:00', + end: '2021-06-02T12:30:00+00:00' + }, + { + resourceId: 'a', + title: 'Lunch', + start: '2021-06-02T12:00:00+00:00' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + + /* required to cause the bug + .fc-theme-standard td { + border: none !important; + } + .fc-theme-standard th { + border: none !important; + } + */ + + /* CSS-only fix + .fc-scrollgrid-section-liquid { + height: 1px !important; + } + */ + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/timegrid.html b/fullcalendar-main/tests/manual/timegrid.html new file mode 100644 index 0000000..07d8f7d --- /dev/null +++ b/fullcalendar-main/tests/manual/timegrid.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + initialDate: '2020-09-12', + initialView: 'timeGridWeek', + nowIndicator: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + navLinks: true, // can click day/week names to navigate views + editable: true, + selectable: true, + selectMirror: true, + dayMaxEvents: true, // allow "more" link when too many events + events: [ + { + title: 'All Day Event', + start: '2020-09-01', + }, + { + title: 'Long Event', + start: '2020-09-07', + end: '2020-09-10' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-09T16:00:00' + }, + { + groupId: 999, + title: 'Repeating Event', + start: '2020-09-16T16:00:00' + }, + { + title: 'Conference', + start: '2020-09-11', + end: '2020-09-13' + }, + { + title: 'Meeting', + start: '2020-09-12T10:30:00', + end: '2020-09-12T12:30:00' + }, + { + title: 'Lunch', + start: '2020-09-12T12:00:00' + }, + { + title: 'Meeting', + start: '2020-09-12T14:30:00' + }, + { + title: 'Happy Hour', + start: '2020-09-12T17:30:00' + }, + { + title: 'Dinner', + start: '2020-09-12T20:00:00' + }, + { + title: 'Birthday Party', + start: '2020-09-13T07:00:00' + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2020-09-28' + } + ] + }); + + calendar.render(); + }); + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 1100px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='calendar'></div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/vue2.html b/fullcalendar-main/tests/manual/vue2.html new file mode 100644 index 0000000..852bd10 --- /dev/null +++ b/fullcalendar-main/tests/manual/vue2.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js'></script> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../../contrib/vue2/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function () { + new Vue({ + el: '#app', + data() { + return { + calendarOptions: { + initialView: 'dayGridMonth', + events: [ + { title: 'Nice Event', start: new Date() } + ] + } + } + } + }) + }) + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='app'> + <full-calendar :options='calendarOptions'> + <template v-slot:event-content='arg'> + <b>{{ arg.timeText }}</b> + <i>{{ arg.event.title }}</i> + </template> + </full-calendar> + </div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/vue3.html b/fullcalendar-main/tests/manual/vue3.html new file mode 100644 index 0000000..46c17a5 --- /dev/null +++ b/fullcalendar-main/tests/manual/vue3.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.prod.js'></script> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../../contrib/vue3/dist/index.global.js'></script> +<script> + + document.addEventListener('DOMContentLoaded', function () { + const App = { + data() { + return { + calendarOptions: { + initialView: 'dayGridMonth', + events: [ + { title: 'Nice Event', start: new Date() } + ] + } + } + } + } + + const app = Vue.createApp(App) + app.component('full-calendar', FullCalendar.Vue.default) + app.mount('#app') + }) + +</script> +<style> + + body { + margin: 40px 10px; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar { + max-width: 900px; + margin: 0 auto; + } + +</style> +</head> +<body> + + <div id='app'> + <full-calendar :options='calendarOptions'> + <template v-slot:event-content='arg'> + <b>{{ arg.timeText }}</b> + <i>{{ arg.event.title }}</i> + </template> + </full-calendar> + </div> + +</body> +</html> diff --git a/fullcalendar-main/tests/manual/web-component.html b/fullcalendar-main/tests/manual/web-component.html new file mode 100644 index 0000000..2655c05 --- /dev/null +++ b/fullcalendar-main/tests/manual/web-component.html @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset='utf-8' /> +<script src='../../packages/core/dist/index.global.js'></script> +<script src='../../packages/web-component/dist/index.global.js'></script> +<script src='../../packages/interaction/dist/index.global.js'></script> +<script src='../../packages/daygrid/dist/index.global.js'></script> +<script src='../../packages/timegrid/dist/index.global.js'></script> +<script src='../../packages/list/dist/index.global.js'></script> +<style> + + body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + font-size: 14px; + } + + #calendar-container { + max-width: 1100px; + margin: 40px auto; + padding: 0 10px; + } + +</style> +</head> +<body> + + <div id='calendar-container'> + <full-calendar shadow options='{ + "headerToolbar": { + "left": "prev,next today", + "center": "title", + "right": "dayGridMonth,timeGridWeek,timeGridDay,listWeek" + }, + "initialDate": "2023-01-12", + "editable": true, + "selectable": true, + "businessHours": true, + "dayMaxEvents": true, + "events": [ + { + "title": "All Day Event", + "start": "2023-01-01" + }, + { + "title": "Long Event", + "start": "2023-01-07", + "end": "2023-01-10" + }, + { + "groupId": 999, + "title": "Repeating Event", + "start": "2023-01-09T16:00:00" + }, + { + "groupId": 999, + "title": "Repeating Event", + "start": "2023-01-16T16:00:00" + }, + { + "title": "Conference", + "start": "2023-01-11", + "end": "2023-01-13" + }, + { + "title": "Meeting", + "start": "2023-01-12T10:30:00", + "end": "2023-01-12T12:30:00" + }, + { + "title": "Lunch", + "start": "2023-01-12T12:00:00" + }, + { + "title": "Meeting", + "start": "2023-01-12T14:30:00" + }, + { + "title": "Happy Hour", + "start": "2023-01-12T17:30:00" + }, + { + "title": "Dinner", + "start": "2023-01-12T20:00:00" + }, + { + "title": "Birthday Party", + "start": "2023-01-13T07:00:00" + }, + { + "title": "Click for Google", + "url": "http://google.com/", + "start": "2023-01-28" + } + ] + }' /> + </div> + +</body> +</html> diff --git a/fullcalendar-main/tests/package.json b/fullcalendar-main/tests/package.json new file mode 100644 index 0000000..5a83759 --- /dev/null +++ b/fullcalendar-main/tests/package.json @@ -0,0 +1,118 @@ +{ + "private": true, + "name": "@fullcalendar-tests/standard", + "version": "0.0.0", + "dependencies": { + "@fullcalendar/bootstrap": "~6.1.10", + "@fullcalendar/core": "~6.1.10", + "@fullcalendar/daygrid": "~6.1.10", + "@fullcalendar/google-calendar": "~6.1.10", + "@fullcalendar/icalendar": "~6.1.10", + "@fullcalendar/interaction": "~6.1.10", + "@fullcalendar/list": "~6.1.10", + "@fullcalendar/luxon3": "~6.1.10", + "@fullcalendar/moment": "~6.1.10", + "@fullcalendar/moment-timezone": "~6.1.10", + "@fullcalendar/multimonth": "~6.1.10", + "@fullcalendar/rrule": "~6.1.10", + "@fullcalendar/timegrid": "~6.1.10", + "fullcalendar": "~6.1.10", + "luxon": "^2.0.0", + "moment": "^2.29.1", + "moment-timezone": "^0.5.40", + "xhr-mock": "^2.5.1" + }, + "devDependencies": { + "@fullcalendar-scripts/standard": "*", + "@types/jasmine": "^3.3.12", + "@types/jasmine-jquery": "^1.5.33", + "@types/jquery": "^3.3.29", + "fetch-mock": "^9.11.0", + "handlebars": "^4.7.7" + }, + "scripts": { + "build": "standard-scripts pkg:build", + "test": "standard-scripts pkg:test", + "test:dev": "standard-scripts pkg:test --dev", + "clean": "standard-scripts pkg:clean", + "lint": "eslint ." + }, + "type": "module", + "tsConfig": { + "extends": "@fullcalendar-scripts/standard/config/tsconfig.browser.json", + "compilerOptions": { + "types": [ + "jasmine", + "jasmine-jquery", + "jquery" + ], + "rootDir": "./src", + "outDir": "./dist/.tsout" + }, + "include": [ + "./src/**/*" + ] + }, + "buildConfig": { + "exports": { + ".": { + "iife": true, + "iifeGenerator": "./scripts/generate-index-iife.js" + } + }, + "iifeGlobals": { + "*": "" + } + }, + "karmaConfig": { + "suites": { + "default": { + "files": [ + "./dist/index.global.js" + ] + }, + "pkg:global:locale": { + "files": [ + "./node_modules/@fullcalendar/core/index.global.js", + "./node_modules/@fullcalendar/core/locales/ar.global.js", + "./node_modules/@fullcalendar/daygrid/index.global.js", + "./src/global-locale.js" + ] + }, + "pkg:global:locales-all": { + "files": [ + "./node_modules/@fullcalendar/core/index.global.js", + "./node_modules/@fullcalendar/core/locales-all.global.js", + "./node_modules/@fullcalendar/daygrid/index.global.js", + "./src/global-locales-all.js" + ] + }, + "bundle:global:locale": { + "files": [ + "./node_modules/fullcalendar/index.global.js", + "./node_modules/@fullcalendar/core/locales/ar.global.js", + "./src/global-locale.js" + ] + }, + "bundle:global:locales-all": { + "files": [ + "./node_modules/fullcalendar/index.global.js", + "./node_modules/@fullcalendar/core/locales-all.global.js", + "./src/global-locales-all.js" + ] + } + } + }, + "exports": { + "./package.json": "./package.json", + "./scripts/*": "./scripts/*.js", + "./lib/*": { + "types": "./dist/.tsout/lib/*.d.ts", + "default": "./dist/.tsout/lib/*.js" + }, + ".": { + "types": "./dist/.tsout/index.d.ts", + "default": "./dist/index.js" + } + } +} diff --git a/fullcalendar-main/tests/scripts/generate-index-iife.js b/fullcalendar-main/tests/scripts/generate-index-iife.js new file mode 100644 index 0000000..c505e19 --- /dev/null +++ b/fullcalendar-main/tests/scripts/generate-index-iife.js @@ -0,0 +1,79 @@ +import { join as joinPaths } from 'path' +import { fileURLToPath } from 'url' +import { readFile } from 'fs/promises' +import handlebars from 'handlebars' +import { execCapture } from '@fullcalendar-scripts/standard/utils/exec' + +/* +TODO: don't always display prefix when doing config.log() +TODO: don't reinit rollup watcher on ANY change. Slow when not using fdescribe technique. +*/ + +const thisPkgDir = joinPaths(fileURLToPath(import.meta.url), '../..') +const templatePath = joinPaths(thisPkgDir, 'src/index.global.js.tpl') + +/* +HACK: watch the transpiled directory, so bundling waits until tsc completes +*/ +export function getWatchPaths(config) { + const transpileDir = joinPaths(config.pkgDir, 'dist/.tsout') + + return [transpileDir, templatePath] +} + +export default async function(config) { + const srcDir = joinPaths(config.pkgDir, 'src') + + // mindepth 2 means subdirectories + let testPaths = await execCapture( + 'find . -mindepth 2 -type f \\( -name \'*.ts\' -or -name \'*.tsx\' \\) -print0 | ' + + 'xargs -0 grep -E "(fdescribe|fit)\\("', + { cwd: srcDir }, + ).then( + (stdout) => strToLines(stdout).map((line) => line.trim().split(':')[0]), + () => { + return [] // TODO: somehow look at stderr string. if empty, simply no testPaths. if populated, real error + }, + ) + + // the `find` command reports multiple matches per file. consolidate duplicates + testPaths = uniqueStrs(testPaths) + + if (testPaths.length) { + config.log( + 'Only test files that have fdescribe/fit:\n' + + testPaths.join('\n'), + ) + } else { + // mindepth 2 means subdirectories + testPaths = strToLines((await execCapture( + 'find . -mindepth 2 -type f \\( -name \'*.ts\' -or -name \'*.tsx\' \\)', + { cwd: srcDir }, + ))) + + config.log(`Using all ${testPaths.length} test files`) + } + + const extensionlessTestPaths = testPaths.map((testPath) => testPath.replace(/\.tsx?$/, '')) + + const templateText = await readFile(templatePath, 'utf8') + const template = handlebars.compile(templateText) + const code = template({ extensionlessTestPaths }) + + return code +} + +function uniqueStrs(strs) { + const map = {} + + for (const str of strs) { + map[str] = true + } + + return Object.keys(map) +} + +function strToLines(str) { + str = str.trim() + return str ? str.split('\n') : [] +} diff --git a/fullcalendar-main/tests/src/date-render/dayNumbers.ts b/fullcalendar-main/tests/src/date-render/dayNumbers.ts new file mode 100644 index 0000000..3791628 --- /dev/null +++ b/fullcalendar-main/tests/src/date-render/dayNumbers.ts @@ -0,0 +1,17 @@ +import arLocale from '@fullcalendar/core/locales/ar' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('dayNumbers', () => { + pushOptions({ + initialDate: '2018-01-01', + }) + + it('respects locale in month view', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + locale: arLocale, + }) + let dayGridViewWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridViewWrapper.getDayNumberText('2018-01-01')).toMatch(/1|١٤?/) // normal 1, or an Arabic 1 + }) +}) diff --git a/fullcalendar-main/tests/src/date-render/slotMinTime.ts b/fullcalendar-main/tests/src/date-render/slotMinTime.ts new file mode 100644 index 0000000..169c122 --- /dev/null +++ b/fullcalendar-main/tests/src/date-render/slotMinTime.ts @@ -0,0 +1,15 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotMinTime', () => { + // root cause of https://github.com/fullcalendar/fullcalendar-vue/issues/88 + it('gets rerendered when changing via resetOptions', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + slotMinTime: '01:00', + }) + let gridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(gridWrapper.getAxisTexts()[0]).toBe('1am') + calendar.setOption('slotMinTime', '09:00') + expect(gridWrapper.getAxisTexts()[0]).toBe('9am') + }) +}) diff --git a/fullcalendar-main/tests/src/date-selection/implicit-unselect.ts b/fullcalendar-main/tests/src/date-selection/implicit-unselect.ts new file mode 100644 index 0000000..302bf19 --- /dev/null +++ b/fullcalendar-main/tests/src/date-selection/implicit-unselect.ts @@ -0,0 +1,51 @@ +describe('implicit unselection', () => { + pushOptions({ + initialView: 'dayGridMonth', + fixedWeekCount: true, + now: '2018-09-11', + }) + + it('happens when dates change', () => { + let selectFired = 0 + let unselectFired = 0 + + initCalendar({ + select() { + selectFired += 1 + }, + unselect() { + unselectFired += 1 + }, + }) + + currentCalendar.select('2018-09-24', '2018-10-03') // will still be visible after .next() + expect(selectFired).toBe(1) + expect(unselectFired).toBe(0) + + currentCalendar.next() + expect(selectFired).toBe(1) + expect(unselectFired).toBe(1) // unselected + }) + + it('happens when view changes', () => { + let selectFired = 0 + let unselectFired = 0 + + initCalendar({ + select() { + selectFired += 1 + }, + unselect() { + unselectFired += 1 + }, + }) + + currentCalendar.select('2018-09-09', '2018-09-14') // will still be visible after view switch + expect(selectFired).toBe(1) + expect(unselectFired).toBe(0) + + currentCalendar.changeView('dayGridWeek') + expect(selectFired).toBe(1) + expect(unselectFired).toBe(1) // unselected + }) +}) diff --git a/fullcalendar-main/tests/src/datelib/formatting-api.ts b/fullcalendar-main/tests/src/datelib/formatting-api.ts new file mode 100644 index 0000000..6434107 --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/formatting-api.ts @@ -0,0 +1,59 @@ +import { formatDate, formatRange } from '@fullcalendar/core' + +describe('formatDate', () => { + it('works with no timezone offset', () => { + let str = formatDate('2018-09-04', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) + expect(str).toBe('September 4, 2018') + }) + + it('works with timezone offset', () => { + let str = formatDate('2018-09-04T00:00:00-05:00', { + month: 'long', + day: 'numeric', + year: 'numeric', + timeZoneName: 'short', + timeZone: 'America/New_York', // but with no named tz implementation + omitCommas: true, // for cross-browser + }) + expect(str.replace(' at ', ' ')) + .toBe('September 4 2018 12:00 AM GMT-5') + }) +}) + +describe('formatRange', () => { + it('works with no timezone offset', () => { + let str = formatRange('2018-09-04', '2018-10-04', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) + expect(str).toBe('September 4 - October 4, 2018') + }) + + it('works with custom separator', () => { + let str = formatRange('2018-09-04', '2018-10-04', { + month: 'long', + day: 'numeric', + year: 'numeric', + separator: ' ... ', + }) + expect(str).toBe('September 4 ... October 4, 2018') + }) + + it('works with timezone offset', () => { + let str = formatRange('2018-09-04T00:00:00-05:00', '2018-10-04T00:00:00-05:00', { + month: 'long', + day: 'numeric', + year: 'numeric', + timeZoneName: 'short', + timeZone: 'America/New_York', // but with no named tz implementation + omitCommas: true, // for cross-browser + }) + expect(str.replace(' at ', ' ')) + .toBe('September 4 - October 4 2018 12:00 AM GMT-5') + }) +}) diff --git a/fullcalendar-main/tests/src/datelib/luxon.ts b/fullcalendar-main/tests/src/datelib/luxon.ts new file mode 100644 index 0000000..ca81bb3 --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/luxon.ts @@ -0,0 +1,197 @@ +import { Calendar } from '@fullcalendar/core' +import esLocale from '@fullcalendar/core/locales/es' +import luxonPlugin, { toLuxonDateTime, toLuxonDuration } from '@fullcalendar/luxon3' +import dayGridPlugin from '@fullcalendar/daygrid' +import { testTimeZoneImpl } from '../lib/timeZoneImpl.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('luxon plugin', () => { + const PLUGINS = [luxonPlugin, dayGridPlugin] // for `new Calendar` + + pushOptions({ // for initCalendar + plugins: PLUGINS, + }) + + testTimeZoneImpl(luxonPlugin) + + describe('toLuxonDateTime', () => { + describe('timezone transfering', () => { + it('transfers UTC', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + timeZone: 'UTC', + }) + let event = calendar.getEvents()[0] + let start = toLuxonDateTime(event.start, calendar) + let end = toLuxonDateTime(event.end, calendar) + expect(start.toISO()).toBe('2018-09-05T12:00:00.000Z') + expect(start.zoneName).toBe('UTC') + expect(end.toISO()).toBe('2018-09-05T18:00:00.000Z') + expect(end.zoneName).toBe('UTC') + }) + + it('transfers local timezone', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + timeZone: 'local', + }) + let event = calendar.getEvents()[0] + let start = toLuxonDateTime(event.start, calendar) + let end = toLuxonDateTime(event.end, calendar) + expect(start.toJSDate()).toEqualLocalDate('2018-09-05T12:00:00') + expect(start.zoneName).toMatch('/') // has a named timezone + expect(end.toJSDate()).toEqualLocalDate('2018-09-05T18:00:00') + expect(end.zoneName).toMatch('/') // has a named timezone + }) + + it('transfers named timezone', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + timeZone: 'Europe/Moscow', + }) + let event = calendar.getEvents()[0] + let start = toLuxonDateTime(event.start, calendar) + let end = toLuxonDateTime(event.end, calendar) + expect(start.toJSDate()).toEqualDate('2018-09-05T12:00:00+03:00') + expect(start.zoneName).toMatch('Europe/Moscow') + expect(end.toJSDate()).toEqualDate('2018-09-05T18:00:00+03:00') + expect(end.zoneName).toMatch('Europe/Moscow') + }) + }) + + it('transfers locale', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + locale: esLocale, + }) + let event = calendar.getEvents()[0] + let datetime = toLuxonDateTime(event.start, calendar) + expect(datetime.locale).toEqual('es') + }) + }) + + describe('toLuxonDuration', () => { + it('converts numeric values correctly', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + defaultTimedEventDuration: '05:00', + defaultAllDayEventDuration: { days: 3 }, + }) + + // hacky way to have a duration parsed + let timedDuration = toLuxonDuration(calendar.getCurrentData().options.defaultTimedEventDuration, calendar) + let allDayDuration = toLuxonDuration(calendar.getCurrentData().options.defaultAllDayEventDuration, calendar) + + expect(timedDuration.as('hours')).toBe(5) + expect(allDayDuration.as('days')).toBe(3) + }) + + it('transfers locale correctly', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + defaultTimedEventDuration: '05:00', + locale: esLocale, + }) + + // hacky way to have a duration parsed + let timedDuration = toLuxonDuration(calendar.getCurrentData().options.defaultTimedEventDuration, calendar) + + expect(timedDuration.locale).toBe('es') + }) + }) + + describe('date formatting', () => { + it('produces event time text', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2018-09-06', + displayEventEnd: false, + eventTimeFormat: 'HH:mm:ss\'abc\'', + events: [ + { title: 'my event', start: '2018-09-06T13:30:20' }, + ], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.timeText).toBe('13:30:20abc') + }) + }) + + describe('range formatting', () => { + it('renders with same month', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2018-09-05', 'MMMM {d}, yyyy \'asdf\'') + expect(s).toEqual('September 3 - 5, 2018 asdf') + + s = calendar.formatRange('2018-09-03', '2018-09-05', '{d} MMMM, yyyy \'asdf\'') + expect(s).toEqual('3 - 5 September, 2018 asdf') + }) + + it('renders with same year but different month', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2018-10-05', '{MMMM {d}}, yyyy \'asdf\'') + expect(s).toEqual('September 3 - October 5, 2018 asdf') + + s = calendar.formatRange('2018-09-03', '2018-10-05', '{{d} MMMM}, yyyy \'asdf\'') + expect(s).toEqual('3 September - 5 October, 2018 asdf') + }) + + it('renders with different years', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2019-10-05', '{MMMM {d}}, yyyy \'asdf\'') + expect(s).toEqual('September 3, 2018 asdf - October 5, 2019 asdf') + + s = calendar.formatRange('2018-09-03', '2019-10-05', '{{d} MMMM}, yyyy \'asdf\'') + expect(s).toEqual('3 September, 2018 asdf - 5 October, 2019 asdf') + }) + + it('renders the same if same day', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03T00:00:00', '2018-09-03T23:59:59', 'MMMM d yyyy') + expect(s).toEqual('September 3 2018') + }) + + it('inherits defaultRangeSeparator', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + defaultRangeSeparator: ' to ', + }) + let s = calendar.formatRange('2018-09-03', '2018-09-05', 'MMMM d, yyyy \'asdf\'') + expect(s).toEqual('September 3, 2018 asdf to September 5, 2018 asdf') + }) + + it('produces title with titleRangeSeparator', () => { + initCalendar({ // need to render the calendar to get view.title :( + plugins: PLUGINS, + initialView: 'dayGridWeek', + now: '2018-09-06', + titleFormat: 'MMMM {d} yy \'yup\'', + titleRangeSeparator: ' to ', + }) + expect(currentCalendar.view.title).toBe('September 2 to 8 18 yup') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/datelib/main.ts b/fullcalendar-main/tests/src/datelib/main.ts new file mode 100644 index 0000000..b747c78 --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/main.ts @@ -0,0 +1,715 @@ +import { Calendar } from '@fullcalendar/core' +import { + DateEnv, + createFormatter, + createDuration, + startOfDay, + diffWholeWeeks, + diffWholeDays, + diffDayAndTime, +} from '@fullcalendar/core/internal' +import dayGridPlugin from '@fullcalendar/daygrid' +import { getDSTDeadZone } from '../lib/dst-dead-zone.js' +import { formatPrettyTimeZoneOffset, formatIsoTimeZoneOffset, formatIsoWithoutTz } from '../lib/datelib-utils.js' + +describe('datelib', () => { + let enLocale + + beforeEach(() => { + enLocale = new Calendar(document.createElement('div'), { // HACK + plugins: [dayGridPlugin], + }).getCurrentData().dateEnv.locale + }) + + describe('computeWeekNumber', () => { + it('works with local', () => { + let env = new DateEnv({ + timeZone: 'UTC', + calendarSystem: 'gregory', + locale: enLocale, + }) + let m1 = env.createMarker('2018-04-07') + let m2 = env.createMarker('2018-04-08') + expect(env.computeWeekNumber(m1)).toBe(14) + expect(env.computeWeekNumber(m2)).toBe(15) + }) + + it('works with ISO', () => { + let env = new DateEnv({ + timeZone: 'UTC', + calendarSystem: 'gregory', + locale: enLocale, + weekNumberCalculation: 'ISO', + }) + let m1 = env.createMarker('2018-04-01') + let m2 = env.createMarker('2018-04-02') + expect(env.computeWeekNumber(m1)).toBe(13) + expect(env.computeWeekNumber(m2)).toBe(14) + }) + + it('works with custom function', () => { + let env = new DateEnv({ + timeZone: 'UTC', + calendarSystem: 'gregory', + locale: enLocale, + weekNumberCalculation(date) { + expect(date instanceof Date).toBe(true) + expect(date.valueOf()).toBe(Date.UTC(2018, 3, 1)) + return 99 + }, + }) + let m1 = env.createMarker('2018-04-01') + expect(env.computeWeekNumber(m1)).toBe(99) + }) + }) + + it('startOfWeek with different firstDay', () => { + let env = new DateEnv({ + timeZone: 'UTC', + calendarSystem: 'gregory', + locale: enLocale, + firstDay: 2, // tues + }) + let m = env.createMarker('2018-04-19') + let w = env.startOfWeek(m) + + expect(env.toDate(w)).toEqual( + new Date(Date.UTC(2018, 3, 17)), + ) + }) + + describe('when UTC', () => { + let env + + beforeEach(() => { + env = new DateEnv({ + timeZone: 'UTC', + calendarSystem: 'gregory', + locale: enLocale, + }) + }) + + describe('createMarker', () => { + it('with date', () => { + expect( + env.toDate( + env.createMarker( + new Date(2017, 5, 8), + ), + ), + ).toEqual( + new Date(2017, 5, 8), + ) + }) + + it('with timestamp', () => { + expect( + env.toDate( + env.createMarker( + new Date(2017, 5, 8).valueOf(), + ), + ), + ).toEqual( + new Date(2017, 5, 8), + ) + }) + + it('with array', () => { + expect( + env.toDate( + env.createMarker( + [2017, 5, 8], + ), + ), + ).toEqual( + new Date(Date.UTC(2017, 5, 8)), + ) + }) + }) + + describe('ISO8601 parsing', () => { + it('parses non-tz as UTC', () => { + let res = env.createMarkerMeta('2018-06-08') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8))) + expect(res.forcedTzo).toBeNull() + }) + + it('parses a date already in UTC', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00Z') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8))) + expect(res.forcedTzo).toBeNull() + }) + + it('parses timezones into UTC', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00+12:00') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 7, 12))) + expect(res.forcedTzo).toBeNull() + }) + + it('detects lack of time', () => { + let res = env.createMarkerMeta('2018-06-08') + expect(res.isTimeUnspecified).toBe(true) + }) + + it('detects presence of time', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00') + expect(res.isTimeUnspecified).toBe(false) + }) + + it('parses a time with no \'T\'', () => { + let res = env.createMarkerMeta('2018-06-08 01:00:00') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8, 1, 0))) + }) + + it('parses just a month', () => { + let res = env.createMarkerMeta('2018-06') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 1))) + }) + + it('detects presence of time even if timezone', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00+12:00') + expect(res.isTimeUnspecified).toBe(false) + }) + }) + + it('outputs ISO8601 formatting', () => { + let marker = env.createMarker('2018-06-08T00:00:00') + let s = env.formatIso(marker) + expect(s).toBe('2018-06-08T00:00:00Z') + }) + + it('outputs pretty format with UTC timezone', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ + weekday: 'long', + day: 'numeric', + month: 'long', + hour: '2-digit', + minute: '2-digit', + year: 'numeric', + timeZoneName: 'short', + omitCommas: true, // for cross-browser + }) + let s = env.format(marker, formatter) + expect(s.replace(' at ', ' ')) + .toBe('Friday June 8 2018 12:00 AM UTC') + }) + + describe('week number formatting', () => { + it('can output only number', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ week: 'numeric' }) + let s = env.format(marker, formatter) + expect(s).toBe('23') + }) + + it('can output narrow', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ week: 'narrow' }) + let s = env.format(marker, formatter) + expect(s).toBe('W23') + }) + + it('can output short', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ week: 'short' }) + let s = env.format(marker, formatter) + expect(s).toBe('W 23') + }) + }) + + describe('range formatting', () => { + let formatter = createFormatter({ + day: 'numeric', + month: 'long', + year: 'numeric', + separator: ' - ', + }) + + it('works with different days of same month', () => { + let m0 = env.createMarker('2018-06-08') + let m1 = env.createMarker('2018-06-09') + let s = env.formatRange(m0, m1, formatter) + expect(s).toBe('June 8 - 9, 2018') + }) + + it('works with different days of same month, with inprecise formatter', () => { + let otherFormatter = createFormatter({ + month: 'long', + year: 'numeric', + }) + let m0 = env.createMarker('2018-06-08') + let m1 = env.createMarker('2018-06-09') + let s = env.formatRange(m0, m1, otherFormatter) + expect(s).toBe('June 2018') + }) + + it('works with different day/month of same year', () => { + let m0 = env.createMarker('2018-06-08') + let m1 = env.createMarker('2018-07-09') + let s = env.formatRange(m0, m1, formatter) + expect(s).toBe('June 8 - July 9, 2018') + }) + + it('works with completely different dates', () => { + let m0 = env.createMarker('2018-06-08') + let m1 = env.createMarker('2020-07-09') + let s = env.formatRange(m0, m1, formatter) + expect(s).toBe('June 8, 2018 - July 9, 2020') + }) + }) + + // date math + + describe('add', () => { + it('works with positives', () => { + let dur = createDuration({ + year: 1, + month: 2, + day: 3, + hour: 4, + minute: 5, + second: 6, + ms: 7, + }) + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12))) + let d1 = env.toDate(env.add(d0, dur)) + expect(d1).toEqual( + new Date(Date.UTC(2019, 7, 8, 16, 5, 6, 7)), + ) + }) + + it('works with negatives', () => { + let dur = createDuration({ + year: -1, + month: -2, + day: -3, + hour: -4, + minute: -5, + second: -6, + millisecond: -7, + }) + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12))) + let d1 = env.toDate(env.add(d0, dur)) + expect(d1).toEqual( + new Date(Date.UTC(2017, 3, 2, 7, 54, 53, 993)), + ) + }) + }) + + // test in Safari! + // https://github.com/fullcalendar/fullcalendar/issues/4363 + it('startOfYear', () => { + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12))) + let d1 = env.toDate(env.startOfYear(d0)) + expect(d1).toEqual( + new Date(Date.UTC(2018, 0, 1)), + ) + }) + + it('startOfMonth', () => { + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12))) + let d1 = env.toDate(env.startOfMonth(d0)) + expect(d1).toEqual( + new Date(Date.UTC(2018, 5, 1)), + ) + }) + + it('startOfWeek', () => { + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12))) + let d1 = env.toDate(env.startOfWeek(d0)) + expect(d1).toEqual( + new Date(Date.UTC(2018, 5, 3)), + ) + }) + + it('startOfDay', () => { + let d0 = env.createMarker(new Date(Date.UTC(2018, 5, 5, 12, 30))) + let d1 = env.toDate(startOfDay(d0)) + expect(d1).toEqual( + new Date(Date.UTC(2018, 5, 5)), + ) + }) + + describe('diffWholeYears', () => { + it('returns null if not whole', () => { + let d0 = new Date(Date.UTC(2018, 5, 5, 12, 0)) + let d1 = new Date(Date.UTC(2020, 5, 5, 12, 30)) + let diff = env.diffWholeYears( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(null) + }) + + it('returns negative', () => { + let d0 = new Date(Date.UTC(2020, 5, 5, 12, 0)) + let d1 = new Date(Date.UTC(2018, 5, 5, 12, 0)) + let diff = env.diffWholeYears( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(-2) + }) + + it('returns positive', () => { + let d0 = new Date(Date.UTC(2018, 5, 5, 12, 0)) + let d1 = new Date(Date.UTC(2020, 5, 5, 12, 0)) + let diff = env.diffWholeYears( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(2) + }) + }) + + describe('diffWholeMonths', () => { + it('returns null if not whole', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2020, 5, 6)) + let diff = env.diffWholeMonths( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(null) + }) + + it('returns negative', () => { + let d0 = new Date(Date.UTC(2020, 9, 5)) + let d1 = new Date(Date.UTC(2018, 5, 5)) + let diff = env.diffWholeMonths( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(-12 * 2 - 4) + }) + + it('returns positive', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2020, 9, 5)) + let diff = env.diffWholeMonths( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(12 * 2 + 4) + }) + }) + + describe('diffWholeWeeks', () => { + it('returns null if not whole', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2018, 5, 20)) + let diff = diffWholeWeeks( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(null) + }) + + it('returns negative', () => { + let d0 = new Date(Date.UTC(2018, 5, 19)) + let d1 = new Date(Date.UTC(2018, 5, 5)) + let diff = diffWholeWeeks( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(-2) + }) + + it('returns positive', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2018, 5, 19)) + let diff = diffWholeWeeks( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(2) + }) + }) + + describe('diffWholeDays', () => { + it('returns null if not whole', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2018, 5, 19, 12)) + let diff = diffWholeDays( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(null) + }) + + it('returns negative', () => { + let d0 = new Date(Date.UTC(2018, 5, 19)) + let d1 = new Date(Date.UTC(2018, 5, 5)) + let diff = diffWholeDays( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(-14) + }) + + it('returns positive', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2018, 5, 19)) + let diff = diffWholeDays( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toBe(14) + }) + }) + + describe('diffDayAndTime', () => { + it('returns negative', () => { + let d0 = new Date(Date.UTC(2018, 5, 19, 12)) + let d1 = new Date(Date.UTC(2018, 5, 5)) + let diff = diffDayAndTime( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toEqual({ + years: 0, + months: 0, + days: -14, + milliseconds: -12 * 60 * 60 * 1000, + }) + }) + + it('returns positive', () => { + let d0 = new Date(Date.UTC(2018, 5, 5)) + let d1 = new Date(Date.UTC(2018, 5, 19, 12)) + let diff = diffDayAndTime( + env.createMarker(d0), + env.createMarker(d1), + ) + expect(diff).toEqual({ + years: 0, + months: 0, + days: 14, + milliseconds: 12 * 60 * 60 * 1000, + }) + }) + }) + }) + + describe('when local', () => { + let env + + beforeEach(() => { + env = new DateEnv({ + timeZone: 'local', + calendarSystem: 'gregory', + locale: enLocale, + }) + }) + + describe('ISO8601 parsing', () => { + it('parses non-tz as local', () => { + let res = env.createMarkerMeta('2018-06-08') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(2018, 5, 8)) + expect(res.forcedTzo).toBeNull() + }) + + it('parses timezones into local', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00+12:00') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 7, 12))) + expect(res.forcedTzo).toBeNull() + }) + + it('does not lose info when parsing a dst-dead-zone date', () => { + let deadZone = getDSTDeadZone() + + if (!deadZone) { + console.log('could not determine DST dead zone') // eslint-disable-line no-console + } else { + // use a utc date to get a ISO8601 string representation of the start of the dead zone + let utcDate = new Date(Date.UTC( + deadZone[1].getFullYear(), + deadZone[1].getMonth(), + deadZone[1].getDate(), + deadZone[1].getHours() - 1, // back one hour. shouldn't exist in local time + deadZone[1].getMinutes(), + deadZone[1].getSeconds(), + deadZone[1].getMilliseconds(), + )) + let s = formatIsoWithoutTz(utcDate) + + // check that the local date falls out of the dead zone + let localDate = new Date(s) + expect(localDate.getHours()).not.toBe(deadZone[1].getHours() - 1) + + // check that is parsed and retained the original hour, + // even tho it falls into the dead zone for local time + let marker = env.createMarker(s) + expect(formatIsoWithoutTz(marker)).toBe(s) + + // TODO + // // when it uses the env to format to local time, + // // it should have jumped out of the dead zone. + // expect(env.formatIso(marker)).not.toMatch(s) + } + }) + }) + + it('outputs ISO8601 formatting', () => { + let marker = env.createMarker('2018-06-08T00:00:00') + let s = env.formatIso(marker) + let realTzo = formatIsoTimeZoneOffset(new Date(2018, 5, 8)) + expect(s).toBe('2018-06-08T00:00:00' + realTzo) + }) + + it('outputs pretty format with local timezone', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + omitCommas: true, // for cross-browser + }) + let s = env.format(marker, formatter) + expect(s.replace(' at ', ' ')) + .toBe('Friday June 8 2018 12:00 AM ' + formatPrettyTimeZoneOffset(new Date(2018, 5, 8))) + }) + + it('can output a timezone only', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ timeZoneName: 'short' }) + let s = env.format(marker, formatter) + expect(s).toBe(formatPrettyTimeZoneOffset(new Date(2018, 5, 8))) + }) + + // because `new Date(year)` is error-prone + it('startOfYear', () => { + let d0 = env.createMarker(new Date(2018, 5, 5, 12)) + let d1 = env.toDate(env.startOfYear(d0)) + expect(d1).toEqual( + new Date(2018, 0, 1), + ) + }) + }) + + describe('when named timezone with coercion', () => { + let env + + beforeEach(() => { + env = new DateEnv({ + timeZone: 'America/Chicago', + calendarSystem: 'gregory', + locale: enLocale, + }) + }) + + describe('ISO8601 parsing', () => { + it('parses non-tz as UTC with no forcedTzo', () => { + let res = env.createMarkerMeta('2018-06-08') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8))) + expect(res.forcedTzo).toBeNull() + }) + + it('parses as UTC after stripping and with a forcedTzo', () => { + let res = env.createMarkerMeta('2018-06-08T00:00:00+12:00') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8))) + expect(res.forcedTzo).toBe(12 * 60) + }) + + it('parses as UTC after stripping and with a forcedTzo, alt format', () => { + let res = env.createMarkerMeta('2018-06-08T01:01:01.100+1200') + let date = env.toDate(res.marker) + expect(date).toEqual(new Date(Date.UTC(2018, 5, 8, 1, 1, 1, 100))) + expect(res.forcedTzo).toBe(12 * 60) + }) + }) + + it('outputs UTC timezone when no timezone specified', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + timeZoneName: 'short', + omitCommas: true, // for cross-browser + }) + let s = env.format(marker, formatter) + expect(s.replace(' at ', ' ')) + .toBe('Friday June 8 2018 12:00 AM UTC') + }) + + it('outputs UTC short timezone when no timezone specified, when requested as long', () => { + let marker = env.createMarker('2018-06-08') + let formatter = createFormatter({ + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + timeZoneName: 'long', + omitCommas: true, // for cross-browser + }) + let s = env.format(marker, formatter) + expect(s.replace(' at ', ' ')) + .toBe('Friday June 8 2018 12:00 AM UTC') + }) + + it('computes current date as local values', () => { + let marker = env.createNowMarker() + let localDate = new Date() + expect(marker.getUTCFullYear()).toBe(localDate.getFullYear()) + expect(marker.getUTCMonth()).toBe(localDate.getMonth()) + expect(marker.getUTCDate()).toBe(localDate.getDate()) + expect(marker.getUTCHours()).toBe(localDate.getHours()) + expect(marker.getUTCMinutes()).toBe(localDate.getMinutes()) + expect(marker.getUTCSeconds()).toBe(localDate.getSeconds()) + }) + }) + + describe('duration parsing', () => { + it('accepts whole day in string', () => { + let dur = createDuration('2.00:00:00') + expect(dur).toEqual({ + years: 0, + months: 0, + days: 2, + milliseconds: 0, + }) + }) + + it('accepts hours, minutes, seconds, and milliseconds', () => { + let dur = createDuration('01:02:03.500') + expect(dur).toEqual({ + years: 0, + months: 0, + days: 0, + milliseconds: + 1 * 60 * 60 * 1000 + + 2 * 60 * 1000 + + 3 * 1000 + + 500, + }) + }) + + it('accepts just hours and minutes', () => { + let dur = createDuration('01:02') + expect(dur).toEqual({ + years: 0, + months: 0, + days: 0, + milliseconds: + 1 * 60 * 60 * 1000 + + 2 * 60 * 1000, + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/datelib/moment-timezone.ts b/fullcalendar-main/tests/src/datelib/moment-timezone.ts new file mode 100644 index 0000000..03b6744 --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/moment-timezone.ts @@ -0,0 +1,6 @@ +import momentTimeZonePlugin from '@fullcalendar/moment-timezone' +import { testTimeZoneImpl } from '../lib/timeZoneImpl.js' + +describe('moment-timezone', () => { + testTimeZoneImpl(momentTimeZonePlugin) +}) diff --git a/fullcalendar-main/tests/src/datelib/moment.ts b/fullcalendar-main/tests/src/datelib/moment.ts new file mode 100644 index 0000000..b8924e1 --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/moment.ts @@ -0,0 +1,176 @@ +import { Calendar } from '@fullcalendar/core' +import momentPlugin, { toMoment, toMomentDuration } from '@fullcalendar/moment' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import 'moment/locale/es' // only test spanish +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('moment plugin', () => { + const PLUGINS = [dayGridPlugin, timeGridPlugin, momentPlugin] + pushOptions({ plugins: PLUGINS }) + + describe('toMoment', () => { + describe('timezone handling', () => { + it('transfers UTC', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: [dayGridPlugin], + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + timeZone: 'UTC', + }) + let event = calendar.getEvents()[0] + let startMom = toMoment(event.start, calendar) + let endMom = toMoment(event.end, calendar) + expect(startMom.format()).toEqual('2018-09-05T12:00:00Z') + expect(endMom.format()).toEqual('2018-09-05T18:00:00Z') + }) + + it('transfers local', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: [dayGridPlugin], + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + timeZone: 'local', + }) + let event = calendar.getEvents()[0] + let startMom = toMoment(event.start, calendar) + let endMom = toMoment(event.end, calendar) + expect(startMom.toDate()).toEqualLocalDate('2018-09-05T12:00:00') + expect(endMom.toDate()).toEqualLocalDate('2018-09-05T18:00:00') + }) + }) + + it('transfers locale', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: [dayGridPlugin], + events: [{ start: '2018-09-05T12:00:00', end: '2018-09-05T18:00:00' }], + locale: 'es', + }) + let event = calendar.getEvents()[0] + let mom = toMoment(event.start, calendar) + expect(mom.locale()).toEqual('es') + }) + }) + + describe('toDuration', () => { + it('converts correctly', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: [dayGridPlugin], + defaultTimedEventDuration: '05:00', + defaultAllDayEventDuration: { days: 3 }, + }) + + // hacky way to have a duration parsed + let timedDuration = toMomentDuration(calendar.getCurrentData().options.defaultTimedEventDuration) + let allDayDuration = toMomentDuration(calendar.getCurrentData().options.defaultAllDayEventDuration) + + expect(timedDuration.asHours()).toBe(5) + expect(allDayDuration.asDays()).toBe(3) + }) + }) + + describe('date formatting', () => { + it('produces event time text', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2018-09-06', + displayEventEnd: false, + eventTimeFormat: 'HH:mm:ss[!]', + events: [ + { title: 'my event', start: '2018-09-06T13:30:20' }, + ], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.timeText).toBe('13:30:20!') + }) + }) + + describe('range formatting', () => { + it('renders with same month', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2018-09-05', 'MMMM {D}, YYYY [nice]') + expect(s).toEqual('September 3 - 5, 2018 nice') + + s = calendar.formatRange('2018-09-03', '2018-09-05', '{D} MMMM, YYYY [nice]') + expect(s).toEqual('3 - 5 September, 2018 nice') + }) + + it('renders with same year but different month', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2018-10-05', '{MMMM {D}}, YYYY [nice]') + expect(s).toEqual('September 3 - October 5, 2018 nice') + + s = calendar.formatRange('2018-09-03', '2018-10-05', '{{D} MMMM}, YYYY [nice]') + expect(s).toEqual('3 September - 5 October, 2018 nice') + }) + + it('renders with different years', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03', '2019-10-05', '{MMMM {D}}, YYYY [nice]') + expect(s).toEqual('September 3, 2018 nice - October 5, 2019 nice') + + s = calendar.formatRange('2018-09-03', '2019-10-05', '{{D} MMMM}, YYYY [nice]') + expect(s).toEqual('3 September, 2018 nice - 5 October, 2019 nice') + }) + + it('renders the same if same day', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + }) + let s + + s = calendar.formatRange('2018-09-03T00:00:00', '2018-09-03T23:59:59', 'MMM Do YY') + expect(s).toEqual('Sep 3rd 18') + }) + + it('inherits defaultRangeSeparator', () => { + let calendar = new Calendar(document.createElement('div'), { + plugins: PLUGINS, + defaultRangeSeparator: ' to ', + }) + let s = calendar.formatRange('2018-09-03', '2018-09-05', 'MMMM D, YYYY [nice]') + expect(s).toEqual('September 3, 2018 nice to September 5, 2018 nice') + }) + + it('produces title with titleRangeSeparator', () => { + initCalendar({ // need to render the calendar to get view.title :( + initialView: 'dayGridWeek', + now: '2018-09-06', + titleFormat: 'MMMM {D} YY [yup]', + titleRangeSeparator: ' to ', + }) + expect(currentCalendar.view.title).toBe('September 2 to 8 18 yup') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5493 + it('displays correct rangeSeparator on events', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + initialDate: '2020-06-26', + scrollTime: '00:00', + eventTimeFormat: 'HH:mm:ss', + events: [ + { title: 'event', start: '2020-06-26T01:00:00', end: '2020-06-26T02:00:00' }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let timeTexts = timeGridWrapper.getEventTimeTexts() + expect(timeTexts[0]).toBe('01:00:00 - 02:00:00') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/datelib/rrule.ts b/fullcalendar-main/tests/src/datelib/rrule.ts new file mode 100644 index 0000000..807b81d --- /dev/null +++ b/fullcalendar-main/tests/src/datelib/rrule.ts @@ -0,0 +1,604 @@ +import dayGridPlugin from '@fullcalendar/daygrid' +import rrulePlugin from '@fullcalendar/rrule' +import luxonPlugin from '@fullcalendar/luxon3' +import { parseUtcDate, parseLocalDate } from '../lib/date-parsing.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('rrule plugin', () => { + pushOptions({ + plugins: [rrulePlugin, dayGridPlugin], + initialView: 'dayGridMonth', + now: '2018-09-07', + timeZone: 'UTC', + }) + + it('expands events when given an rrule object', () => { + initCalendar({ + events: [ + { + rrule: { + dtstart: '2018-09-04T13:00:00', + freq: 'weekly', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04T13:00:00Z') + expect(events[0].end).toBe(null) + expect(events[1].start).toEqualDate('2018-09-11T13:00:00Z') + expect(events[2].start).toEqualDate('2018-09-18T13:00:00Z') + expect(events[3].start).toEqualDate('2018-09-25T13:00:00Z') + expect(events[4].start).toEqualDate('2018-10-02T13:00:00Z') + }) + + it('can expand monthly recurrence when given an rrule object', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + events: [{ + rrule: { + dtstart: '2018-11-01', + freq: 'monthly', + count: 13, + bymonthday: [13], + }, + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].start).toEqualDate('2018-12-13') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6059 + it('can specify strings in byweekday', () => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2021-01-01', + events: [{ + allDay: true, + rrule: { + freq: 'weekly', + byweekday: ['mo', 'tu'], + dtstart: '2021-01-01', + }, + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(10) + expect(events[0].start).toEqualDate('2021-01-04') + }) + + it('can exclude a recurrence with exdate', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2020-12-01', + events: [{ + rrule: { + dtstart: '2020-12-01', + freq: 'weekly', + }, + exdate: '2020-12-08', + }], + }) + + let events = calendar.getEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2020-12-01') + expect(events[1].start).toEqualDate('2020-12-15') + expect(events[2].start).toEqualDate('2020-12-22') + expect(events[3].start).toEqualDate('2020-12-29') + expect(events[4].start).toEqualDate('2021-01-05') + }) + + it('can exclude multiple recurrences with exdate', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2020-12-01', + events: [{ + rrule: { + dtstart: '2020-12-01', + freq: 'weekly', + }, + exdate: ['2020-12-08', '2020-12-15'], + }], + }) + + let events = calendar.getEvents() + expect(events.length).toBe(4) + expect(events[0].start).toEqualDate('2020-12-01') + expect(events[1].start).toEqualDate('2020-12-22') + expect(events[2].start).toEqualDate('2020-12-29') + expect(events[3].start).toEqualDate('2021-01-05') + }) + + it('can exclude recurrences with an exrule', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2020-12-01', + events: [{ + rrule: { + dtstart: '2020-12-01', + freq: 'weekly', + }, + exrule: { + dtstart: '2020-12-08', + until: '2020-12-15', // will include this date for exclusion + freq: 'weekly', + }, + }], + }) + + let events = calendar.getEvents() + expect(events.length).toBe(4) + expect(events[0].start).toEqualDate('2020-12-01') + expect(events[1].start).toEqualDate('2020-12-22') + expect(events[2].start).toEqualDate('2020-12-29') + expect(events[3].start).toEqualDate('2021-01-05') + }) + + it('can exclude recurrences with multiple exrules', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now: '2020-12-01', + events: [{ + rrule: { + dtstart: '2020-12-01', + freq: 'weekly', + }, + exrule: [ + { + dtstart: '2020-12-08', + until: '2020-12-15', // will include this date for exclusion + freq: 'weekly', + }, + { + dtstart: '2020-12-22', + until: '2020-12-29', // will include this date for exclusion + freq: 'weekly', + }, + ], + }], + }) + + let events = calendar.getEvents() + expect(events.length).toBe(2) + expect(events[0].start).toEqualDate('2020-12-01') + expect(events[1].start).toEqualDate('2021-01-05') + }) + + it('expands events until a date', () => { + initCalendar({ + events: [ + { + rrule: { + dtstart: '2018-09-04T13:00:00', + until: '2018-10-01', + freq: 'weekly', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(4) + expect(events[0].start).toEqualDate('2018-09-04T13:00:00Z') + expect(events[0].end).toBe(null) + expect(events[1].start).toEqualDate('2018-09-11T13:00:00Z') + expect(events[2].start).toEqualDate('2018-09-18T13:00:00Z') + expect(events[3].start).toEqualDate('2018-09-25T13:00:00Z') + }) + + it('expands a range that starts exactly at the current view\'s start', () => { + initCalendar({ + initialDate: '2019-04-02', + initialView: 'dayGridDay', + events: [ + { + title: 'event with everyday with range', + allDay: true, + rrule: { + freq: 'daily', + dtstart: '2019-04-02', + until: '2019-04-09', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBeGreaterThanOrEqual(1) + expect(events[0].start).toEqualDate('2019-04-02') + }) + + it('expands events with a duration', () => { + initCalendar({ + events: [ + { + rrule: { + dtstart: '2018-09-04T13:00:00', + freq: 'weekly', + }, + duration: '03:00', + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04T13:00:00Z') + expect(events[0].end).toEqualDate('2018-09-04T16:00:00Z') + }) + + it('expands events with guessed allDay', () => { + initCalendar({ + events: [ + { + rrule: { + dtstart: '2018-09-04', + freq: 'weekly', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(true) + }) + + it('inherits defaultAllDay from source', () => { + initCalendar({ + defaultAllDay: false, + events: [ + { + rrule: { + dtstart: parseUtcDate('2018-09-04'), // no allDay info + freq: 'weekly', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(false) + }) + + it('inherits defaultAllDay from source setting', () => { + initCalendar({ + eventSources: [{ + defaultAllDay: false, + events: [ + { + rrule: { + dtstart: parseUtcDate('2018-09-04'), // no allDay info + freq: 'weekly', + }, + }, + ], + }], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(false) + }) + + it('can generate local dates when given an rrule object', () => { + initCalendar({ + timeZone: 'local', + events: [ + { + rrule: { + dtstart: parseLocalDate('2018-09-04T05:00:00').toISOString(), + freq: 'weekly', + }, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualLocalDate('2018-09-04T05:00:00') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(false) + }) + + describe('when given an rrule string', () => { + it('expands', () => { + initCalendar({ + events: [ + { + rrule: + 'DTSTART:20180904T130000\n' + + 'RRULE:FREQ=WEEKLY', + }, + ], + }) + + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-09-04T13:00:00Z') + expect(events[0].end).toBe(null) + expect(events[1].start).toEqualDate('2018-09-11T13:00:00Z') + expect(events[2].start).toEqualDate('2018-09-18T13:00:00Z') + expect(events[3].start).toEqualDate('2018-09-25T13:00:00Z') + expect(events[4].start).toEqualDate('2018-10-02T13:00:00Z') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6126 + it('expands correctly with UNTIL followed by newline', () => { + initCalendar({ + events: [ + { + rrule: + 'DTSTART:20180904T130000\n' + + 'RRULE:FREQ=WEEKLY;UNTIL=20180925T130000\n' + + 'RDATE:20180904T130000', + }, + ], + }) + + let events = getSortedEvents() + expect(events.length).toBe(4) + }) + + it('respects allDay', () => { + initCalendar({ + events: [ + { + allDay: true, + rrule: 'DTSTART:20180904T130000\nRRULE:FREQ=WEEKLY', + }, + ], + }) + + let events = getSortedEvents() + expect(events[0].start).toEqualDate('2018-09-04') // should round down + expect(events[0].allDay).toBe(true) + expect(events[0].extendedProps).toEqual({}) // didnt accumulate allDay or rrule props + }) + + it('can expand monthly recurrence in UTC', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + timeZone: 'UTC', + events: [{ + rrule: 'DTSTART:20181101\nRRULE:FREQ=MONTHLY;COUNT=13;BYMONTHDAY=13', + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].start).toEqualDate('2018-12-13') + }) + + it('can expand monthly recurrence in local timeZone', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + timeZone: 'local', + events: [{ + rrule: 'DTSTART:20181101\nRRULE:FREQ=MONTHLY;COUNT=13;BYMONTHDAY=13', + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].start).toEqualLocalDate('2018-12-13') + }) + + it('can expand weekly timed recurrence in local timeZone', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + timeZone: 'local', + events: [{ + rrule: 'DTSTART:20181201T000000\nRRULE:FREQ=WEEKLY', + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(6) + expect(events[0].start).toEqualLocalDate('2018-12-01') + }) + + it('can expand weekly UTC-timed recurrence in local timeZone', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + timeZone: 'local', + events: [{ + rrule: 'DTSTART:20181201T000000Z\nRRULE:FREQ=WEEKLY', + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(6) + expect(events[0].start).toEqualDate('2018-12-01') + }) + + it('can expand weekly UTC-timed recurrence in local timeZone, with exclusion', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-12-25T12:00:00', + timeZone: 'local', + events: [{ + rrule: 'DTSTART:20181201T000000Z\nRRULE:FREQ=WEEKLY\nEXDATE:20181208T000000Z', + }], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualDate('2018-12-01') + }) + + it('can generate local dates', () => { + let localStart = buildLocalRRuleDateStr('2018-09-04T05:00:00') + + initCalendar({ + timeZone: 'local', + events: [ + { + rrule: `DTSTART:${localStart}\nRRULE:FREQ=WEEKLY`, + }, + ], + }) + + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualLocalDate('2018-09-04T05:00:00') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(false) + }) + + it('can generate local dates, including EXDATE', () => { + let localStart = buildLocalRRuleDateStr('2018-09-04T05:00:00') + let localExdate = buildLocalRRuleDateStr('2018-09-05T05:00:00') + + initCalendar({ + timeZone: 'local', + events: [ + { + rrule: `DTSTART:${localStart}\nRRULE:FREQ=WEEKLY\nEXDATE:${localExdate}`, + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(5) + expect(events[0].start).toEqualLocalDate('2018-09-04T05:00:00') + expect(events[0].end).toBe(null) + expect(events[0].allDay).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5726 + it('can generate local dates, including EXDATE, when BYDAY and TZ shifting', () => { + initCalendar({ + timeZone: 'local', + initialDate: '2020-09-10', + events: [ + { + rrule: 'DTSTART:20200915T030000Z\nRRULE:FREQ=WEEKLY;BYDAY=SA\nEXDATE:20201003T030000Z', + }, + ], + }) + let events = getSortedEvents() + expect(events.length).toBe(3) + expect(events[0].start).toEqualDate('2020-09-19T03:00:00') + expect(events[1].start).toEqualDate('2020-09-26T03:00:00') + expect(events[2].start).toEqualDate('2020-10-10T03:00:00') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5993 + it('won\'t accidentally clip dates when calendar has non-UTC timezone', () => { + let calendar = initCalendar({ + plugins: [rrulePlugin, dayGridPlugin, luxonPlugin], + initialDate: '2020-11-01', + timeZone: 'Asia/Manila', + events: [ + { + duration: '01:00', + rrule: { + freq: 'daily', + dtstart: '2020-10-24T16:00:00Z', // will be 00:00 in Manila + }, + }, + ], + }) + + let events = calendar.getEvents() + expect(events[0].start).toEqualDate(calendar.view.activeStart) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7230 + it('updating the rrule dynamically renders correct number of events', () => { + const recurringEventDef = { + id: '4', + groupId: '4', + allDay: true, + rrule: { + freq: 'weekly', + dtstart: '2023-03-10', + }, + } + + let calendar = initCalendar({ + plugins: [rrulePlugin, dayGridPlugin], + initialDate: '2023-03-10', + initialView: 'dayGridMonth', + events: [recurringEventDef], + }) + + const dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getEventEls().length).toBe(5) + + calendar.next() + expect(dayGridWrapper.getEventEls().length).toBe(6) + + calendar.resetOptions({ + events: [{ + ...recurringEventDef, + duration: { days: 2 }, + }], + }, ['events']) + expect(dayGridWrapper.getEventEls().length).toBe(6) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5273 + it('updates rrule timed events when timeZone changes', () => { + const timeTexts = [] + + const calendar = initCalendar({ + plugins: [rrulePlugin, dayGridPlugin, luxonPlugin], + timeZone: 'America/New_York', + initialDate: '2023-02-10', + initialView: 'dayGridMonth', + events: [{ + id: '4', + groupId: '4', + allDay: false, + rrule: { + freq: 'weekly', + dtstart: '2023-02-10T12:00:00', // assumed to be Asia/Chicago + until: '2023-02-11', // only one instance + }, + }], + eventContent(arg) { + timeTexts.push(arg.timeText) + return true + }, + }) + + let events = calendar.getEvents() + expect(events[0].allDay).toBe(false) + expect(events[0].start).toEqualDate('2023-02-10T17:00:00Z') + expect(timeTexts.length).toBe(1) + expect(timeTexts[0]).toBe('12p') + + calendar.setOption('timeZone', 'America/Chicago') + events = calendar.getEvents() + expect(events[0].allDay).toBe(false) + expect(events[0].start).toEqualDate('2023-02-10T17:00:00Z') + expect(timeTexts.length).toBe(2) + expect(timeTexts[1]).toBe('11a') + }) + + // utils + + function buildLocalRRuleDateStr(inputStr) { // produces strings like '20200101123030' + return parseLocalDate(inputStr).toISOString().replace('.000', '').replace(/[-:]/g, '') + } + + function getSortedEvents() { + let events = currentCalendar.getEvents() + + events.sort((eventA, eventB) => eventA.start.valueOf() - eventB.start.valueOf()) + + return events + } +}) diff --git a/fullcalendar-main/tests/src/event-click-hover/eventClick.ts b/fullcalendar-main/tests/src/event-click-hover/eventClick.ts new file mode 100644 index 0000000..d578e4a --- /dev/null +++ b/fullcalendar-main/tests/src/event-click-hover/eventClick.ts @@ -0,0 +1,62 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('eventClick', () => { + pushOptions({ + initialDate: '2018-08-31', + initialView: 'dayGridMonth', + }) + + it('receives correct args', (done) => { + let calendar = initCalendar({ + events: [ + { start: '2018-08-31' }, + ], + eventClick(arg) { + expect(arg.el instanceof HTMLElement).toBe(true) + expect(typeof arg.event).toBe('object') + expect(arg.event.start instanceof Date).toBe(true) + expect(arg.jsEvent instanceof UIEvent).toBe(true) + expect(typeof arg.view).toBe('object') + done() + }, + }) + + let eventEls = new CalendarWrapper(calendar).getEventEls() + + expect(eventEls.length).toBe(1) + $(eventEls[0]).simulate('click') + }) + + it('fires on a background event', (done) => { + let calendar = initCalendar({ + events: [ + { start: '2018-08-31', display: 'background' }, + ], + eventClick(arg) { + expect(arg.event.display).toBe('background') + done() + }, + }) + + let bgEventEls = new CalendarWrapper(calendar).getBgEventEls() + + expect(bgEventEls.length).toBe(1) + $(bgEventEls[0]).simulate('click') + }) + + it('works via touch', (done) => { + let calendar = initCalendar({ + events: [ + { start: '2018-08-31' }, + ], + eventClick() { + done() + }, + }) + + let eventEls = new CalendarWrapper(calendar).getEventEls() + + expect(eventEls.length).toBe(1) + $(eventEls[0]).simulate('click') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Calendar.addEvent.ts b/fullcalendar-main/tests/src/event-data/Calendar.addEvent.ts new file mode 100644 index 0000000..347a634 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Calendar.addEvent.ts @@ -0,0 +1,87 @@ +describe('addEvent', () => { + pushOptions({ + initialDate: '2018-09-07', + }) + + it('will re-add an event that was previously removed', () => { + initCalendar({ + events: [ + { id: 'a', start: '2018-09-07' }, + ], + }) + let event = currentCalendar.getEventById('a') + expect(currentCalendar.getEvents().length).toBe(1) + event.remove() + expect(currentCalendar.getEvents().length).toBe(0) + let newEvent = currentCalendar.addEvent(event) + expect(currentCalendar.getEvents().length).toBe(1) + expect(newEvent).toBe(event) + }) + + it('won\'t double-add an event that was previously added', () => { + initCalendar({ + events: [ + { id: 'a', start: '2018-09-07' }, + ], + }) + let event = currentCalendar.getEventById('a') + expect(currentCalendar.getEvents().length).toBe(1) + let newEvent = currentCalendar.addEvent(event) + expect(currentCalendar.getEvents().length).toBe(1) + expect(newEvent).toBe(event) + }) + + it('will accept a string source ID', () => { + initCalendar({ + eventSources: [ + { + id: '9', + color: 'purple', + events: [ + { id: 'a', start: '2018-09-07' }, + ], + }, + ], + }) + + let theSource = currentCalendar.getEventSourceById('9') + let newEvent = currentCalendar.addEvent({ id: 'b', start: '2018-09-10' }, '9') + expect(newEvent.source.id === theSource.id) + }) + + it('will accept a number source ID', () => { + initCalendar({ + eventSources: [ + { + id: '9', + color: 'purple', + events: [ + { id: 'a', start: '2018-09-07' }, + ], + }, + ], + }) + + let theSource = currentCalendar.getEventSourceById('9') + let newEvent = currentCalendar.addEvent({ id: 'b', start: '2018-09-10' }, '9') + expect(newEvent.source.id === theSource.id) + }) + + it('will accept an object source', () => { + initCalendar({ + eventSources: [ + { + id: '9', + color: 'purple', + events: [ + { id: 'a', start: '2018-09-07' }, + ], + }, + ], + }) + + let theSource = currentCalendar.getEventSourceById('9') + let newEvent = currentCalendar.addEvent({ id: 'b', start: '2018-09-10' }, theSource) + expect(newEvent.source.id === theSource.id) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.formatRange.ts b/fullcalendar-main/tests/src/event-data/Event.formatRange.ts new file mode 100644 index 0000000..42d2d0a --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.formatRange.ts @@ -0,0 +1,49 @@ +import { FormatterInput } from '@fullcalendar/core' + +describe('Event::formatRange', () => { + pushOptions({ + timeZone: 'America/New_York', // for forced timezone offsets + locale: 'en', + }) + + const FORMAT_SETTINGS: FormatterInput = { + month: 'long', + day: 'numeric', + year: 'numeric', + timeZoneName: 'short', + separator: ' to ', + omitCommas: true, // for cross-browser + } + + describe('when event has an end', () => { + pushOptions({ + events: [ + { start: '2018-09-04T12:00:00-05:00', end: '2018-09-05T12:00:00-05:00' }, + ], + }) + + it('formats start and end', () => { + initCalendar() + let event = currentCalendar.getEvents()[0] + let str = event.formatRange(FORMAT_SETTINGS) + expect(str.replace(' at ', ' ')) + .toBe('September 4 to 5 2018 12:00 PM GMT-5') + }) + }) + + describe('when event has NO end', () => { + pushOptions({ + events: [ + { start: '2018-09-04T12:00:00-05:00' }, + ], + }) + + it('formats start', () => { + initCalendar() + let event = currentCalendar.getEvents()[0] + let str = event.formatRange(FORMAT_SETTINGS) + expect(str.replace(' at ', ' ')) + .toBe('September 4 2018 12:00 PM GMT-5') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.moveDates.ts b/fullcalendar-main/tests/src/event-data/Event.moveDates.ts new file mode 100644 index 0000000..e8e401f --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.moveDates.ts @@ -0,0 +1,67 @@ +describe('Event::moveDates', () => { + pushOptions({ + timeZone: 'UTC', + }) + + describe('when event doesn\'t have an end', () => { + it('moves start and keeps end null', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03T00:00:00' }, + ], + }) + let event = currentCalendar.getEventById('1') + event.moveDates({ days: 1, hours: 1 }) + expect(event.start).toEqualDate('2018-09-04T01:00:00Z') + expect(event.end).toBe(null) + }) + }) + + describe('when event does have an end', () => { + it('moves start and end by same delta', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03T00:00:00', end: '2018-09-04T12:00:00' }, + ], + }) + let event = currentCalendar.getEventById('1') + event.moveDates({ days: 1, hours: 1 }) + expect(event.start).toEqualDate('2018-09-04T01:00:00Z') + expect(event.end).toEqualDate('2018-09-05T13:00:00Z') + }) + }) + + it('moves related events of different duration by same delta', () => { + initCalendar({ + events: [ + { id: '1', groupId: 'a', start: '2018-09-03T00:00:00', end: '2018-09-04T12:00:00' }, + { id: '2', groupId: 'a', start: '2018-10-03T00:00:00', end: '2018-10-04T12:00:00' }, + ], + }) + let event1 = currentCalendar.getEventById('1') + event1.moveDates({ days: 1, hours: 1 }) + expect(event1.start).toEqualDate('2018-09-04T01:00:00Z') + expect(event1.end).toEqualDate('2018-09-05T13:00:00Z') + + let event2 = currentCalendar.getEventById('2') + expect(event2.start).toEqualDate('2018-10-04T01:00:00Z') + expect(event2.end).toEqualDate('2018-10-05T13:00:00Z') + }) + + it('does not move unrelated events', () => { + initCalendar({ + events: [ + { id: '1', groupId: 'a', start: '2018-09-03T00:00:00', end: '2018-09-04T12:00:00' }, + { id: '2', groupId: 'bbb', start: '2018-10-03T00:00:00', end: '2018-10-04T12:00:00' }, + ], + }) + let event1 = currentCalendar.getEventById('1') + event1.moveDates({ days: 1, hours: 1 }) + expect(event1.start).toEqualDate('2018-09-04T01:00:00Z') + expect(event1.end).toEqualDate('2018-09-05T13:00:00Z') + + let event2 = currentCalendar.getEventById('2') + expect(event2.start).toEqualDate('2018-10-03T00:00:00Z') // same + expect(event2.end).toEqualDate('2018-10-04T12:00:00Z') // same + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.moveEnd.ts b/fullcalendar-main/tests/src/event-data/Event.moveEnd.ts new file mode 100644 index 0000000..5908307 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.moveEnd.ts @@ -0,0 +1,38 @@ +describe('Event::moveEnd', () => { + pushOptions({ + timeZone: 'UTC', + defaultTimedEventDuration: '01:00', + }) + + describe('when event doesn\'t have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03T12:00:00' }, + ], + }) + + it('generates a new end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.moveEnd('01:00') + expect(event.start).toEqualDate('2018-09-03T12:00:00Z') + expect(event.end).toEqualDate('2018-09-03T14:00:00Z') + }) + }) + + describe('when event does have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03T12:00:00', end: '2018-09-03T15:00:00' }, + ], + }) + + it('moves end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.moveEnd('01:00') + expect(event.start).toEqualDate('2018-09-03T12:00:00Z') + expect(event.end).toEqualDate('2018-09-03T16:00:00Z') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.moveStart.ts b/fullcalendar-main/tests/src/event-data/Event.moveStart.ts new file mode 100644 index 0000000..0d58d08 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.moveStart.ts @@ -0,0 +1,54 @@ +describe('Event::moveStart', () => { + pushOptions({ + timeZone: 'UTC', + defaultTimedEventDuration: '01:00', + }) + + describe('when event doesn\'t have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03T12:00:00' }, + ], + }) + + it('moves start and generates an end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.moveStart({ hours: -5 }) + expect(event.start).toEqualDate('2018-09-03T07:00:00Z') + expect(event.end).toEqualDate('2018-09-03T13:00:00Z') + }) + }) + + describe('when event does have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03T12:00:00', end: '2018-09-03T15:00:00' }, + ], + }) + + it('moves start and keeps end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.moveStart({ hours: -5 }) + expect(event.start).toEqualDate('2018-09-03T07:00:00Z') + expect(event.end).toEqualDate('2018-09-03T15:00:00Z') + }) + }) + + describe('when moving start past end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03T12:00:00', end: '2018-09-03T15:00:00' }, + ], + }) + + it('resets end to reflect default duration', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.moveStart({ days: 1 }) + expect(event.start).toEqualDate('2018-09-04T12:00:00Z') + expect(event.end).toEqualDate('2018-09-04T13:00:00Z') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.mutation.ts b/fullcalendar-main/tests/src/event-data/Event.mutation.ts new file mode 100644 index 0000000..b80af2e --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.mutation.ts @@ -0,0 +1,34 @@ +describe('event mutations on non-instances', () => { + pushOptions({ + initialView: 'dayGridWeek', + now: '2018-09-03', + events: [ + { id: '1', start: '2018-09-04', display: 'inverse-background' }, // will make two segs + ], + }) + + describe('with date mutating', () => { + it('doesn\'t do anything', () => { + let renderCnt = 0 + let calendar = initCalendar({ + eventContent(arg) { + renderCnt += 1 + if (renderCnt === 2) { + arg.event.setStart('2018-08-04') + arg.event.setEnd('2018-10-04') + arg.event.setDates('2018-08-04', '2018-10-04') + } + }, + }) + + expect(renderCnt).toBe(2) + + let event = calendar.getEventById('1') + expect(event.start).toEqualDate('2018-09-04') + expect(event.end).toBe(null) + expect(event.allDay).toBe(true) + }) + }) + + // TODO: test for non-instances to have other props and extended props modified +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.setAllDay.ts b/fullcalendar-main/tests/src/event-data/Event.setAllDay.ts new file mode 100644 index 0000000..9665bc8 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.setAllDay.ts @@ -0,0 +1,111 @@ +describe('Event::setAllDay', () => { + describe('when setting from all-day to all-day', () => { + it('causes no change', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03', end: '2018-09-05', allDay: true }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(true) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toEqualDate('2018-09-05') + expect(event.allDay).toBe(true) + }) + }) + + describe('when setting from timed to timed', () => { + it('causes no change', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03T09:00:00', end: '2018-09-05T09:00:00', allDay: false }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(false) + expect(event.start).toEqualDate('2018-09-03T09:00:00Z') + expect(event.end).toEqualDate('2018-09-05T09:00:00Z') + expect(event.allDay).toBe(false) + }) + }) + + describe('when setting from all-day to timed', () => { + describe('when not maintaining duration', () => { + it('removes the end', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03', end: '2018-09-05', allDay: true }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(false) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toBe(null) + expect(event.allDay).toBe(false) + }) + }) + + describe('when maintaining duration', () => { + it('keeps exact duration', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03', end: '2018-09-05', allDay: true }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(false, { maintainDuration: true }) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toEqualDate('2018-09-05') + expect(event.allDay).toBe(false) + }) + }) + }) + + describe('when setting from timed to all-day', () => { + describe('when not maintaining duration', () => { + it('removes the end', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03T09:00:00', end: '2018-09-05T09:00:00', allDay: false }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(true) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toBe(null) + expect(event.allDay).toBe(true) + }) + }) + + describe('when maintaining duration', () => { + it('rounds the end down to the prev whole day', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-03T09:00:00', end: '2018-09-05T10:00:00', allDay: false }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(true, { maintainDuration: true }) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toEqualDate('2018-09-05') + expect(event.allDay).toBe(true) + }) + }) + + describe('when maintaining duration (from calendar setting)', () => { + it('rounds the end to the next whole day', () => { + initCalendar({ + allDayMaintainDuration: true, + events: [ + { id: '1', start: '2018-09-03T09:00:00', end: '2018-09-05T10:00:00', allDay: false }, + ], + }) + let event = currentCalendar.getEventById('1') + event.setAllDay(true) + expect(event.start).toEqualDate('2018-09-03') + expect(event.end).toEqualDate('2018-09-05') + expect(event.allDay).toBe(true) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.setDates.ts b/fullcalendar-main/tests/src/event-data/Event.setDates.ts new file mode 100644 index 0000000..fd9f2c1 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.setDates.ts @@ -0,0 +1,109 @@ +describe('Event::setDates', () => { + pushOptions({ + now: '2018-09-03', + timeZone: 'UTC', + defaultTimedEventDuration: '01:00', + events: [ + { id: '1', start: '2018-09-05T12:00:00' }, + ], + }) + + describe('when setting different start', () => { + it('changes start and gives it an end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-05T14:00:00', '2018-09-05T16:00:00') + expect(event.start).toEqualDate('2018-09-05T14:00:00Z') + expect(event.end).toEqualDate('2018-09-05T16:00:00Z') + }) + }) + + describe('when setting same start and end', () => { + it('changes nothing and end remains null', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-05T12:00:00', '2018-09-05T13:00:00') + expect(event.start).toEqualDate('2018-09-05T12:00:00Z') + expect(event.end).toBe(null) + }) + }) + + describe('when setting different end', () => { + it('changes end and gives it an end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-05T12:00:00', '2018-09-05T18:00:00') + expect(event.start).toEqualDate('2018-09-05T12:00:00Z') + expect(event.end).toEqualDate('2018-09-05T18:00:00Z') + }) + }) + + describe('when setting different start AND end', () => { + describe('if duration is effectively the same', () => { + it('changes start and leaves end null', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-06T01:00:00', '2018-09-06T02:00:00') + expect(event.start).toEqualDate('2018-09-06T01:00:00Z') + expect(event.end).toBe(null) + }) + }) + }) + + describe('when called with a null end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-05T12:00:00', end: '2018-09-05T14:00:00' }, + ], + }) + + it('clears the end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-06T01:00:00', null) + expect(event.start).toEqualDate('2018-09-06T01:00:00Z') + expect(event.end).toBe(null) + }) + }) + + it('can set allDay to true', () => { + initCalendar() // { id: '1', start: '2018-09-05T12:00:00' } + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-06', '2018-09-10', { allDay: true }) + expect(event.start).toEqualDate('2018-09-06') + expect(event.end).toEqualDate('2018-09-10') + expect(event.allDay).toBe(true) + }) + + it('can set allDay to false', () => { + initCalendar({ + events: [ + { id: '1', start: '2018-09-05', end: '2018-09-08' }, + ], + }) + + let event = currentCalendar.getEventById('1') + event.setDates('2018-09-06T10:00:00', '2018-09-10T02:00:00', { allDay: false }) + expect(event.start).toEqualDate('2018-09-06T10:00:00Z') + expect(event.end).toEqualDate('2018-09-10T02:00:00Z') + expect(event.allDay).toBe(false) + }) + + it('shortens related events of different duration by same delta', () => { + initCalendar({ + events: [ + { id: '1', groupId: 'a', start: '2018-09-03', end: '2018-09-05' }, + { id: '2', groupId: 'a', start: '2018-09-13', end: '2018-09-15' }, + ], + }) + + let event1 = currentCalendar.getEventById('1') + event1.setDates('2018-09-02', '2018-09-06') // start back by 1, end ahead by 1 + expect(event1.start).toEqualDate('2018-09-02') + expect(event1.end).toEqualDate('2018-09-06') + + let event2 = currentCalendar.getEventById('2') + expect(event2.start).toEqualDate('2018-09-12') + expect(event2.end).toEqualDate('2018-09-16') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.setEnd.ts b/fullcalendar-main/tests/src/event-data/Event.setEnd.ts new file mode 100644 index 0000000..d07f5b6 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.setEnd.ts @@ -0,0 +1,57 @@ +describe('Event::setEnd', () => { + pushOptions({ + now: '2018-09-03', + timeZone: 'UTC', + defaultTimedEventDuration: '01:00', + }) + + describe('when event doesn\'t have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-10T00:00:00' }, + ], + }) + + it('sets end and keeps start', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setEnd('2018-09-12T02:00:00') + expect(event.start).toEqualDate('2018-09-10T00:00:00Z') + expect(event.end).toEqualDate('2018-09-12T02:00:00Z') + }) + }) + + describe('when event does have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-10T00:00:00', end: '2018-09-11T00:00:00' }, + ], + }) + + it('changes end and keeps start', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setEnd('2018-09-12T02:00:00') + expect(event.start).toEqualDate('2018-09-10T00:00:00Z') + expect(event.end).toEqualDate('2018-09-12T02:00:00Z') + }) + }) + + it('shortens related events of different duration by same delta', () => { + initCalendar({ + events: [ + { id: '1', groupId: 'a', start: '2018-09-10T00:00:00', end: '2018-09-11T00:00:00' }, + { id: '2', groupId: 'a', start: '2018-09-14T00:00:00', end: '2018-09-16T00:00:00' }, + ], + }) + + let event1 = currentCalendar.getEventById('1') + event1.setEnd('2018-09-12T02:00:00') // move end forward by 1 day, 2 hours + expect(event1.start).toEqualDate('2018-09-10T00:00:00Z') + expect(event1.end).toEqualDate('2018-09-12T02:00:00Z') + + let event2 = currentCalendar.getEventById('2') + expect(event2.start).toEqualDate('2018-09-14T00:00:00Z') + expect(event2.end).toEqualDate('2018-09-17T02:00:00Z') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.setProp.ts b/fullcalendar-main/tests/src/event-data/Event.setProp.ts new file mode 100644 index 0000000..7a71ea6 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.setProp.ts @@ -0,0 +1,21 @@ +describe('Event::setProps', () => { + it('allows setting id', () => { + const calendar = initCalendar({ + events: [ + { id: '123', start: '2021-01-01' }, + ], + }) + + let events = calendar.getEvents() + let event = events[0] + + expect(event.id).toBe('123') + + event.setProp('id', '456') + expect(event.id).toBe('456') + + events = calendar.getEvents() + event = events[0] + expect(event.id).toBe('456') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.setStart.ts b/fullcalendar-main/tests/src/event-data/Event.setStart.ts new file mode 100644 index 0000000..678e8f5 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.setStart.ts @@ -0,0 +1,129 @@ +describe('Event::setStart', () => { + pushOptions({ + now: '2018-09-03', + timeZone: 'UTC', + defaultTimedEventDuration: '01:00', + }) + + describe('when event doesn\'t have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-05T00:00:00' }, + ], + }) + + describe('when not maintaining duration', () => { + it('moves start and gives event an end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01') + expect(event.start).toEqualDate('2018-09-01T00:00:00Z') + expect(event.end).toEqualDate('2018-09-05T01:00:00Z') + }) + }) + + describe('when maintaining duration', () => { + it('moves start and keeps no end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01', { maintainDuration: true }) + expect(event.start).toEqualDate('2018-09-01') + expect(event.end).toBe(null) + }) + }) + + it('can revert', () => { + let revertCalled = false + let calendar = initCalendar({ + eventChange(info) { + revertCalled = true + info.revert() + }, + }) + + let event = calendar.getEventById('1') + event.setStart('2018-09-01') // will be immediately undone + expect(revertCalled).toBe(true) + + let events = calendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].start).toEqualDate('2018-09-05T00:00:00') + }) + }) + + describe('when event does have an end', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-05T00:00:00', end: '2018-09-07T00:00:00' }, + ], + }) + + describe('when not maintaining duration', () => { + it('moves start and keeps the same end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01') + expect(event.start).toEqualDate('2018-09-01') + expect(event.end).toEqualDate('2018-09-07') + }) + }) + + describe('when maintaining duration', () => { + it('move start and keeps the end', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01', { maintainDuration: true }) + expect(event.start).toEqualDate('2018-09-01') + expect(event.end).toEqualDate('2018-09-03') + }) + }) + }) + + describe('when event is all-day', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-05', end: '2018-09-07', allDay: true }, + ], + }) + + describe('when setting start to another all-day', () => { + it('moves start', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01') + expect(event.start).toEqualDate('2018-09-01') + expect(event.end).toEqualDate('2018-09-07') + expect(event.allDay).toBe(true) + }) + }) + + describe('when setting start to timed', () => { + it('moves start to rounded-down start-of-day', () => { + initCalendar() + let event = currentCalendar.getEventById('1') + event.setStart('2018-09-01T23:00:00') + expect(event.start).toEqualDate('2018-09-01') + expect(event.end).toEqualDate('2018-09-07') + expect(event.allDay).toBe(true) + }) + }) + }) + + it('shortens related events of different duration by same delta', () => { + initCalendar({ + events: [ + { id: '1', groupId: 'a', start: '2018-09-05T00:00:00', end: '2018-09-10T00:00:00' }, + { id: '2', groupId: 'a', start: '2018-09-06T00:00:00', end: '2018-09-09T00:00:00' }, + ], + }) + + let event1 = currentCalendar.getEventById('1') + event1.setStart('2018-09-01') // move start back by 4 days + expect(event1.start).toEqualDate('2018-09-01') + expect(event1.end).toEqualDate('2018-09-10') + + let event2 = currentCalendar.getEventById('2') + expect(event2.start).toEqualDate('2018-09-02') + expect(event2.end).toEqualDate('2018-09-09') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/Event.source.ts b/fullcalendar-main/tests/src/event-data/Event.source.ts new file mode 100644 index 0000000..83d5ac8 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/Event.source.ts @@ -0,0 +1,23 @@ +describe('Event::source', () => { + it('returns the correct source', () => { + initCalendar({ + eventSources: [{ + id: 'sourceA', + events: [ + { id: 'eventA', start: '2018-09-07' }, + ], + }], + }) + let event = currentCalendar.getEventById('eventA') + let source = event.source + expect(source.id).toBe('sourceA') + }) + + it('returns null for events with no source', () => { + initCalendar() + currentCalendar.addEvent({ id: 'eventA', start: '2018-09-07' }) + let event = currentCalendar.getEventById('eventA') + let source = event.source + expect(source).toBe(null) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/EventObject-parsing.ts b/fullcalendar-main/tests/src/event-data/EventObject-parsing.ts new file mode 100644 index 0000000..3bf6c96 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/EventObject-parsing.ts @@ -0,0 +1,50 @@ +describe('Event Object parsing', () => { + it('records _id as an extended prop', () => { + initCalendar({ + initialDate: '2017-09-05', + initialView: 'dayGridMonth', + events: [ + { _id: 'a', start: '2017-09-05' }, + ], + }) + + let events = currentCalendar.getEvents() + expect(events[0].extendedProps._id).toBe('a') + }) + + it('parses an all-day event with timed same-day start/end', () => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-11-01', + timeZone: 'local', + events: [ + { + title: 'All Day with time', + allDay: true, + start: new Date(2017, 10, 1, 10, 0, 0), + end: new Date(2017, 10, 1, 18, 0, 0), // same-day. will result in null + }, + ], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].start).toEqualLocalDate('2017-11-01T00:00:00') + expect(events[0].end).toBe(null) + }) + + xit('won\'t accept two events with the same ID', () => { + initCalendar({ + initialView: 'dayGridDay', + initialDate: '2018-01-01', + events: [ + { id: '1', start: '2018-01-01', title: 'cool' }, + { id: '1', start: '2018-01-01' }, + ], + }) + + let events = currentCalendar.getEvents() + expect(events.length).toBe(1) + expect(events[0].title).toBe('cool') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/dynamic-options.ts b/fullcalendar-main/tests/src/event-data/dynamic-options.ts new file mode 100644 index 0000000..68e06f8 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/dynamic-options.ts @@ -0,0 +1,22 @@ +describe('setting option dynamically', () => { + it('does not cause refetch of events', (done) => { + let fetchCnt = 0 + + initCalendar({ + initialView: 'dayGridMonth', + events(arg, callback) { + fetchCnt += 1 + callback([]) + }, + }) + + expect(fetchCnt).toBe(1) + + currentCalendar.setOption('selectable', true) + + setTimeout(() => { // in case async + expect(fetchCnt).toBe(1) + done() + }, 0) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/eventDataTransform.ts b/fullcalendar-main/tests/src/event-data/eventDataTransform.ts new file mode 100644 index 0000000..604e308 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/eventDataTransform.ts @@ -0,0 +1,30 @@ +describe('eventDataTransform', () => { + let transform = (raw) => ( + $.extend({}, raw, { + was_processed: true, + }) + ) + + describeOptions({ + 'when on the calendar': { + events: [ + { start: '2017-10-23' }, + ], + eventDataTransform: transform, + }, + 'when on an event source': { + eventSources: [{ + events: [ + { start: '2017-10-23' }, + ], + eventDataTransform: transform, + }], + }, + }, () => { + it('affects parsing of the event', () => { + initCalendar() + let eventObj = currentCalendar.getEvents()[0] + expect(eventObj.extendedProps.was_processed).toBe(true) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/events-function.ts b/fullcalendar-main/tests/src/event-data/events-function.ts new file mode 100644 index 0000000..1f51f03 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/events-function.ts @@ -0,0 +1,123 @@ +describe('events as a function', () => { + pushOptions({ + timeZone: 'UTC', + }) + + it('requests the correct dates when days at the start/end of the month are hidden', (done) => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2013-06-01', // June 2013 has first day as Saturday, and last as Sunday! + weekends: false, + fixedWeekCount: false, + events(arg, callback) { + expect(arg.start).toEqualDate('2013-06-03T00:00:00Z') + expect(arg.end).toEqualDate('2013-06-29T00:00:00Z') + expect(arg.timeZone).toBe('UTC') + expect(typeof callback).toBe('function') + done() + }, + }) + }) + + it('does not request dates excluded by showNonCurrentDates:false', (done) => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2013-06-01', + showNonCurrentDates: false, + events(arg) { + expect(arg.start).toEqualDate('2013-06-01T00:00:00Z') + expect(arg.end).toEqualDate('2013-07-01T00:00:00Z') + done() + }, + }) + }) + + it('requests a timed range when slotMinTime is negative', (done) => { + initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + slotMinTime: { hours: -2 }, + events(arg) { + expect(arg.start).toEqualDate('2017-06-03T22:00:00Z') + expect(arg.end).toEqualDate('2017-06-11T00:00:00Z') + done() + }, + }) + }) + + it('requests a timed range when slotMaxTime exceeds 24 hours', (done) => { + initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + slotMaxTime: '26:00', + events(arg) { + expect(arg.start).toEqualDate('2017-06-04T00:00:00Z') + expect(arg.end).toEqualDate('2017-06-11T02:00:00Z') + done() + }, + }) + }) + + it('calls loading callback', (done) => { + let loadingCallArgs = [] + + initCalendar({ + loading(bool) { + loadingCallArgs.push(bool) + }, + events(arg, callback) { + setTimeout(() => { + expect(loadingCallArgs).toEqual([true]) + callback([]) + setTimeout(() => { + expect(loadingCallArgs).toEqual([true, false]) + done() + }, 0) + }, 0) + }, + }) + }) + + it('calls loading callback only once for multiple sources', (done) => { + let loadingCallArgs = [] + + initCalendar({ + loading(bool) { + loadingCallArgs.push(bool) + }, + eventSources: [ + (arg, callback) => { + setTimeout(() => { + callback([]) + }, 0) + }, + (arg, callback) => { + setTimeout(() => { + callback([]) + }, 10) + }, + ], + }) + + setTimeout(() => { + expect(loadingCallArgs).toEqual([true, false]) + done() + }, 20) + }) + + it('can call failure callback with error', () => { + let calledFailure = false + + initCalendar({ + events(arg, successCallback, failureCallback) { + failureCallback(new Error()) + }, + eventSourceFailure(error) { + calledFailure = true + expect(error instanceof Error).toBe(true) + }, + }) + + expect(calledFailure).toBe(true) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/lazyFetching.ts b/fullcalendar-main/tests/src/event-data/lazyFetching.ts new file mode 100644 index 0000000..46fe917 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/lazyFetching.ts @@ -0,0 +1,77 @@ +describe('lazyFetching', () => { + pushOptions({ + timeZone: 'UTC', + initialView: 'dayGridMonth', + initialDate: '2017-10-04', + }) + + describe('when on', () => { + pushOptions({ + lazyFetching: true, + }) + + it('won\'t fetch weeks already queried', () => { + let options = { + events(fetchInfo, callback) { + callback([]) + }, + } + spyOn(options, 'events').and.callThrough() + + initCalendar(options) + currentCalendar.changeView('timeGridWeek') + currentCalendar.next() + currentCalendar.next() + currentCalendar.next() + + expect(options.events.calls.count()).toBe(1) + + let arg = options.events.calls.argsFor(0)[0] + expect(arg.start).toEqualDate('2017-10-01T00:00:00Z') + expect(arg.end).toEqualDate('2017-11-12T00:00:00Z') + }) + }) + + describe('when off', () => { + pushOptions({ + lazyFetching: false, + }) + + it('will fetch each new week range', () => { + let options = { + events(fetchInfo, callback) { + callback([]) + }, + } + spyOn(options, 'events') + + initCalendar(options) + currentCalendar.changeView('timeGridWeek') + currentCalendar.next() + currentCalendar.next() + currentCalendar.next() + + expect(options.events.calls.count()).toBe(5) + + let arg = options.events.calls.argsFor(0)[0] + expect(arg.start).toEqualDate('2017-10-01T00:00:00Z') + expect(arg.end).toEqualDate('2017-11-12T00:00:00Z') + + arg = options.events.calls.argsFor(1)[0] + expect(arg.start).toEqualDate('2017-10-01T00:00:00Z') + expect(arg.end).toEqualDate('2017-10-08T00:00:00Z') + + arg = options.events.calls.argsFor(2)[0] + expect(arg.start).toEqualDate('2017-10-08T00:00:00Z') + expect(arg.end).toEqualDate('2017-10-15T00:00:00Z') + + arg = options.events.calls.argsFor(3)[0] + expect(arg.start).toEqualDate('2017-10-15T00:00:00Z') + expect(arg.end).toEqualDate('2017-10-22T00:00:00Z') + + arg = options.events.calls.argsFor(4)[0] + expect(arg.start).toEqualDate('2017-10-22T00:00:00Z') + expect(arg.end).toEqualDate('2017-10-29T00:00:00Z') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/recurring.ts b/fullcalendar-main/tests/src/event-data/recurring.ts new file mode 100644 index 0000000..d781ef7 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/recurring.ts @@ -0,0 +1,107 @@ +import timeGridPlugin from '@fullcalendar/timegrid' +import luxonPlugin from '@fullcalendar/luxon3' + +describe('recurring events', () => { + describe('when timed events in local timezone', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-07-03', + timeZone: 'local', + events: [ + { startTime: '09:00', endTime: '11:00', daysOfWeek: [2, 4] }, + ], + }) + + it('expands events with local time', () => { + initCalendar() + + let events = currentCalendar.getEvents() + + expect(events[0].start).toEqualLocalDate('2017-07-04T09:00:00') + expect(events[0].end).toEqualLocalDate('2017-07-04T11:00:00') + + expect(events[1].start).toEqualLocalDate('2017-07-06T09:00:00') + expect(events[1].end).toEqualLocalDate('2017-07-06T11:00:00') + }) + }) + + describe('when given recur range', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-07-03', + events: [ + { startTime: '09:00', endTime: '11:00', startRecur: '2017-07-05', endRecur: '2017-07-08' }, + ], + }) + + it('expands within given range', () => { + initCalendar() + + let events = currentCalendar.getEvents() + expect(events.length).toBe(3) + + expect(events[0].start).toEqualDate('2017-07-05T09:00:00Z') + expect(events[1].start).toEqualDate('2017-07-06T09:00:00Z') + expect(events[2].start).toEqualDate('2017-07-07T09:00:00Z') + }) + + describe('when current range is completely outside of recur-range', () => { + pushOptions({ + initialDate: '2017-02-02', + }) + + it('won\'t render any events', () => { + initCalendar() + let events = currentCalendar.getEvents() + expect(events.length).toBe(0) + }) + }) + }) + + describe('when event has a duration', () => { + pushOptions({ + initialView: 'dayGridWeek', + initialDate: '2019-06-02', + events: [ + { daysOfWeek: [6], duration: { days: 2 } }, + ], + }) + + it('will render from week before', () => { + initCalendar() + let events = currentCalendar.getEvents() + expect(events[0].start).toEqualDate('2019-06-01') + expect(events[0].end).toEqualDate('2019-06-03') + expect(events[1].start).toEqualDate('2019-06-08') + expect(events[1].end).toEqualDate('2019-06-10') + expect(events.length).toBe(2) + }) + }) + + it('when timeZone changes, events with unspecified timezone offsets move', () => { + const timeTexts = [] + const calendar = initCalendar({ + plugins: [timeGridPlugin, luxonPlugin], + timeZone: 'America/New_York', + initialView: 'timeGridWeek', + initialDate: '2023-02-07', + events: [ + { startTime: '12:00', daysOfWeek: [2] }, + ], + eventContent(arg) { + timeTexts.push(arg.timeText) + return true + }, + }) + + let events = calendar.getEvents() + expect(events[0].start).toEqualDate('2023-02-07T17:00:00Z') + expect(timeTexts.length).toBe(1) + expect(timeTexts[0]).toBe('12:00') + + calendar.setOption('timeZone', 'America/Chicago') + expect(events[0].start).toEqualDate('2023-02-07T17:00:00Z') + expect(timeTexts.length).toBe(2) + expect(timeTexts[1]).toBe('11:00') + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/refetchEvents.ts b/fullcalendar-main/tests/src/event-data/refetchEvents.ts new file mode 100644 index 0000000..6faa4a1 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/refetchEvents.ts @@ -0,0 +1,41 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('refetchEvents', () => { + it('retains scroll when in month view', () => { + let el = $('<div id="calendar" style="width:300px"/>').appendTo('body') + let scrollEl + let scrollTop + + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-04-25', + events: [ + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + { start: '2017-04-04', title: 'event' }, + ], + }, el) + + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getEventEls().length).toBe(8) + + let viewWrapper = new DayGridViewWrapper(calendar) + scrollEl = viewWrapper.getScrollerEl() + scrollEl.scrollTop = 1000 + scrollTop = scrollEl.scrollTop + + // verify that we queried the correct scroller el + expect(scrollTop).toBeGreaterThan(10) + + currentCalendar.refetchEvents() + expect(calendarWrapper.getEventEls().length).toBe(8) + expect(scrollEl.scrollTop).toBe(scrollTop) + }) +}) diff --git a/fullcalendar-main/tests/src/event-data/timeZoneChange.ts b/fullcalendar-main/tests/src/event-data/timeZoneChange.ts new file mode 100644 index 0000000..c790083 --- /dev/null +++ b/fullcalendar-main/tests/src/event-data/timeZoneChange.ts @@ -0,0 +1,33 @@ +import timeGridPlugin from '@fullcalendar/timegrid' +import luxonPlugin from '@fullcalendar/luxon3' + +describe('timeZone change', () => { + describe('with non-recurring timed events and luxon plugin', () => { + it('adjusts timed event', () => { + const timeTexts = [] + const calendar = initCalendar({ + plugins: [timeGridPlugin, luxonPlugin], + timeZone: 'America/New_York', + initialView: 'timeGridWeek', + initialDate: '2023-02-07', + events: [ + { start: '2023-02-07T12:00:00' }, + ], + eventContent(arg) { + timeTexts.push(arg.timeText) + return true + }, + }) + + let events = calendar.getEvents() + expect(events[0].start).toEqualDate('2023-02-07T17:00:00Z') + expect(timeTexts.length).toBe(1) + expect(timeTexts[0]).toBe('12:00') + + calendar.setOption('timeZone', 'America/Chicago') + expect(events[0].start).toEqualDate('2023-02-07T17:00:00Z') + expect(timeTexts.length).toBe(2) + expect(timeTexts[1]).toBe('11:00') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/all-day-change.ts b/fullcalendar-main/tests/src/event-drag/all-day-change.ts new file mode 100644 index 0000000..4b1a873 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/all-day-change.ts @@ -0,0 +1,130 @@ +import { parseMarker, addMs } from '@fullcalendar/core/internal' +import { drag } from '../lib/EventDragUtils.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { intersectRects } from '../lib/geom.js' + +describe('allDay change', () => { + pushOptions({ + timeZone: 'UTC', + initialView: 'timeGridWeek', + now: '2018-09-03', + scrollTime: 0, + editable: true, + dragScroll: false, + }) + + describe('when dragged from all-day to timed', () => { + pushOptions({ + events: [ + { id: '1', start: '2018-09-03', end: '2018-09-05' }, + ], + }) + + function doDrag() { + let viewWrapper = new TimeGridViewWrapper(currentCalendar) + let dayGridWrapper = viewWrapper.dayGrid + let timeGridWrapper = viewWrapper.timeGrid + + let startRect = intersectRects( + dayGridWrapper.getDayEls('2018-09-03')[0].getBoundingClientRect(), + dayGridWrapper.getEventEls()[0].getBoundingClientRect(), + ) + let endDate = parseMarker('2018-09-03T02:00:00').marker + let endRect = timeGridWrapper.computeSpanRects( + endDate, + addMs(endDate, 1000 * 60 * 30), // hardcoded 30 minute slot :( + )[0] + + return drag(startRect, endRect, false) // debug=false + } + + it('discards duration when allDayMaintainDuration:false', (done) => { + initCalendar({ + allDayMaintainDuration: false, + }) + doDrag().then(() => { + let event = currentCalendar.getEventById('1') + expect(event.start).toEqualDate('2018-09-03T02:00:00Z') + expect(event.end).toBe(null) + }).then(() => done()) + }) + + it('keeps duration when allDayMaintainDuration:true', (done) => { + initCalendar({ + allDayMaintainDuration: true, + }) + doDrag().then(() => { + let event = currentCalendar.getEventById('1') + expect(event.start).toEqualDate('2018-09-03T02:00:00Z') + expect(event.end).toEqualDate('2018-09-05T02:00:00Z') + }).then(() => done()) + }) + + it('sets a default duration when forceEventDuration:true', (done) => { + initCalendar({ + forceEventDuration: true, + defaultTimedEventDuration: '04:00', + }) + doDrag().then(() => { + let event = currentCalendar.getEventById('1') + expect(event.start).toEqualDate('2018-09-03T02:00:00Z') + expect(event.end).toEqualDate('2018-09-03T06:00:00Z') + }).then(() => done()) + }) + }) + + describe('when dragging from timed to all-day', () => { + it('sets a default duration when forceEventDuration:true', (done) => { + initCalendar({ + forceEventDuration: true, + defaultAllDayEventDuration: { days: 2 }, + events: [ + { id: '1', start: '2018-09-03T01:00:00', end: '2018-09-03T02:00:00' }, + ], + }) + + let viewWrapper = new TimeGridViewWrapper(currentCalendar) + let dayGridWrapper = viewWrapper.dayGrid + let timeGridWrapper = viewWrapper.timeGrid + let startRect = timeGridWrapper.getEventEls()[0].getBoundingClientRect() + let endRect = dayGridWrapper.getDayEls('2018-09-03')[0].getBoundingClientRect() + + drag(startRect, endRect, false).then(() => { // debug=false + let event = currentCalendar.getEventById('1') + expect(event.start).toEqualDate('2018-09-03T00:00:00Z') + expect(event.end).toEqualDate('2018-09-05T00:00:00Z') + done() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7222 + it('from more-popover', (done) => { + initCalendar({ + eventMaxStack: 1, + events: [ + { id: '1', start: '2018-09-03T01:00:00', end: '2018-09-03T02:00:00' }, + { id: '2', start: '2018-09-03T01:00:00', end: '2018-09-03T02:00:00' }, // in popover + ], + }) + + let viewWrapper = new TimeGridViewWrapper(currentCalendar) + let dayGridWrapper = viewWrapper.dayGrid + let timeGridWrapper = viewWrapper.timeGrid + + timeGridWrapper.openMorePopover() + setTimeout(() => { + let popoverEventEl = timeGridWrapper.getMorePopoverEventEls()[0] + let startRect = popoverEventEl.getBoundingClientRect() + let endRect = dayGridWrapper.getDayEls('2018-09-03')[0].getBoundingClientRect() + + drag(startRect, endRect, false, popoverEventEl).then(() => { // debug=false + let event = currentCalendar.getEventById('2') + expect(event.start).toEqualDate('2018-09-03T00:00:00Z') + expect(event.end).toBe(null) + expect(event.allDay).toBe(true) + done() + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/between-calendars.ts b/fullcalendar-main/tests/src/event-drag/between-calendars.ts new file mode 100644 index 0000000..df89103 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/between-calendars.ts @@ -0,0 +1,148 @@ +import { Calendar } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import { getRectCenter } from '../lib/geom.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dragging events between calendars', () => { + let DEFAULT_DATE = '2019-01-01' + let el0 + let el1 + let calendar0 + let calendar1 + + beforeEach(() => { + el0 = document.createElement('div') + el1 = document.createElement('div') + + el0.style.width = el1.style.width = '50%' + el0.style.cssFloat = el1.style.cssFloat = 'left' + + document.body.appendChild(el0) + document.body.appendChild(el1) + }) + + afterEach(() => { + if (calendar0) { + calendar0.destroy() + } + + if (calendar1) { + calendar1.destroy() + } + + document.body.removeChild(el0) + document.body.removeChild(el1) + }) + + it('fires all triggers', (done) => { + let triggerNames = [] + let eventAllowCalled = false + let eventEl + + calendar0 = new Calendar(el0, { + plugins: [interactionPlugin, dayGridPlugin], + timeZone: 'UTC', + initialDate: DEFAULT_DATE, + initialView: 'dayGridMonth', + editable: true, + events: [ + { start: '2019-01-01', id: 'a' }, + ], + eventLeave(info) { + triggerNames.push('eventLeave') + expect(info.draggedEl).toBe(eventEl) + expect(info.event.id).toBe('a') + expect(typeof info.revert).toBe('function') + expect(Array.isArray(info.relatedEvents)).toBe(true) + }, + }) + + calendar1 = new Calendar(el1, { + plugins: [interactionPlugin, dayGridPlugin], + timeZone: 'UTC', + initialDate: DEFAULT_DATE, + initialView: 'dayGridMonth', + editable: true, + droppable: true, + drop(info) { + triggerNames.push('drop') + expect(info.draggedEl).toBe(eventEl) + expect(info.date).toEqualDate('2019-01-05') + expect(info.dateStr).toBe('2019-01-05') + expect(info.allDay).toBe(true) + }, + eventAllow() { + eventAllowCalled = true + return true + }, + eventReceive(info) { + triggerNames.push('eventReceive') + expect(info.draggedEl).toBe(eventEl) + expect(info.event.start).toEqualDate('2019-01-05') + expect(typeof info.revert).toBe('function') + expect(Array.isArray(info.relatedEvents)).toBe(true) + }, + }) + + calendar0.render() + calendar1.render() + + let dayGridWrapper0 = new DayGridViewWrapper(calendar0).dayGrid + let dayGridWrapper1 = new DayGridViewWrapper(calendar1).dayGrid + + eventEl = dayGridWrapper0.getEventEls()[0] + let dayEl = dayGridWrapper1.getDayEls('2019-01-05')[0] + let point1 = getRectCenter(dayEl.getBoundingClientRect()) + + $(eventEl).simulate('drag', { + end: point1, + callback() { + expect(triggerNames).toEqual(['eventLeave', 'drop', 'eventReceive']) + expect(eventAllowCalled).toBe(true) + done() + }, + }) + }) + + it('works between timeGrid views', (done) => { + calendar0 = new Calendar(el0, { + plugins: [interactionPlugin, timeGridPlugin], + scrollTime: '00:00', + timeZone: 'UTC', + initialDate: DEFAULT_DATE, + initialView: 'timeGridDay', + editable: true, + events: [ + { start: '2019-01-01T00:00:00', id: 'a' }, + ], + }) + + calendar1 = new Calendar(el1, { + plugins: [interactionPlugin, timeGridPlugin], + scrollTime: '00:00', + timeZone: 'UTC', + initialDate: DEFAULT_DATE, + initialView: 'timeGridDay', + editable: true, + droppable: true, + eventReceive(info) { + done() + }, + }) + + calendar0.render() + calendar1.render() + + let eventEl = new CalendarWrapper(calendar0).getEventEls()[0] // of the source calendar + let destViewWrapper = new TimeGridViewWrapper(calendar1) + let point1 = getRectCenter(destViewWrapper.getScrollerEl().getBoundingClientRect()) + + $(eventEl).simulate('drag', { + end: point1, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/fixedMirrorParent.ts b/fullcalendar-main/tests/src/event-drag/fixedMirrorParent.ts new file mode 100644 index 0000000..ea36c46 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/fixedMirrorParent.ts @@ -0,0 +1,33 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('fixedMirrorParent', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2020-10-26', + }) + + it('changes the mirror\'s parent element', (done) => { + let calendar = initCalendar({ + editable: true, + fixedMirrorParent: document.body, + events: [ + { start: '2020-10-04' }, + ], + }) + + let wrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = wrapper.getEventEls() + + $(eventEls[0]).simulate('drag', { + dx: -100, // out of the calendar, to create a fixed mirror el + onBeforeRelease() { + let $mirrorEl = $('body').find('> .' + CalendarWrapper.EVENT_CLASSNAME) // direct child + expect($mirrorEl.length).toBe(1) + }, + onRelease() { + done() + }, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/from-external.ts b/fullcalendar-main/tests/src/event-drag/from-external.ts new file mode 100644 index 0000000..477d835 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/from-external.ts @@ -0,0 +1,90 @@ +import { Draggable } from '@fullcalendar/interaction' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('external event dragging', () => { + let $dragEl + let thirdPartyDraggable + + beforeEach(() => { + $dragEl = $('<div class="drag">yo</div>') + .css({ + width: 200, + background: 'blue', + color: 'white', + }) + .appendTo('body') + }) + + afterEach(() => { + if (thirdPartyDraggable) { + thirdPartyDraggable.destroy() + } + $dragEl.remove() + $dragEl = null + }) + + describe('with forceEventDuration', () => { + pushOptions({ + forceEventDuration: true, + defaultTimedEventDuration: '1:30', + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4597 + it('should yield an event with an end', (done) => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2019-04-01', + droppable: true, + defaultAllDayEventDuration: { days: 2 }, + eventReceive(arg) { + expect(arg.event.end).toEqualDate('2019-04-04') + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + thirdPartyDraggable = new Draggable($dragEl[0], { + eventData: {}, + }) + + $dragEl.simulate('drag', { + end: dayGridWrapper.getDayEl('2019-04-02'), + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4575 + it('provides eventAllow with a valid event with null start/end', (done) => { + let called = false + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2019-04-01', + droppable: true, + defaultAllDayEventDuration: { days: 2 }, + eventAllow(dropInfo, draggedEvent) { + expect(draggedEvent.id).toBe('a') + expect(draggedEvent.title).toBe('hey') + expect(draggedEvent.start).toBe(null) + expect(draggedEvent.end).toBe(null) + called = true + return true + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + thirdPartyDraggable = new Draggable($dragEl[0], { + eventData: { + id: 'a', + title: 'hey', + }, + }) + + $dragEl.simulate('drag', { + end: dayGridWrapper.getDayEl('2019-04-02'), + callback() { + expect(called).toBe(true) + done() + }, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/repeating.ts b/fullcalendar-main/tests/src/event-drag/repeating.ts new file mode 100644 index 0000000..05bf1f6 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/repeating.ts @@ -0,0 +1,102 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { waitEventDrag } from '../lib/wrappers/interaction-util.js' +import { filterVisibleEls } from '../lib/dom-misc.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('event dragging on repeating events', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-02-12', + editable: true, + events: [ + { + groupId: '999', + title: 'Repeating Event', + start: '2017-02-09T16:00:00', + }, + { + groupId: '999', + title: 'Repeating Event', + start: '2017-02-16T16:00:00', + }, + ], + }) + + // bug where offscreen instance of a repeating event was being incorrectly dragged + it('drags correct instance of event', (done) => { + let calendar = initCalendar() + + // event range needs out large (month) then scope down (week) + // so that the new view receives out-of-range events. + currentCalendar.changeView('timeGridWeek') + + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let dragging = timeGridWrapper.dragEventToDate(eventEl, '2017-02-16T12:00:00') + + waitEventDrag(calendar, dragging).then((res) => { + expect(typeof res).toBe('object') + done() + }) + }) + + it('hides other repeating events when dragging', (done) => { + let dayGridWrapper + let calendar = initCalendar({ + eventDragStart() { + setTimeout(() => { // try go execute DURING the drag + let visibleEventEls = filterVisibleEls(dayGridWrapper.getEventEls()) + expect(visibleEventEls.length).toBe(0) + }, 0) + }, + eventDrop() { + setTimeout(() => { + done() + }, 10) + }, + }) + + dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $(dayGridWrapper.getFirstEventEl()).simulate('drag', { + dx: 100, + duration: 100, // ample time for separate eventDragStart/eventDrop + }) + }) + + // inverse of above test + it('doesnt accidentally hide all non-id events when dragging', (done) => { + let dayGridWrapper + let calendar = initCalendar({ + events: [ + { + title: 'Regular Event', + start: '2017-02-09T16:00:00', + }, + { + title: 'Other Regular Event', + start: '2017-02-16T16:00:00', + }, + ], + + eventDragStart() { + setTimeout(() => { // try go execute DURING the drag + let visibleEventEls = filterVisibleEls(dayGridWrapper.getEventEls()) + expect(visibleEventEls.length).toBe(1) // the dragging event AND the other regular event + }, 0) + }, + + eventDrop() { + setTimeout(() => { + done() + }, 10) + }, + }) + + dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $(dayGridWrapper.getFirstEventEl()).simulate('drag', { + dx: 100, + duration: 100, // ample time for separate eventDragStart/eventDrop + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/showNonCurrentDates.ts b/fullcalendar-main/tests/src/event-drag/showNonCurrentDates.ts new file mode 100644 index 0000000..e41703e --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/showNonCurrentDates.ts @@ -0,0 +1,49 @@ +import * as EventDragUtils from '../lib/EventDragUtils.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventDrag } from '../lib/wrappers/interaction-util.js' + +describe('showNonCurrentDates event dragging', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + showNonCurrentDates: false, + events: [ + { start: '2017-06-07', end: '2017-06-10' }, + ], + editable: true, + }) + + describe('when dragging pointer into disabled region', () => { + it('won\'t allow the drop', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + EventDragUtils.drag( + dayGridWrapper.getDayEl('2017-06-08').getBoundingClientRect(), + dayGridWrapper.getDisabledDayEls()[3].getBoundingClientRect(), // the cell before Jun 1 + ) + .then((res) => { + expect(res).toBe(false) + }) + .then(() => done()) + }) + }) + + describe('when dragging an event\'s start into a disabled region', () => { + it('allow the drop if the cursor stays over non-disabled cells', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + let dragging = dayGridWrapper.dragEventToDate( + dayGridWrapper.getEventEls()[0], + '2017-06-08', + '2017-06-01', + ) + + waitEventDrag(calendar, dragging).then((res) => { + expect(typeof res).toBe('object') + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/touch.ts b/fullcalendar-main/tests/src/event-drag/touch.ts new file mode 100644 index 0000000..4345fff --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/touch.ts @@ -0,0 +1,31 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventDrag } from '../lib/wrappers/interaction-util.js' + +describe('event touch dragging', () => { + // https://github.com/fullcalendar/fullcalendar/issues/5706 + it('keeps event selected when initiated on custom element', (done) => { + let calendar = initCalendar({ + initialDate: '2020-08-12', + editable: true, + longPressDelay: 100, // dragEventToDate waits 200. TODO: no more hardcoding + events: [ + { title: 'event', start: '2020-08-12' }, + ], + eventContent: { html: '<i>the text</i>' }, + }) + let gridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = gridWrapper.getEventEls()[0] + + let dragging = gridWrapper.dragEventToDate( + eventEl.querySelector('i'), + null, // don't specify start date. start drag on center of given element + '2020-08-13', + true, + ) + + waitEventDrag(calendar, dragging).then((event) => { + expect(event.startStr).toBe('2020-08-13') + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-drag/validRange.ts b/fullcalendar-main/tests/src/event-drag/validRange.ts new file mode 100644 index 0000000..b3164c9 --- /dev/null +++ b/fullcalendar-main/tests/src/event-drag/validRange.ts @@ -0,0 +1,69 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('validRange event dragging', () => { + describe('when start constraint', () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { start: '2017-06-06' }, + events: [ + { start: '2017-06-07', end: '2017-06-10' }, + ], + editable: true, + }) + + it('won\'t go before validRange', (done) => { + let modifiedEvent: any = false + + let calendar = initCalendar({ + eventDrop(arg) { + modifiedEvent = arg.event + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getEventEls()).simulate('drag', { + end: dayGridWrapper.getDayEl('2017-06-06').previousElementSibling, // the invalid day before + callback() { + expect(modifiedEvent).toBe(false) + done() + }, + }) + }) + }) + }) + + describe('when end constraint', () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { end: '2017-06-09' }, + events: [ + { start: '2017-06-04', end: '2017-06-07' }, + ], + editable: true, + }) + + it('won\'t go after validRange', (done) => { + let modifiedEvent: any = false + + let calendar = initCalendar({ + eventDrop(arg) { + modifiedEvent = arg.event + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getEventEls()).simulate('drag', { + end: dayGridWrapper.getDayEl('2017-06-08').nextElementSibling, // the invalid day after + callback() { + expect(modifiedEvent).toBe(false) + done() + }, + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/bg-events.ts b/fullcalendar-main/tests/src/event-render/bg-events.ts new file mode 100644 index 0000000..100e76e --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/bg-events.ts @@ -0,0 +1,26 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('background event', () => { + pushOptions({ + initialDate: '2020-06-23', + }) + + describe('that are timed', () => { + pushOptions({ + events: [ + { + start: '2020-06-23T12:00:00', + end: '2020-06-23T14:00:00', + display: 'background', + }, + ], + }) + + it('won\'t appear in daygrid', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getBgEventEls() + expect(eventEls.length).toBe(0) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/dayGrid-events.ts b/fullcalendar-main/tests/src/event-render/dayGrid-events.ts new file mode 100644 index 0000000..03f7d75 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/dayGrid-events.ts @@ -0,0 +1,701 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { anyElsIntersect } from '../lib/dom-geom.js' +import { filterVisibleEls } from '../lib/dom-misc.js' + +describe('dayGrid advanced event rendering', () => { + pushOptions({ + initialDate: '2020-05-01', + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5408 + it('renders without intersecting', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-05-01', + events: [ + { start: '2020-05-02', end: '2020-05-04', title: 'event a' }, + { start: '2020-05-02', end: '2020-05-04', title: 'event b' }, + { start: '2020-05-03', end: '2020-05-05', title: 'event c' }, + { start: '2020-05-04', title: 'event d' }, + { start: '2020-05-04', title: 'event e' }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + expect(anyElsIntersect(eventEls)).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5771 + it('renders more-links correctly when first obscured event is longer than event before it', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-08-01', + dayMaxEventRows: 3, + events: [ + { title: 'big1', start: '2020-07-23', end: '2020-07-28' }, + { title: 'small1', start: '2020-07-24', end: '2020-07-27' }, + { title: 'small2', start: '2020-07-24', end: '2020-07-27' }, + { title: 'big2', start: '2020-07-25', end: '2020-07-28' }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + let moreLinkEls = dayGridWrapper.getMoreEls() + + expect(visibleEventEls.length).toBe(3) + expect(moreLinkEls.length).toBe(1) + expect(anyElsIntersect(visibleEventEls.concat(moreLinkEls))).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5790 + it('positions more-links correctly in columns that have empty space', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-09-01', + dayMaxEventRows: 4, + events: [ + { start: '2020-08-30', end: '2020-09-04' }, + { start: '2020-08-31', end: '2020-09-03' }, + { start: '2020-09-01', end: '2020-09-04' }, + { start: '2020-09-02', end: '2020-09-04' }, + { start: '2020-09-02', end: '2020-09-04' }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + let moreLinkEls = dayGridWrapper.getMoreEls() + + expect(visibleEventEls.length).toBe(3) + expect(moreLinkEls.length).toBe(2) + expect(anyElsIntersect(visibleEventEls.concat(moreLinkEls))).toBe(false) + + expect(Math.abs( + moreLinkEls[0].getBoundingClientRect().top - + moreLinkEls[1].getBoundingClientRect().top, + )).toBeLessThan(1) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5883 + it('it renders without gaps when ordered by title', () => { + let calendar = initCalendar({ + initialDate: '2020-10-01', + eventOrder: 'title', + dayMaxEventRows: 3, + events: [ + { + title: 'b1', + start: '2020-10-20', + end: '2020-10-22', + }, + { + title: 'b2', + start: '2020-10-21', + end: '2020-10-22', + }, + { + title: 'b3', + start: '2020-10-20', + end: '2020-10-23', + }, + { + title: 'b4', + start: '2020-10-20', + end: '2020-10-23', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + let moreLinkEls = dayGridWrapper.getMoreEls() + + expect(visibleEventEls.length).toBe(2) + expect(moreLinkEls.length).toBe(3) + expect(anyElsIntersect(visibleEventEls.concat(moreLinkEls))).toBe(false) + }) + + it('won\'t intersect when doing custom rendering', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-06-01', + events: [ + { start: '2020-06-04', end: '2020-06-08', title: 'event a' }, + { start: '2020-06-05', end: '2020-06-09', title: 'event b' }, + { start: '2020-06-08T12:00:00', title: 'event c' }, + ], + eventContent(arg) { // creates varying-height events, which revealed the bug + return { + html: ` + <b>${arg.timeText}</b> + <i>${arg.event.title}</i> + `, + } + }, + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + expect(anyElsIntersect(eventEls)).toBe(false) + }) + + it('renders single-day timed event as list-item', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-05-01', + eventDisplay: 'auto', + events: [ + { + title: 'event 1', + start: '2020-05-11T22:00:00', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + + expect(dayGridWrapper.isEventListItem(eventEl)).toBe(true) + }) + + it('does not render multi-day event as list-item', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-05-01', + eventDisplay: 'auto', + events: [ + { + title: 'event 1', + start: '2020-05-11T22:00:00', + end: '2020-05-12T06:00:00', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + + expect(dayGridWrapper.isEventListItem(eventEl)).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5634 + it('does not render split multi-day event as list-item', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-05-01', + eventDisplay: 'auto', + events: [ + { + title: 'event', + start: '2020-05-09T12:00:00', + end: '2020-05-10T12:00:00', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + expect(eventEls.length).toBe(2) + expect(dayGridWrapper.isEventListItem(eventEls[0])).toBe(false) + expect(dayGridWrapper.isEventListItem(eventEls[0])).toBe(false) + }) + + it('render only block when eventDislay:block', () => { + let calendar = initCalendar({ + eventDisplay: 'block', + events: [ + { start: '2020-05-02T02:00:00', title: 'event a' }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + + expect(dayGridWrapper.isEventListItem(eventEl)).toBe(false) + }) + + it('adjusts more link when getting bigger then smaller with liquid height', () => { + const LARGE_HEIGHT = 800 + const SMALL_HEIGHT = 500 + let $container = $( + `<div style="height:${LARGE_HEIGHT}px"><div></div></div>`, + ).appendTo('body') + + let calendar = initCalendar({ + height: '100%', + dayMaxEvents: true, // will cause visible event count to vary + events: [ + { start: '2020-05-02', end: '2020-05-03', title: 'event a' }, + { start: '2020-05-02', end: '2020-05-03', title: 'event b' }, + { start: '2020-05-02', end: '2020-05-03', title: 'event c' }, + { start: '2020-05-02', end: '2020-05-03', title: 'event d' }, + { start: '2020-05-02', end: '2020-05-03', title: 'event e' }, + { start: '2020-05-02', end: '2020-05-03', title: 'event f' }, + ], + }, $container.find('div')) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let origEventCnt = filterVisibleEls(dayGridWrapper.getEventEls()).length + + $container.css('height', SMALL_HEIGHT) + calendar.updateSize() + let smallEventCnt = filterVisibleEls(dayGridWrapper.getEventEls()).length + expect(smallEventCnt).not.toBe(origEventCnt) + + $container.css('height', LARGE_HEIGHT) + calendar.updateSize() + let largeEventCnt = filterVisibleEls(dayGridWrapper.getEventEls()).length + expect(largeEventCnt).toBe(origEventCnt) + + $container.remove() + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5850 + it('does not have JS error when dayMaxEventRows and almost no height', () => { + initCalendar({ + height: '100%', + eventDisplay: 'block', + dayMaxEventRows: true, + events: [ + { start: '2020-05-02T02:00:00', title: 'event a' }, + ], + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5863 + it('does not have JS error when dayMaxEventRows and almost no height', () => { + let $container = $('<div style="width:100px" />').appendTo('body') + initCalendar({ + height: '100%', + eventDisplay: 'block', + dayMaxEventRows: true, + events: [ + { start: '2020-05-02T02:00:00', title: 'event a' }, + ], + }, $container[0]) + $container.remove() + }) + + it('doesn\'t create more-link while positioning events with temporary unknown dimensions', () => { + let renderedMoreLink = false + initCalendar({ + initialView: 'dayGridMonth', + moreLinkDidMount() { + renderedMoreLink = true + }, + events: [ + { id: '1', start: '2020-05-05' }, + ], + }) + expect(renderedMoreLink).toBe(false) + }) + + it('can render events with strict ordering', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + eventOrder: 'id', + eventOrderStrict: true, + events: [ + { id: '1', start: '2020-05-05' }, + { id: '2', start: '2020-05-03', end: '2020-05-08' }, + { id: '3', start: '2020-05-04' }, + ], + eventDidMount(arg) { + arg.el.setAttribute('data-event-id', arg.event.id) // TODO: more formal system for this + }, + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + expect(anyElsIntersect(visibleEventEls)).toBe(false) + + let el1 = document.querySelector('[data-event-id="1"]') + let el2 = document.querySelector('[data-event-id="2"]') + let el3 = document.querySelector('[data-event-id="3"]') + let top1 = el1.getBoundingClientRect().top + let top2 = el2.getBoundingClientRect().top + let top3 = el3.getBoundingClientRect().top + expect(top1).toBeLessThan(top2) + expect(top2).toBeLessThan(top3) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5767 + it('consumes empty gaps in space when strict ordering', () => { + let calendar = initCalendar({ + initialDate: '2020-08-23', + initialView: 'dayGridWeek', + eventOrder: 'title', + eventOrderStrict: true, + dayMaxEventRows: 4, + eventDidMount(arg) { + arg.el.setAttribute('data-event-id', arg.event.id) // TODO: more formal system for this + }, + events: [ + { + title: 'a', + id: 'a', + start: '2020-08-24', + end: '2020-08-27', + }, + { + title: 'b', + id: 'b', + start: '2020-08-24', + end: '2020-08-27', + }, + { + title: 'c', + id: 'c', + start: '2020-08-28', + end: '2020-08-29', + }, + { + title: 'd', + id: 'd', + start: '2020-08-24', + end: '2020-08-29', + }, + { + title: 'e', + id: 'e', + start: '2020-08-27', + end: '2020-08-29', + }, + { // will cause 'e' to hide + title: 'f', + id: 'f', + start: '2020-08-24', + end: '2020-08-29', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + expect(anyElsIntersect(visibleEventEls)).toBe(false) + + let rect0 = document.querySelector('[data-event-id="d"]').getBoundingClientRect() + let rect1 = document.querySelector('[data-event-id="f"]').getBoundingClientRect() + expect(rect1.top - rect0.bottom).toBeLessThan(2) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6393 + it('doesn\'t overlap with eventOrderStrict', () => { + let calendar = initCalendar({ + initialDate: '2021-06-21', + initialView: 'dayGridWeek', + eventOrderStrict: true, + events: [ + { + title: 'Busy1', + start: '2021-06-21T10:00:00Z', + end: '2021-06-21T11:00:00Z', + }, + { + title: 'Busy2', + start: '2021-06-21T08:00:00Z', + end: '2021-06-21T10:00:00Z', + }, + { + title: 'Busy3', + start: '2021-06-22T11:00:00Z', + end: '2021-06-22T12:00:00Z', + }, + { + title: 'Busy4', + start: '2021-06-24T08:30:00Z', + end: '2021-06-24T11:00:00Z', + }, + { + title: 'Busy5', + start: '2021-06-24T16:00:00Z', + end: '2021-06-24T16:30:00Z', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + expect(anyElsIntersect(visibleEventEls)).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6397 + it('doesn\'t show duplicate events in popover when eventOrder by start date', (done) => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2021-07-07', + eventOrder: 'start', + dayMaxEventRows: 4, + events: [ + { + title: 'Ariana Florescu', + start: '2021-07-02', + end: '2021-07-03', + }, + { + title: 'Alan Leaclaire', + start: '2021-07-02', + end: '2021-07-10', + }, + { + title: 'Divya Sundavaridevelu', + start: '2021-07-05', + end: '2021-07-06', + }, + { + title: 'Phyllis Benoussan', + start: '2021-07-05', + end: '2021-07-06', + }, + { + title: 'Allison Olsen', + start: '2021-07-05', + end: '2021-07-10', + }, + { + title: 'Justin Sinnaeve', + start: '2021-07-05', + end: '2021-07-10', + }, + { + title: 'Sylwia Pitel', + start: '2021-07-07', + end: '2021-07-08', + }, + { + title: 'Derrick Leach', + start: '2021-07-07', + end: '2021-07-10', + }, + { + title: 'Sebastien Pillon', + start: '2021-07-08', + end: '2021-07-13', + }, + { + title: 'Nishat Ayub', + start: '2021-07-08', + end: '2021-07-10', + }, + { + title: 'Ognjen Stoisavljevic', + start: '2021-07-09', + end: '2021-07-10', + }, + { + title: 'Slobodan Stojanovic', + start: '2021-07-09', + end: '2021-07-10', + }, + { + title: 'Phyllis Benoussan', + start: '2021-07-09', + end: '2021-07-10', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + dayGridWrapper.openMorePopover(4) // on July 9th + setTimeout(() => { + let eventEls = dayGridWrapper.getMorePopoverEventEls() + expect(eventEls.length).toBe(9) + done() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7447 + it('Doesn\'t error or overlap event positions when white-space:normal', () => { + let calendar = initCalendar({ + initialView: 'dayGridWeek', + initialDate: '2023-04-09', + dayMaxEvents: 4, + eventContent() { + return { + html: '<div style="white-space: normal">' + + '<strong>AAAAAAAAAA</strong> <strong>BBBBBBBBB</strong></div>', + } + }, + events: [ + { + id: 'a', + start: '2023-04-14', + end: '2023-04-21', + }, + { + id: 'b', + start: '2023-04-13', + end: '2023-04-22', + }, + { + id: 'c', + start: '2023-04-06', + end: '2023-04-15', + }, + { + id: 'd', + start: '2023-04-11', + end: '2023-04-14', + }, + { + id: 'e', + start: '2023-04-14', + end: '2023-04-19', + }, + { + id: 'f', + start: '2023-04-13', + end: '2023-04-19', + }, + { + id: 'g', + start: '2023-04-05', + end: '2023-04-14', + }, + { + id: 'h', + start: '2023-04-06', + end: '2023-04-15', + }, + { + id: 'i', + start: '2023-04-13', + end: '2023-04-15', + }, + { + id: 'j', + start: '2023-04-12', + end: '2023-04-15', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + expect(anyElsIntersect(visibleEventEls)).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6486 + it('renders events starting yesterday, ending at midnight, as "past"', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2023-04-09', // "today" + now: '2023-04-09', // "today" + events: [{ + start: '2023-04-08', // yesterday + allDay: true, + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + expect(eventEls[0]).toHaveClass('fc-event-past') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7462 + it('Cannot infinitely recurse with dayMaxEventRows and many hidden event rows', () => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2023-09-01', + dayMaxEventRows: 6, + events: [ + { + start: '2023-09-28T00:00:00', + end: '2023-10-01T00:00:00', + }, + { + start: '2023-09-26T00:00:00', + end: '2023-09-27T00:00:00', + }, + { + start: '2023-09-20T17:00:00', + end: '2023-09-27T17:00:00', + }, + { + start: '2023-09-21T16:00:00', + end: '2023-09-25T14:00:00', + }, + { + start: '2023-09-21T16:00:00', + end: '2023-09-25T11:00:00', + }, + { + start: '2023-09-28T10:00:00', + end: '2023-09-28T15:00:00', + }, + { + start: '2023-09-27T08:00:00', + end: '2023-10-04T18:00:00', + }, + { + start: '2023-09-20T13:00:00', + end: '2023-09-29T12:00:00', + }, + { + start: '2023-09-20T12:00:00', + end: '2023-09-29T12:00:00', + }, + { + start: '2023-09-27T11:00:00', + end: '2023-09-28T18:00:00', + }, + { + start: '2023-03-29T23:00:00', + end: '2024-03-29T22:00:00', + }, + { + start: '2023-09-25T02:00:00', + end: '2023-09-29T12:00:00', + }, + { + start: '2023-09-22T14:00:00', + end: '2023-09-29T12:00:00', + }, + { + start: '2023-09-22T14:00:00', + end: '2023-09-28T12:00:00', + }, + { + start: '2023-09-19T13:00:00', + end: '2023-09-30T13:00:00', + }, + ], + }) + }) + + it('will limit events to dayMaxEventRows:1', () => { + const calendar = initCalendar({ + initialDate: '2021-10-31', + dayMaxEventRows: 1, + events: [ + { title: 'A', start:'2021-10-31', end:'2021-11-02' }, + { title: 'B', start:'2021-10-29', end:'2021-11-02' }, + { title: 'C', start:'2021-10-28 12:00:00', end:'2021-10-31 12:00:00' }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let visibleEventEls = filterVisibleEls(dayGridWrapper.getEventEls()) + let moreEls = dayGridWrapper.getMoreEls() + let allEls = [...visibleEventEls, ...moreEls] + let offsetTopHash = {} + + for (let el of allEls) { + offsetTopHash[Math.round(el.getBoundingClientRect().top)] = true + } + + // two weeks, two distinct lines of events (one per week) + expect(Object.keys(offsetTopHash).length).toBe(2) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/event-render-hooks.ts b/fullcalendar-main/tests/src/event-render/event-render-hooks.ts new file mode 100644 index 0000000..e0669a1 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/event-render-hooks.ts @@ -0,0 +1,147 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { RED_REGEX } from '../lib/dom-misc.js' + +describe('eventContent', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2020-06-01', + events: [ + { title: 'my event', start: '2020-06-01T01:00:00' }, + ], + }) + + it('can inject vdom nodes', () => { + let calendar = initCalendar({ + eventContent(info, createElement) { + return ( + createElement('span', {}, [ // TODO: document how to use Fragment + createElement('b', {}, info.timeText), + createElement('i', {}, info.event.title), + ]) + ) + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + expect(eventEl.querySelector('b').innerHTML).toBe('1a') + expect(eventEl.querySelector('i').innerHTML).toBe('my event') + }) + + it('can inject html content', () => { + let calendar = initCalendar({ + eventContent(info) { + return { + html: `<b>${info.timeText}</b><i>${info.event.title}</i>`, + } + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + expect(eventEl.querySelector('b').innerHTML).toBe('1a') + expect(eventEl.querySelector('i').innerHTML).toBe('my event') + }) + + it('can inject text content', () => { + let calendar = initCalendar({ + eventContent(info) { + return info.timeText + ' - ' + info.event.title + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + expect(eventEl.innerHTML).toBe('1a - my event') + }) + + it('will render blank content if nothing returned', () => { + let calendar = initCalendar({ + eventContent() { + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + expect($(eventEl).text()).toBe('') + }) + + it('can return true to render default content', () => { + let calendar = initCalendar({ + eventContent() { + return true + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getEventEls()[0] + expect($(eventEl).text()).toMatch('my event') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5916 + xit('can render multiple appearance changes in eventDidMount', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2020-12-13', + eventDidMount(arg) { + arg.event.setProp('backgroundColor', 'red') + arg.event.setProp('title', 'name changed') + }, + events: [ + { + id: 'a', + title: 'a', + start: '2020-12-15T09:30:00', + }, + { + id: 'b', + title: 'b', + start: '2020-12-22T09:30:00', + }, + ], + }) + + function expectEventDataChanged(id) { + let event = calendar.getEventById(id) + expect(event.title).toBe('name changed') + expect(event.backgroundColor).toBe('red') + } + + let viewWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEl = viewWrapper.getEventEls()[0] + expect($(eventEl).css('background-color')).toMatch(RED_REGEX) + expectEventDataChanged('a') + + calendar.next() + eventEl = viewWrapper.getEventEls()[0] + expect($(eventEl).css('background-color')).toMatch(RED_REGEX) + expectEventDataChanged('b') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6079 + it('can handle view-specific custom content generators', () => { + let calendar = initCalendar({ + initialView: 'dayGridWeek', + initialDate: '2021-01-07', + views: { + dayGridWeek: { + eventContent() { + let eventWrapper = document.createElement('div') + eventWrapper.innerText = 'test dayGridWeek' + let arrayOfDomNodes = [eventWrapper] + return { domNodes: arrayOfDomNodes } + }, + }, + }, + events: [ + { start: '2021-01-07', title: 'default title' }, + ], + }) + + let dayGrid = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGrid.getEventEls()[0] + expect(eventEl.innerText.trim()).toBe('test dayGridWeek') + + calendar.changeView('dayGridMonth') + + dayGrid = new DayGridViewWrapper(calendar).dayGrid + eventEl = dayGrid.getEventEls()[0] + expect(eventEl.innerText.trim()).toBe('default title') + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/eventMinHeight.ts b/fullcalendar-main/tests/src/event-render/eventMinHeight.ts new file mode 100644 index 0000000..a6d3a16 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/eventMinHeight.ts @@ -0,0 +1,38 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('eventMinHeight', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-08-10', + events: [ + { start: '2017-08-10T10:30:00', end: '2017-08-10T10:31:00' }, + ], + }) + + it('has a non-zero default', () => { + let calendar = initCalendar() + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + expect(eventEl.offsetHeight).toBeGreaterThan(5) + }) + + it('can be set and rendered', () => { + let calendar = initCalendar({ + eventMinHeight: 40, + }) + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + expect(eventEl.offsetHeight).toBeGreaterThanOrEqual(39) + }) + + it('will ignore temporal non-collision and render side-by-side', () => { + let calendar = initCalendar({ + eventMinHeight: 40, + events: [ + { start: '2017-08-10T10:30:00', end: '2017-08-10T10:31:00', title: 'event a' }, + { start: '2017-08-10T10:31:20', end: '2017-08-10T10:31:40', title: 'event b' }, + ], + }) + let eventEls = new CalendarWrapper(calendar).getEventEls() + expect(eventEls[0].getBoundingClientRect().left) + .toBeLessThan(eventEls[1].getBoundingClientRect().left) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/eventOrder.ts b/fullcalendar-main/tests/src/event-render/eventOrder.ts new file mode 100644 index 0000000..eca16e0 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/eventOrder.ts @@ -0,0 +1,126 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('eventOrder', () => { + pushOptions({ + initialDate: '2018-01-01', + initialView: 'dayGridMonth', + eventDidMount(arg) { + arg.el.setAttribute('data-event-id', arg.event.id) + }, + }) + + describe('when all different start times', () => { + pushOptions({ + events: [ + { id: 'z', title: 'a', start: '2018-01-01T10:00:00' }, + { id: 'y', title: 'b', start: '2018-01-01T09:00:00' }, + { id: 'x', title: 'c', start: '2018-01-01T08:00:00' }, + ], + }) + + it('will sort by start time by default', () => { + initCalendar() + expect(getEventOrder()).toEqual(['x', 'y', 'z']) + }) + }) + + describe('when all the same date', () => { + pushOptions({ + events: [ + { id: 'z', title: 'a', start: '2018-01-01T09:00:00', myOrder: 3 }, + { id: 'y', title: 'b', start: '2018-01-01T09:00:00', myOrder: 1 }, + { id: 'x', title: 'c', start: '2018-01-01T09:00:00', myOrder: 2 }, + ], + }) + + it('sorts by title by default', () => { + initCalendar() + expect(getEventOrder()).toEqual(['z', 'y', 'x']) + }) + + it('can sort by a standard prop', () => { + initCalendar({ + eventOrder: 'id', + }) + expect(getEventOrder()).toEqual(['x', 'y', 'z']) + }) + + it('can sort by a non-standard prop', () => { + initCalendar({ + eventOrder: 'myOrder', + }) + expect(getEventOrder()).toEqual(['y', 'x', 'z']) + }) + }) + + describe('when different dates', () => { + pushOptions({ + events: [ + { id: 'z', title: 'a', start: '2018-01-03T09:00:00', end: '2018-01-06T09:00:00', myOrder: 3 }, + { id: 'y', title: 'b', start: '2018-01-02T09:00:00', end: '2018-01-06T09:00:00', myOrder: 1 }, + { id: 'x', title: 'c', start: '2018-01-01T09:00:00', end: '2018-01-06T09:00:00', myOrder: 2 }, + ], + }) + + it('sorting by a prop will override date-determined order', () => { + initCalendar({ + eventOrder: 'myOrder', + }) + expect(getEventOrder()).toEqual(['y', 'x', 'z']) + }) + }) + + describe('when different durations', () => { + pushOptions({ + events: [ + { id: 'z', title: 'a', start: '2018-01-01T09:00:00', end: '2018-01-04T09:00:00', myOrder: 3 }, // 3 day + { id: 'y', title: 'b', start: '2018-01-01T09:00:00', end: '2018-01-02T09:00:00', myOrder: 1 }, // 1 day + { id: 'x', title: 'c', start: '2018-01-01T09:00:00', end: '2018-01-03T09:00:00', myOrder: 2 }, // 2 day + ], + }) + + it('sorting by a prop will override duration-determined order', () => { + initCalendar({ + eventOrder: 'myOrder', + }) + expect(getEventOrder()).toEqual(['y', 'x', 'z']) + }) + }) + + describe('when long event split across weeks', () => { + pushOptions({ + events: [ + { id: 'x', start: '2018-01-06', end: '2018-01-08' }, + { id: 'y', start: '2018-01-06', end: '2018-01-07' }, + { id: 'z', start: '2018-01-07', end: '2018-01-08' }, + ], + }) + + it('should prioritize eventOrder duration', () => { + let calendar = initCalendar({ + eventOrder: '-duration', + }) + let dayGrid = new DayGridViewWrapper(calendar).dayGrid + let rowEls = dayGrid.getRowEls() + let xEvent0 = rowEls[0].querySelector('[data-event-id="x"]') + let xEvent1 = rowEls[1].querySelector('[data-event-id="x"]') + let yEvent = rowEls[0].querySelector('[data-event-id="y"]') + let zEvent = rowEls[1].querySelector('[data-event-id="z"]') + + expect(xEvent0.getBoundingClientRect().top) + .toBeLessThan(yEvent.getBoundingClientRect().top) + expect(xEvent1.getBoundingClientRect().top) + .toBeLessThan(zEvent.getBoundingClientRect().top) + }) + }) + + function getEventOrder() { + let objs = new CalendarWrapper(currentCalendar).getEventEls().map((el) => ({ + id: el.getAttribute('data-event-id'), + top: el.getBoundingClientRect().top, + })) + objs.sort((a, b) => a.top - b.top) + return objs.map((obj) => obj.id) + } +}) diff --git a/fullcalendar-main/tests/src/event-render/list-events.ts b/fullcalendar-main/tests/src/event-render/list-events.ts new file mode 100644 index 0000000..7b6d933 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/list-events.ts @@ -0,0 +1,20 @@ +import { ListViewWrapper } from '../lib/wrappers/ListViewWrapper.js' + +describe('list-view event rendering', () => { + // https://github.com/fullcalendar/fullcalendar/issues/6486 + it('renders events starting yesterday, ending at midnight, as "past"', () => { + let calendar = initCalendar({ + initialView: 'listMonth', + initialDate: '2023-04-09', // "today" + now: '2023-04-09', // "today" + events: [{ + start: '2023-04-08', // yesterday + allDay: true, + }], + }) + let wrapper = new ListViewWrapper(calendar) + let eventEls = wrapper.getEventEls() + + expect(eventEls[0]).toHaveClass('fc-event-past') + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/maxTime.ts b/fullcalendar-main/tests/src/event-render/maxTime.ts new file mode 100644 index 0000000..8051e8c --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/maxTime.ts @@ -0,0 +1,48 @@ +import { directionallyTestSeg } from '../lib/DayGridEventRenderUtils.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('event rendering with slotMaxTime', () => { // TODO: rename file + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-03-22', + scrollTime: '00:00', + }) + + describe('when event is within extended slotMaxTime', () => { + pushOptions({ + slotMaxTime: '26:00', + events: [ + { start: '2017-03-22T00:00:00', end: '2017-03-22T02:00:00' }, + ], + }) + + it('renders two event elements in the correct places', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let res = timeGridWrapper.checkEventRendering( + '2017-03-22T00:00:00Z', + '2017-03-22T02:00:00Z', + ) + expect(res.length).toBe(2) + expect(res.isMatch).toBe(true) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4483 + it('displays events on the last day', () => { + initCalendar({ + initialView: 'dayGridWeek', + slotMaxTime: '20:00', + events: [ + { start: '2017-03-19', end: '2017-03-26' }, + ], + }) + + directionallyTestSeg({ + firstCol: 0, + lastCol: 6, + isStart: true, + isEnd: true, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/minTime.ts b/fullcalendar-main/tests/src/event-render/minTime.ts new file mode 100644 index 0000000..557cfcd --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/minTime.ts @@ -0,0 +1,55 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotMinTime', () => { // TODO: rename file + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-03-22', + scrollTime: '00:00', + }) + + describe('event rendering', () => { + describe('when event is within negative slotMinTime', () => { + pushOptions({ + slotMinTime: { hours: -2 }, + events: [ + { start: '2017-03-22T22:00:00', end: '2017-03-23T00:00:00' }, + ], + }) + + it('renders two event elements in the correct places', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let res = timeGridWrapper.checkEventRendering( + '2017-03-22T22:00:00Z', + '2017-03-23T00:00:00Z', + ) + expect(res.length).toBe(2) + expect(res.isMatch).toBe(true) + }) + }) + + describe('when event start cut off by positive slotMinTime', () => { + pushOptions({ + slotMinTime: { hours: 12 }, + events: [ + { start: '2017-03-22T10:00:00', end: '2017-03-22T14:00:00' }, + ], + }) + + it('shows time-text as original event start time', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let timeTexts = timeGridWrapper.getEventTimeTexts() + expect(timeTexts[0]).toBe('10:00 - 2:00') + }) + }) + }) + + it('can be changed dynamically', () => { + let calendar = initCalendar() + currentCalendar.setOption('slotMinTime', '09:00') + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.getTimeAxisInfo()[0].text).toBe('9am') + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/multiMonth-events.ts b/fullcalendar-main/tests/src/event-render/multiMonth-events.ts new file mode 100644 index 0000000..d750bc9 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/multiMonth-events.ts @@ -0,0 +1,45 @@ +import { filterVisibleEls } from '../lib/dom-misc.js' +import { MultiMonthViewWrapper } from '../lib/wrappers/MultiMonthViewWrapper.js' + +describe('multi-month-view event rendering', () => { + // https://github.com/fullcalendar/fullcalendar/issues/7573 + it('will not incorrectly put events under +more link', () => { + const calendarEl = document.createElement('div') + calendarEl.style.width = '1200px' + calendarEl.style.maxWidth = '1200px' + document.body.appendChild(calendarEl) + + const calendar = initCalendar({ + initialView: 'multiMonthYear', + initialDate: '2024-01-15', + multiMonthMaxColumns: 2, + events: [ + { + title: 'event 1', + start: '2024-01-15', + end: '2024-01-20', + }, + { + title: 'event 2', + start: '2024-01-15', + end: '2024-01-20', + }, + { + title: 'event 3', + start: '2024-01-15', + }, + ], + }, calendarEl) + + const viewWrapper = new MultiMonthViewWrapper(calendar) + const dayGridWrapper = viewWrapper.getDayGrid(0) + const visibleEventEls = filterVisibleEls(dayGridWrapper.getEventEls()) + const moreEls = dayGridWrapper.getMoreEls() + const moreTexts = moreEls.map((moreEl) => moreEl.innerText) + + expect(visibleEventEls.length).toBe(2) + expect(moreTexts).toEqual(['+2 more']) + + document.body.removeChild(calendarEl) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/print-preview.ts b/fullcalendar-main/tests/src/event-render/print-preview.ts new file mode 100644 index 0000000..12a9c5e --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/print-preview.ts @@ -0,0 +1,51 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('print preview', () => { + pushOptions({ + initialDate: '2019-04-08', + scrollTime: '00:00', + events: [ + { id: '2', start: '2019-04-08T05:00:00' }, + { id: '1', start: '2019-04-08T01:00:00' }, + ], + eventDidMount(arg) { + arg.el.setAttribute('data-id', arg.event.id) + }, + }) + + describeOptions('initialView', { + 'with timeGrid view': 'timeGridDay', + 'with dayGrid view': 'dayGridDay', + }, () => { + it('orders events in DOM by start time', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + let eventEls = calendarWrapper.getEventEls() + + let ids = eventEls.map((el) => el.getAttribute('data-id')) + + expect(ids).toEqual(['1', '2']) + }) + }) + + describeOptions('initialView', { + 'with timeGrid view': 'timeGridWeek', + 'with dayGrid view': 'dayGridDay', + }, () => { + // https://github.com/fullcalendar/fullcalendar/issues/5709 + it('orders by start time when in actually printing', (done) => { + let calendar = initCalendar() + calendar.trigger('_beforeprint') + + setTimeout(() => { + let calendarWrapper = new CalendarWrapper(calendar) + let eventEls = calendarWrapper.getEventEls() + + let ids = eventEls.map((el) => el.getAttribute('data-id')) + + expect(ids).toEqual(['1', '2']) + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/timeGrid-events-short.ts b/fullcalendar-main/tests/src/event-render/timeGrid-events-short.ts new file mode 100644 index 0000000..010d45c --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/timeGrid-events-short.ts @@ -0,0 +1,31 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { queryEventElInfo } from '../lib/wrappers/TimeGridWrapper.js' + +describe('short timegrid events', () => { + it('gets corrected className when short, by default', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-08-10', + events: [ + { start: '2017-08-10T10:30:00', end: '2017-08-10T10:31:00', title: 'event a' }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + expect(queryEventElInfo(eventEls[0]).isShort).toBe(true) + }) + + it('can apply short className when customized to be larger', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-08-10', + eventShortHeight: 200, + events: [ + { start: '2017-08-10T10:30:00', end: '2017-08-10T12:30:00', title: 'event a' }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + expect(queryEventElInfo(eventEls[0]).isShort).toBe(true) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/timeGrid-events.ts b/fullcalendar-main/tests/src/event-render/timeGrid-events.ts new file mode 100644 index 0000000..4a97acc --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/timeGrid-events.ts @@ -0,0 +1,170 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { anyElsObscured } from '../lib/dom-geom.js' + +describe('timeGrid event rendering', () => { + // https://github.com/fullcalendar/fullcalendar/issues/6019 + it('renders without intersecting when sorted by title', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2020-12-15', + scrollTime: '05:00', + eventOrder: 'title,-allDay,start,-duration', + slotEventOverlap: false, + events: [ + { + title: 'a', + start: '2020-12-15 15:00:00', + end: '2020-12-15 18:00:00', + }, + { + title: 'b', + start: '2020-12-15 05:00:00', + end: '2020-12-15 08:00:00', + }, + { + title: 'c', + start: '2020-12-15 09:00:00', + end: '2020-12-15 12:00:00', + }, + { + title: 'd', + start: '2020-12-15 05:00:00', + end: '2020-12-15 09:00:00', + }, + { + title: 'e', + start: '2020-12-15 05:00:00', + end: '2020-12-15 08:00:00', + }, + { + color: 'red', + title: 'f', + start: '2020-12-15 08:00:00', + end: '2020-12-15 12:00:00', + }, + { + title: 'g', + start: '2020-12-15 08:00:00', + end: '2020-12-15 17:30:00', + }, + ], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + let obscured = anyElsObscured(eventEls) + + expect(obscured).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/2758 + it('renders without intersecting for certain arrangement', () => { + let calendar = initCalendar({ + initialDate: '2015-04-22', + initialView: 'timeGridDay', + scrollTime: '09:00', + slotEventOverlap: false, + editable: true, + events: [ + { + title: 'A', + start: '2015-04-22 10:00:00', + end: '2015-04-22 13:00:00', + }, + { + title: 'B', + start: '2015-04-22 13:00:00', + end: '2015-04-22 13:30:00', + }, + { + title: 'C', + start: '2015-04-22 10:00:00', + end: '2015-04-22 11:00:00', + }, + { + title: 'D', + start: '2015-04-22 22:00:00', + end: '2015-04-22 23:00:00', + }, + { + title: 'E', + start: '2015-04-22 10:00:00', + end: '2015-04-22 14:00:00', + }, + { + title: 'F', + start: '2015-04-22 14:00:00', + end: '2015-04-22 15:30:00', + }, + { + title: 'G', + start: '2015-04-22 22:00:00', + end: '2015-04-22 23:00:00', + }, + { + title: 'H', + start: '2015-04-22 22:00:00', + end: '2015-04-22 23:00:00', + }, + { + title: 'I', + start: '2015-04-22 15:00:00', + end: '2015-04-22 23:30:00', + }, + { + title: 'J', + start: '2015-04-22 10:00:00', + end: '2015-04-22 15:30:00', + }, + { + title: 'K', + start: '2015-04-22 22:00:00', + end: '2015-04-22 23:00:00', + }, + { + title: 'L', + start: '2015-04-22 12:00:00', + end: '2015-04-22 15:00:00', + }, + ], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + let obscured = anyElsObscured(eventEls) + + expect(obscured).toBe(false) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5004 + it('renders event widths somewhat equally', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + initialDate: '2019-08-01', + slotDuration: '00:15:00', + slotEventOverlap: false, + events: [ + { start: '2019-08-01 08:00', end: '2019-08-01 09:00' }, + { start: '2019-08-01 08:00', end: '2019-08-01 09:00' }, + { start: '2019-08-01 08:30', end: '2019-08-01 09:30' }, + { start: '2019-08-01 09:00', end: '2019-08-01 10:00' }, + { start: '2019-08-01 09:00', end: '2019-08-01 10:00' }, + { start: '2019-08-01 09:30', end: '2019-08-01 10:30' }, + { start: '2019-08-01 09:30', end: '2019-08-01 10:30' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + { start: '2019-08-01 10:00', end: '2019-08-01 11:00' }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + let eventWidths = eventEls.map((eventEl) => eventEl.getBoundingClientRect().width) + eventWidths.sort() // sorts highest to lowest + eventWidths.splice(0, 1) // remove first item, which is exceptionally wide event + expect(Math.abs(eventWidths[0] - eventWidths[eventWidths.length - 1])).toBeLessThan(1) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/timeText.ts b/fullcalendar-main/tests/src/event-render/timeText.ts new file mode 100644 index 0000000..bee4182 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/timeText.ts @@ -0,0 +1,45 @@ +import { FormatterInput } from '@fullcalendar/core' +import { parseLocalDate } from '../lib/date-parsing.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('the time text on events', () => { + describe('in week', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-07-03', + scrollTime: '00:00', + }) + + it('renders segs with correct local timezone', () => { + const FORMAT: FormatterInput = { + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + } + + let calendar = initCalendar({ + timeZone: 'local', + eventTimeFormat: FORMAT, + events: [ + { start: '2017-07-03T23:00:00', end: '2017-07-04T13:00:00' }, + ], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let timeText = timeGridWrapper.getEventTimeTexts() + + expect(timeText).toEqual([ + currentCalendar.formatRange( + parseLocalDate('2017-07-03T23:00:00'), + parseLocalDate('2017-07-04T00:00:00'), + FORMAT, + ), + currentCalendar.formatRange( + parseLocalDate('2017-07-04T00:00:00'), + parseLocalDate('2017-07-04T13:00:00'), + FORMAT, + ), + ]) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-render/validRange.ts b/fullcalendar-main/tests/src/event-render/validRange.ts new file mode 100644 index 0000000..8cf5b15 --- /dev/null +++ b/fullcalendar-main/tests/src/event-render/validRange.ts @@ -0,0 +1,63 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('validRange event rendering', () => { + describe('with start constraint', () => { + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { start: '2017-06-07' }, + }) + + describe('when event is partially before', () => { + pushOptions({ + events: [ + { start: '2017-06-05', end: '2017-06-09' }, + ], + }) + + it('truncates the event\'s beginning', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.isStart).toBe(false) + expect(eventInfo.isEnd).toBe(true) + // TODO: more test about positioning + }) + }) + }) + }) + + describe('with end constraint', () => { + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { end: '2017-06-07' }, + }) + + describe('when event is partially before', () => { + pushOptions({ + events: [ + { start: '2017-06-05', end: '2017-06-09' }, + ], + }) + + it('truncates the event\'s end', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.isStart).toBe(true) + expect(eventInfo.isEnd).toBe(false) + // TODO: more test about positioning + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-resize/eventResizableFromStart.ts b/fullcalendar-main/tests/src/event-resize/eventResizableFromStart.ts new file mode 100644 index 0000000..f70b7aa --- /dev/null +++ b/fullcalendar-main/tests/src/event-resize/eventResizableFromStart.ts @@ -0,0 +1,38 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventResize } from '../lib/wrappers/interaction-util.js' + +describe('eventResizableFromStart', () => { + pushOptions({ + editable: true, + eventResizableFromStart: true, + }) + + describe('for DayGrid', () => { + pushOptions({ + initialDate: '2019-08-26', + initialView: 'dayGridMonth', + events: [ + { start: '2019-08-27', title: 'all day event' }, + ], + }) + + it('allows resizing from start', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + let resizing = dayGridWrapper.resizeEvent( + dayGridWrapper.getEventEls()[0], + '2019-08-27', + '2019-08-26', + true, // resize-from-start + ) + + waitEventResize(calendar, resizing).then(() => { + let event = calendar.getEvents()[0] + expect(event.start).toEqualDate('2019-08-26') + expect(event.end).toEqualDate('2019-08-28') + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-resize/mirror-hooks.ts b/fullcalendar-main/tests/src/event-resize/mirror-hooks.ts new file mode 100644 index 0000000..afb03ac --- /dev/null +++ b/fullcalendar-main/tests/src/event-resize/mirror-hooks.ts @@ -0,0 +1,110 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { waitEventResize } from '../lib/wrappers/interaction-util.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('event resize mirror', () => { + pushOptions({ + editable: true, + initialDate: '2018-12-25', + eventDragMinDistance: 0, // so mirror will render immediately upon mousedown + }) + + describe('in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + events: [ + { start: '2018-12-03', title: 'all day event' }, + ], + }) + + it('gets passed through render hooks', (done) => { + let mirrorMountCalls = 0 + let mirrorContentCalls = 0 + let mirrorUnmountCalls = 0 + + let calendar = initCalendar({ + eventDidMount(info) { + if (info.isMirror) { + mirrorMountCalls += 1 + } + }, + eventContent(info) { + if (info.isMirror) { + mirrorContentCalls += 1 + } + }, + eventWillUnmount(info) { + if (info.isMirror) { + mirrorUnmountCalls += 1 + } + }, + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let resizing = dayGridWrapper.resizeEvent( // drag TWO days + dayGridWrapper.getEventEls()[0], + '2018-12-03', + '2018-12-05', + ) + + waitEventResize(calendar, resizing).then(() => { + expect(mirrorMountCalls).toBe(1) + expect(mirrorContentCalls).toBe(3) + expect(mirrorUnmountCalls).toBe(1) + done() + }) + }) + }) + + describe('in timeGrid view', () => { + pushOptions({ + initialView: 'timeGridWeek', + scrollTime: '00:00', + slotDuration: '01:00', + snapDuration: '01:00', + events: [ + { start: '2018-12-25T01:00:00', end: '2018-12-25T02:00:00', title: 'timed event' }, + ], + }) + + it('gets passed through eventWillUnmount', (done) => { + let mirrorMountCalls = 0 + let mirrorContentCalls = 0 + let mirrorUnmountCalls = 0 + + let calendar = initCalendar({ + eventDidMount(info) { + if (info.isMirror) { + mirrorMountCalls += 1 + } + }, + eventContent(info) { + if (info.isMirror) { + mirrorContentCalls += 1 + } + }, + eventWillUnmount(info) { + if (info.isMirror) { + mirrorUnmountCalls += 1 + } + }, + }) + + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEvent( + eventEl, + '2018-12-25T02:00:00', + '2018-12-25T04:00:00', // drag TWO snaps + ) + + waitEventResize(calendar, resizing).then(() => { + expect(mirrorMountCalls).toBe(1) + expect(mirrorContentCalls).toBe(3) + expect(mirrorUnmountCalls).toBe(1) + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-resize/mirror-rendering.ts b/fullcalendar-main/tests/src/event-resize/mirror-rendering.ts new file mode 100644 index 0000000..d247600 --- /dev/null +++ b/fullcalendar-main/tests/src/event-resize/mirror-rendering.ts @@ -0,0 +1,37 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventResize } from '../lib/wrappers/interaction-util.js' + +describe('event mirror rendering', () => { + pushOptions({ + editable: true, + }) + + it('maintains vertical position while dragging', (done) => { + let calendar = initCalendar({ + initialDate: '2019-08-26', + initialView: 'dayGridMonth', + eventOrder: 'title', + events: [ + { start: '2019-08-27', title: 'event0' }, + { start: '2019-08-27', title: 'event1' }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + let resizing = dayGridWrapper.resizeEvent( + eventEls[1], + '2019-08-27', + '2019-08-28', + false, // resize-from-start + () => { // onBeforeRelease + let mirrorEls = dayGridWrapper.getMirrorEls() + expect(mirrorEls[0].getBoundingClientRect().top).toBe( + eventEls[1].getBoundingClientRect().top, + ) + }, + ) + + waitEventResize(calendar, resizing).then(() => done()) + }) +}) diff --git a/fullcalendar-main/tests/src/event-resize/validRange.ts b/fullcalendar-main/tests/src/event-resize/validRange.ts new file mode 100644 index 0000000..e19cf71 --- /dev/null +++ b/fullcalendar-main/tests/src/event-resize/validRange.ts @@ -0,0 +1,28 @@ +import * as EventResizeUtils from '../lib/EventResizeUtils.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('validRange event resizing', () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { end: '2017-06-09' }, + events: [ + { start: '2017-06-04', end: '2017-06-07' }, + ], + editable: true, + }) + + it('won\'t go after validRange', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + EventResizeUtils.resize( + dayGridWrapper.getDayEl('2017-06-06').getBoundingClientRect(), + dayGridWrapper.getDisabledDayEls()[0].getBoundingClientRect(), // where Jun 9th would be + ).then((res) => { + expect(res).toBe(false) + }).then(() => done()) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/Calendar.addEventSource.ts b/fullcalendar-main/tests/src/event-source/Calendar.addEventSource.ts new file mode 100644 index 0000000..70aa243 --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/Calendar.addEventSource.ts @@ -0,0 +1,29 @@ +describe('addEventSource', () => { + it('will accept a processed api object after it was removed', () => { + initCalendar({ + eventSources: [ + { id: 'sourceA', events: [] }, + ], + }) + expect(currentCalendar.getEventSources().length).toBe(1) + let source = currentCalendar.getEventSourceById('sourceA') + source.remove() + expect(currentCalendar.getEventSources().length).toBe(0) + let newSource = currentCalendar.addEventSource(source) + expect(currentCalendar.getEventSources().length).toBe(1) + expect(newSource).toBe(source) + }) + + it('won\'t re-add a source that it already has', () => { + initCalendar({ + eventSources: [ + { id: 'sourceA', events: [] }, + ], + }) + expect(currentCalendar.getEventSources().length).toBe(1) + let source = currentCalendar.getEventSourceById('sourceA') + let newSource = currentCalendar.addEventSource(source) + expect(currentCalendar.getEventSources().length).toBe(1) + expect(newSource).toBe(source) + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/eventSourceSuccess.ts b/fullcalendar-main/tests/src/event-source/eventSourceSuccess.ts new file mode 100644 index 0000000..53bb4b7 --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/eventSourceSuccess.ts @@ -0,0 +1,37 @@ +describe('eventSourceSuccess', () => { + const FETCH_FUNC = (info, successCallback) => { + successCallback({ + something: [ + { title: 'hi', start: '2018-10-01' }, + ], + }) + } + + const TRANSFORM = (input) => input.something + + pushOptions({ + initialDate: '2018-10-01', + }) + + it('massages event data with calendar-wide setting', () => { + initCalendar({ + eventSources: [FETCH_FUNC], + eventSourceSuccess: TRANSFORM, + }) + + expect(currentCalendar.getEvents().length).toBe(1) + }) + + it('massages event data with source setting', () => { + initCalendar({ + eventSources: [ + { + events: FETCH_FUNC, + success: TRANSFORM, + }, + ], + }) + + expect(currentCalendar.getEvents().length).toBe(1) + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/getEventSourceById.ts b/fullcalendar-main/tests/src/event-source/getEventSourceById.ts new file mode 100644 index 0000000..98f9e3c --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/getEventSourceById.ts @@ -0,0 +1,10 @@ +describe('getEventSourceById', () => { + xit('correctly retrieves an event source provided via `events` at initialization', () => { + }) + + xit('correctly retrieves an event source provided via `eventSources` at initialization', () => { + }) + + xit('correctly retrieves an event source provided via `addEventSource` method', () => { + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/getEventSources.ts b/fullcalendar-main/tests/src/event-source/getEventSources.ts new file mode 100644 index 0000000..2b125cd --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/getEventSources.ts @@ -0,0 +1,10 @@ +describe('getEventSources', () => { + xit('correctly retrieves event sources provided via `events` at initialization', () => { + }) + + xit('correctly retrieves event sources provided via `eventSources` at initialization', () => { + }) + + xit('correctly retrieves event sources provided via `addEventSource` method', () => { + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/navigation.ts b/fullcalendar-main/tests/src/event-source/navigation.ts new file mode 100644 index 0000000..b7c3698 --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/navigation.ts @@ -0,0 +1,40 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('event fetching while date-navigating', () => { + // https://github.com/fullcalendar/fullcalendar/issues/4975 + it('renders events when doing next() and then prev()', (done) => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2020-02-11', + events(arg, callback) { + if (arg.startStr.indexOf('2020-01-26') === 0) { // for Feb + setTimeout(() => { + callback([ + { start: '2020-02-15' }, // middle of month + ]) + }, 100) + } else if (arg.startStr.indexOf('2020-03-01') === 0) { // for March + setTimeout(() => { + callback([ + { start: '2020-03-15' }, // middle of month + ]) + }, 100) + } else { + throw new Error('bad range') + } + }, + }) + let calendarWrapper = new CalendarWrapper(calendar) + + setTimeout(() => { + currentCalendar.next() + setTimeout(() => { + currentCalendar.prev() + setTimeout(() => { + expect(calendarWrapper.getEventEls().length).toBe(1) + done() + }, 1000) // after everything + }, 50) // before second fetch finishes + }, 200) // let first fetch finish + }) +}) diff --git a/fullcalendar-main/tests/src/event-source/refetch.ts b/fullcalendar-main/tests/src/event-source/refetch.ts new file mode 100644 index 0000000..3c93627 --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/refetch.ts @@ -0,0 +1,132 @@ +describe('event source refetch', () => { + const OPTIONS = { + now: '2015-08-07', + initialView: 'timeGridDay', + scrollTime: '00:00', + } + + describe('with a single event source', () => { // reword this stuff + it('will be refetched', () => { + let fetchConfig = { eventCount: 1, fetchId: 7 } + let calendar = initWithSources(fetchConfig) + + expect($('.source1-7').length).toEqual(1) + expect($('.source2-7').length).toEqual(1) + expect($('.source3-7').length).toEqual(1) + + fetchConfig.eventCount = 2 + fetchConfig.fetchId = 8 + calendar.getEventSourceById('blue').refetch() + + // events from unaffected sources remain + expect($('.source1-7').length).toEqual(1) + expect($('.source3-7').length).toEqual(1) + + // events from old fetch were cleared + expect($('.source2-7').length).toEqual(0) + + // events from new fetch were rendered + expect($('.source2-8').length).toEqual(2) + }) + }) + + describe('multiple event sources', () => { + it('will be refetched', () => { + let fetchConfig = { eventCount: 1, fetchId: 7 } + let calendar = initWithSources(fetchConfig) + + expect($('.source1-7').length).toEqual(1) + expect($('.source2-7').length).toEqual(1) + expect($('.source3-7').length).toEqual(1) + + fetchConfig.eventCount = 2 + fetchConfig.fetchId = 8 + calendar.getEventSourceById('green0').refetch() + calendar.getEventSourceById('green1').refetch() + + // events from unaffected sources remain + expect($('.source2-7').length).toEqual(1) + + // events from old fetch were cleared + expect($('.source1-7').length).toEqual(0) + expect($('.source3-7').length).toEqual(0) + + // events from new fetch were rendered + expect($('.source1-8').length).toEqual(2) + expect($('.source3-8').length).toEqual(2) + }) + }) + + describe('when called while initial fetch is still pending', () => { + it('keeps old events and rerenders new', (done) => { + let fetchConfig = { eventCount: 1, fetchId: 7, fetchDelay: 100 } + let calendar = initWithSources(fetchConfig) + + fetchConfig.eventCount = 2 + fetchConfig.fetchId = 8 + calendar.getEventSourceById('green0').refetch() + calendar.getEventSourceById('green1').refetch() + + setTimeout(() => { + // events from unaffected sources remain + expect($('.source2-7').length).toEqual(1) + + // events from old fetch were cleared + expect($('.source1-7').length).toEqual(0) + expect($('.source3-7').length).toEqual(0) + + // events from new fetch were rendered + expect($('.source1-8').length).toEqual(2) + expect($('.source3-8').length).toEqual(2) + + done() + }, fetchConfig.fetchDelay + 1) + }) + }) + + function initWithSources(fetchConfig) { + return initCalendar({ + ...OPTIONS, + eventSources: [ + { + id: 'green0', + events: createEventGenerator('source1-', fetchConfig), + color: 'green', + }, + { + id: 'blue', + events: createEventGenerator('source2-', fetchConfig), + color: 'blue', + }, + { + id: 'green1', + events: createEventGenerator('source3-', fetchConfig), + color: 'green', + }, + ], + }) + } + + function createEventGenerator(classNamePrefix, fetchConfig) { + return (arg, callback) => { + let events = [] + + for (let i = 0; i < fetchConfig.eventCount; i += 1) { + events.push({ + start: '2015-08-07T02:00:00', + end: '2015-08-07T03:00:00', + className: classNamePrefix + fetchConfig.fetchId, + title: classNamePrefix + fetchConfig.fetchId, // also make it the title + }) + } + + if (fetchConfig.fetchDelay) { + setTimeout(() => { + callback(events) + }, fetchConfig.fetchDelay) + } else { + callback(events) + } + } + } +}) diff --git a/fullcalendar-main/tests/src/event-source/remove.ts b/fullcalendar-main/tests/src/event-source/remove.ts new file mode 100644 index 0000000..a221ff2 --- /dev/null +++ b/fullcalendar-main/tests/src/event-source/remove.ts @@ -0,0 +1,69 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('event source remove', () => { + pushOptions({ + initialDate: '2014-08-01', + }) + + it('correctly removes events provided via `eventSources` at initialization', () => { + let calendar = initCalendar({ + eventSources: [{ + id: '5', + events: [ + { title: 'event1', start: '2014-08-01' }, + { title: 'event2', start: '2014-08-02' }, + ], + }], + }) + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendar.getEvents().length).toBe(2) + expect(calendarWrapper.getEventEls().length).toBe(2) + + calendar.getEventSourceById('5').remove() + + expect(calendar.getEvents().length).toBe(0) + expect(calendarWrapper.getEventEls().length).toBe(0) + }) + + it('won\'t render removed events when subsequent addEventSource', (done) => { + let source1 = { + id: '1', + events(arg, callback) { + setTimeout(() => { + callback([{ + title: 'event1', + className: 'event1', + start: '2014-08-01T02:00:00', + }]) + }, 100) + }, + } + + let source2 = { + id: '2', + events(arg, callback) { + setTimeout(() => { + callback([{ + title: 'event2', + className: 'event2', + start: '2014-08-01T02:00:00', + }]) + }, 100) + }, + } + + let calendar = initCalendar({ + eventSources: [source1], + }) + + calendar.getEventSourceById('1').remove() + calendar.addEventSource(source2) + + setTimeout(() => { + expect($('.event1').length).toBe(0) + expect($('.event2').length).toBe(1) + done() + }, 101) + }) +}) diff --git a/fullcalendar-main/tests/src/global-locale.js b/fullcalendar-main/tests/src/global-locale.js new file mode 100644 index 0000000..74a5728 --- /dev/null +++ b/fullcalendar-main/tests/src/global-locale.js @@ -0,0 +1,15 @@ + +describe('FullCalendar single locale', () => { + it('loads correctly', () => { + const el = document.createElement('div') + document.body.appendChild(el) + + const calendar = new FullCalendar.Calendar(el) + const availableLocales = calendar.getAvailableLocaleCodes() + + expect(availableLocales.indexOf('ar') !== -1).toBe(true) + + calendar.destroy() + document.body.removeChild(el) + }) +}) diff --git a/fullcalendar-main/tests/src/global-locales-all.js b/fullcalendar-main/tests/src/global-locales-all.js new file mode 100644 index 0000000..e90428c --- /dev/null +++ b/fullcalendar-main/tests/src/global-locales-all.js @@ -0,0 +1,16 @@ + +describe('FullCalendar locales-all', () => { + it('loads correctly', () => { + const el = document.createElement('div') + document.body.appendChild(el) + + const calendar = new FullCalendar.Calendar(el) + const availableLocales = calendar.getAvailableLocaleCodes() + + expect(availableLocales.indexOf('es') !== -1).toBe(true) + expect(availableLocales.indexOf('fr') !== -1).toBe(true) + + calendar.destroy() + document.body.removeChild(el) + }) +}) diff --git a/fullcalendar-main/tests/src/icalendar/data/alldayEvent.ts b/fullcalendar-main/tests/src/icalendar/data/alldayEvent.ts new file mode 100644 index 0000000..e9c417b --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/alldayEvent.ts @@ -0,0 +1,21 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190415 +DTSTAMP:20201006T124223Z +UID:1234578 +CREATED:20190408T110429Z +DESCRIPTION:this is the description +URL:https://fullcalendar.io/ +LAST-MODIFIED:20190409T110738Z +LOCATION:this is the location +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:First conference +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/meetingWithMungedStart.ts b/fullcalendar-main/tests/src/icalendar/data/meetingWithMungedStart.ts new file mode 100644 index 0000000..9bf6237 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/meetingWithMungedStart.ts @@ -0,0 +1,22 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART:20190415TRRRRRRZ +DTEND:20190415T109900Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Munged meeting (No DTSTART) +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/multidayEvent.ts b/fullcalendar-main/tests/src/icalendar/data/multidayEvent.ts new file mode 100644 index 0000000..b04edbb --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/multidayEvent.ts @@ -0,0 +1,21 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190410 +DTSTAMP:20201006T124223Z +DTEND;VALUE=DATE:20190413 +UID:1234578 +CREATED:20190408T110429Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:First conference +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/multipleEventsOneMunged.ts b/fullcalendar-main/tests/src/icalendar/data/multipleEventsOneMunged.ts new file mode 100644 index 0000000..31d2b87 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/multipleEventsOneMunged.ts @@ -0,0 +1,34 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTEND;VALUE=DATE:20190413 +DTSTAMP:20201006T124223Z +UID:1234578 +CREATED:20190408T110429Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Munged conference (No DTSTART) +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190416 +DTEND;VALUE=DATE:20190417 +DTSTAMP:20201008T153019Z +UID:1234578 +DTSTAMP:20201008T153019Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Valid conference +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/multipleMultidayEvents.ts b/fullcalendar-main/tests/src/icalendar/data/multipleMultidayEvents.ts new file mode 100644 index 0000000..7eb1bbd --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/multipleMultidayEvents.ts @@ -0,0 +1,35 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190410 +DTEND;VALUE=DATE:20190413 +DTSTAMP:20201006T124223Z +UID:1234578 +CREATED:20190408T110429Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:First conference +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190416 +DTEND;VALUE=DATE:20190417 +DTSTAMP:20201008T153019Z +UID:1234578 +DTSTAMP:20201008T153019Z +DESCRIPTION: +LAST-MODIFIED:20190409T110738Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Second conference +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/mungedOneHourMeeting.ts b/fullcalendar-main/tests/src/icalendar/data/mungedOneHourMeeting.ts new file mode 100644 index 0000000..eebedef --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/mungedOneHourMeeting.ts @@ -0,0 +1,21 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTEND:20190415T109900Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Munged meeting (No DTSTART) +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/oneHourMeeting.ts b/fullcalendar-main/tests/src/icalendar/data/oneHourMeeting.ts new file mode 100644 index 0000000..bde6ddb --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/oneHourMeeting.ts @@ -0,0 +1,22 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART:20190415T093000Z +DTEND:20190415T103000Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Hour long meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/recurrenceId.ts b/fullcalendar-main/tests/src/icalendar/data/recurrenceId.ts new file mode 100644 index 0000000..21d0fbf --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/recurrenceId.ts @@ -0,0 +1,60 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//vereinify.com//NONSGML kigkonsult.se iCalcreator 2.39.1// +CALSCALE:GREGORIAN +METHOD:PUBLISH +UID:3ddaed11-9b03-4b3b-a90b-4aa1b8742fff +REFRESH-INTERVAL;VALUE=DURATION:PT15M +X-WR-CALNAME:Example +X-WR-CALDESC:Description +X-WR-RELCALID:3ddaed11-9b03-4b3b-a90b-4aa1b8742fff +X-WR-TIMEZONE:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +X-PUBLISHED-TTL:PT15M: +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:STANDARD +TZNAME:CET +DTSTART:20201025T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RDATE:20211031T030000 +RDATE:20221030T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +DTSTART:20210328T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RDATE:20220327T020000 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:af80d1e9-2adb-4e06-8483-3130c18305b1 +DTSTAMP:20210713T065637Z +CLASS:PUBLIC +CREATED:20210712T150845Z +DTSTART;TZID=Europe/Berlin:20210705T140000 +DTEND;TZID=Europe/Berlin:20210705T143000 +EXDATE;TZID=Europe/Berlin:20210713T140000 +EXDATE;TZID=Europe/Berlin:20210715T140000 +LAST-MODIFIED:20210712T150845Z +RRULE:FREQ=DAILY +SEQUENCE:1 +SUMMARY:Daily Recurring +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +UID:af80d1e9-2adb-4e06-8483-3130c18305b1 +DTSTAMP:20210713T065637Z +CLASS:PUBLIC +CREATED:20210712T150845Z +DTSTART;TZID=Europe/Berlin:20210708T141500 +DTEND;TZID=Europe/Berlin:20210708T144500 +LAST-MODIFIED:20210712T150845Z +RECURRENCE-ID;TZID=Europe/Berlin:20210708T140000 +SEQUENCE:0 +SUMMARY:Daily Recurring (Update-1) +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/recurringWeekly.ts b/fullcalendar-main/tests/src/icalendar/data/recurringWeekly.ts new file mode 100644 index 0000000..1ee808b --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/recurringWeekly.ts @@ -0,0 +1,24 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;TZID=Europe/Zurich:20190401T173000 +DTEND;TZID=Europe/Zurich:20190401T183000 +RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=MO +DTSTAMP:20201006T124223Z +ORGANIZER;CN=Testy McTestface:mailto:test@fullcalendar.test +UID:12345678 +CREATED:20181210T150458Z +DESCRIPTION:this is the description +URL:https://fullcalendar.io/ +LAST-MODIFIED:20190508T170523Z +LOCATION:this is the location +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly Monday meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithCount.ts b/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithCount.ts new file mode 100644 index 0000000..ed29cba --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithCount.ts @@ -0,0 +1,23 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;TZID=Europe/Zurich:20190301T173000 +DTEND;TZID=Europe/Zurich:20190301T183000 +RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=MO;COUNT=9 +DTSTAMP:20201006T124223Z +ORGANIZER;CN=Testy McTestface:mailto:test@fullcalendar.test +UID:12345678 +CREATED:20181210T150458Z +DESCRIPTION: +LAST-MODIFIED:20190508T170523Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly Monday meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithoutEnd.ts b/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithoutEnd.ts new file mode 100644 index 0000000..66892f2 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/recurringWeeklyWithoutEnd.ts @@ -0,0 +1,22 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART;TZID=Europe/Zurich:20190401T173000 +RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=MO +DTSTAMP:20201006T124223Z +ORGANIZER;CN=Testy McTestface:mailto:test@fullcalendar.test +UID:12345678 +CREATED:20181210T150458Z +DESCRIPTION: +LAST-MODIFIED:20190508T170523Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Weekly Monday meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithDuration.ts b/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithDuration.ts new file mode 100644 index 0000000..0f653a4 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithDuration.ts @@ -0,0 +1,22 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART:20190415T093000Z +DURATION:PT4H +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Hour long meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithoutEnd.ts b/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithoutEnd.ts new file mode 100644 index 0000000..1429479 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/data/timedMeetingWithoutEnd.ts @@ -0,0 +1,21 @@ +export default `BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:events@fullcalendar.test +X-WR-TIMEZONE:Europe/Paris +BEGIN:VEVENT +DTSTART:20190415T093000Z +DTSTAMP:20201006T124223Z +UID:12345678 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=test@fullcalendar.test;X-NUM-GUESTS=0:mailto:test@fullcalendar.test +CREATED:20190412T223947Z +DESCRIPTION: +LAST-MODIFIED:20190412T223947Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Hour long meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR` diff --git a/fullcalendar-main/tests/src/icalendar/day-view.ts b/fullcalendar-main/tests/src/icalendar/day-view.ts new file mode 100644 index 0000000..9531dd7 --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/day-view.ts @@ -0,0 +1,224 @@ +import fetchMock from 'fetch-mock' +import timeGridPlugin from '@fullcalendar/timegrid' +import { EventSourceInput } from '@fullcalendar/core' +import iCalendarPlugin from '@fullcalendar/icalendar' +import oneHourMeeting from './data/oneHourMeeting.js' +import recurringWeekly from './data/recurringWeekly.js' +import mungedOneHourMeeting from './data/mungedOneHourMeeting.js' +import meetingWithMungedStart from './data/meetingWithMungedStart.js' +import alldayEvent from './data/alldayEvent.js' +import timedMeetingWithoutEnd from './data/timedMeetingWithoutEnd.js' +import timedMeetingWithDuration from './data/timedMeetingWithDuration.js' +import dataWithRecurrenceId from './data/recurrenceId.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('addICalEventSource with day view', () => { + const ICAL_MIME_TYPE = 'text/calendar' + + pushOptions({ + plugins: [iCalendarPlugin, timeGridPlugin], + initialDate: '2019-04-15', // The start of the week for oneHourMeeting + initialView: 'timeGridDay', + timeZone: 'Europe/Paris', + }) + + afterEach(() => { + fetchMock.restore() + }) + + it('adds a one-hour long meeting', (done) => { + loadICalendarWith(oneHourMeeting, () => { + setTimeout(() => { + assertEventCount(1) + done() + }, 100) + }) + }) + + it('adds a repeating weekly meeting', (done) => { + loadICalendarWith(recurringWeekly, () => { + setTimeout(() => { + assertEventCount(1) + const event = currentCalendar.getEvents()[0] + // test non-date props + expect(event.title).toBe('Weekly Monday meeting') + expect(event.url).toBe('https://fullcalendar.io/') + expect(event.extendedProps.description).toBe('this is the description') + expect(event.extendedProps.location).toBe('this is the location') + done() + }, 100) + }) + }) + + it('adds an all day event', (done) => { + loadICalendarWith(alldayEvent, () => { + setTimeout(() => { + assertEventCount(1) + const events = currentCalendar.getEvents() + events.forEach((event) => expect(event.allDay).toBeTruthy()) + // test non-date props + expect(events[0].title).toBe('First conference') + expect(events[0].url).toBe('https://fullcalendar.io/') + expect(events[0].extendedProps.description).toBe('this is the description') + expect(events[0].extendedProps.location).toBe('this is the location') + done() + }, 100) + }) + }) + + it('ignores a munged event', (done) => { + loadICalendarWith(mungedOneHourMeeting, () => { + setTimeout(() => { + assertEventCount(0) + done() + }, 100) + }) + }) + + it('ignores a meeting with a munged start', (done) => { + loadICalendarWith(meetingWithMungedStart, () => { + setTimeout(() => { + assertEventCount(0) + done() + }, 100) + }) + }) + + it('sets default duration when forceEventDuration is enabled and no end or duration included in the VEVENT', (done) => { + loadICalendarWith( + timedMeetingWithoutEnd, + () => { + setTimeout(() => { + assertEventCount(1) + const event = currentCalendar.getEvents()[0] + expect(event.end.getHours()).toEqual(event.start.getHours() + 3) + done() + }, 100) + }, + (source) => { + initCalendar({ + forceEventDuration: true, + defaultTimedEventDuration: '03:00', + }).addEventSource(source) + }, + ) + }) + + it('sets end to null when forceEventDuration is disabled and no end or duration included in the VEVENT', (done) => { + loadICalendarWith( + timedMeetingWithoutEnd, + () => { + setTimeout(() => { + assertEventCount(1) + const event = currentCalendar.getEvents()[0] + expect(event.end).toBe(null) + done() + }, 100) + }, + (source) => { + initCalendar({ + defaultTimedEventDuration: '03:00', + forceEventDuration: false, + }).addEventSource(source) + }, + ) + }) + + it('does not override iCal DURATION in VEVENT', (done) => { + loadICalendarWith( + timedMeetingWithDuration, + () => { + setTimeout(() => { + assertEventCount(1) + const event = currentCalendar.getEvents()[0] + expect(event.end.getHours()).toEqual(event.start.getHours() + 4) + done() + }, 100) + }, + (source) => { + initCalendar({ + forceEventDuration: true, + defaultTimedEventDuration: '03:00', + }).addEventSource(source) + }, + ) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6451 + it('respects RECURRENCE-ID and does not render double events', (done) => { + loadICalendarWith( + dataWithRecurrenceId, + () => { + setTimeout(() => { + let timeGridWrapper = new TimeGridViewWrapper(currentCalendar).timeGrid + let eventEls = timeGridWrapper.getEventEls() + expect(eventEls.length).toBe(1) + done() + }, 100) + }, + (source) => { + initCalendar({ + initialDate: '2021-07-08', + }).addEventSource(source) + }, + ) + }) + + it('does not reload data on next', (done) => { + let requestCnt = 0 + + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, () => { + requestCnt++ + return { + headers: { 'content-type': ICAL_MIME_TYPE }, + body: timedMeetingWithDuration, + } + }) + + initCalendar().addEventSource({ url: givenUrl, format: 'ics' } as EventSourceInput) + + setTimeout(() => { + assertEventCount(1) + currentCalendar.next() + expect(requestCnt).toBe(1) + done() + }, 100) + }) + + function loadICalendarWith( + rawICal: string, + assertions: () => void, + calendarSetup?: (source: EventSourceInput) => void, + ) { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { + headers: { 'content-type': ICAL_MIME_TYPE }, + body: rawICal, + }) + + const source = { url: givenUrl, format: 'ics' } as EventSourceInput + + if (calendarSetup) { + calendarSetup(source) + } else { + initCalendar().addEventSource(source) + } + + const [requestUrl] = fetchMock.lastCall() + const requestParamStr = new URL(requestUrl).searchParams.toString() + expect(requestParamStr).toBe('') + + assertions() + } + + // Checks to make sure all events have been rendered and that the calendar + // has internal info on all the events. + function assertEventCount(expectedCount: number) { + expect(currentCalendar.getEvents().length).toEqual(expectedCount) + + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(expectedCount) + } +}) diff --git a/fullcalendar-main/tests/src/icalendar/month-view.ts b/fullcalendar-main/tests/src/icalendar/month-view.ts new file mode 100644 index 0000000..1ab5a4c --- /dev/null +++ b/fullcalendar-main/tests/src/icalendar/month-view.ts @@ -0,0 +1,211 @@ +import fetchMock from 'fetch-mock' +import dayGridMonth from '@fullcalendar/daygrid' +import { EventSourceInput } from '@fullcalendar/core' +import iCalendarPlugin from '@fullcalendar/icalendar' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import alldayEvent from './data/alldayEvent.js' +import multidayEvent from './data/multidayEvent.js' +import multipleMultidayEvents from './data/multipleMultidayEvents.js' +import multipleEventsOneMunged from './data/multipleEventsOneMunged.js' +import oneHourMeeting from './data/oneHourMeeting.js' +import recurringWeekly from './data/recurringWeekly.js' +import recurringWeeklyWithoutEnd from './data/recurringWeeklyWithoutEnd.js' +import recurringWeeklyWithCount from './data/recurringWeeklyWithCount.js' +import mungedOneHourMeeting from './data/mungedOneHourMeeting.js' + +describe('addICalEventSource with month view', () => { + const ICAL_MIME_TYPE = 'text/calendar' + + pushOptions({ + plugins: [iCalendarPlugin, dayGridMonth], + initialDate: '2019-04-10', // the start of the three-day event in the feed + initialView: 'dayGridMonth', + }) + + afterEach(() => { + fetchMock.restore() + }) + + it('adds an all day event', (done) => { + loadICalendarWith(alldayEvent, () => { + setTimeout(() => { + let events = currentCalendar.getEvents() + expect(events[0].end).toBe(null) + events.forEach((event) => expect(event.allDay).toBeTruthy()) + assertEventCount(1) + done() + }, 100) + }) + }) + + it('adds a single multi-day event', (done) => { + loadICalendarWith(multidayEvent, () => { + setTimeout(() => { + assertEventCount(1) + currentCalendar.getEvents().forEach((event) => expect(event.allDay).toBeTruthy()) + done() + }, 100) + }) + }) + + it('adds multiple multi-day events', (done) => { + loadICalendarWith(multipleMultidayEvents, () => { + setTimeout(() => { + assertEventCount(2) + currentCalendar.getEvents().forEach((event) => expect(event.allDay).toBeTruthy()) + done() + }, 100) + }) + }) + + it('adds a one-hour long meeting', (done) => { + loadICalendarWith(oneHourMeeting, () => { + setTimeout(() => { + let events = currentCalendar.getEvents() + expect(events[0].start).toEqualDate('2019-04-15T09:30:00') + expect(events[0].end).toEqualDate('2019-04-15T10:30:00') + assertEventCount(1) + currentCalendar.getEvents().forEach((event) => expect(event.allDay).not.toBeTruthy()) + done() + }, 100) + }) + }) + + it('adds a repeating weekly meeting', (done) => { + loadICalendarWith(recurringWeekly, () => { + setTimeout(() => { + let events = currentCalendar.getEvents() + expect(events[0].start).toEqualDate('2019-04-01T17:30:00') + expect(events[0].end).toEqualDate('2019-04-01T18:30:00') + assertEventCount(6) + done() + }, 100) + }) + }) + + it('adds a repeating weekly meeting, with null end', (done) => { + loadICalendarWith(recurringWeeklyWithoutEnd, () => { + setTimeout(() => { + let events = currentCalendar.getEvents() + expect(events[0].start).toEqualDate('2019-04-01T17:30:00') + expect(events[0].end).toBe(null) + assertEventCount(6) + done() + }, 100) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6190 + // this feed starts at beginning of previous month (March 2019) and has 9 total occurences, + // 5 of which will be visible in the current month (April 2019) + it('adds a repeating weekly meeting, limited by COUNT, but across months', (done) => { + loadICalendarWith(recurringWeeklyWithCount, () => { + setTimeout(() => { + assertEventCount(5) + done() + }, 100) + }) + }) + + it('ignores a munged event', (done) => { + loadICalendarWith(mungedOneHourMeeting, () => { + setTimeout(() => { + assertEventCount(0) + done() + }, 100) + }) + }) + + it('adds a valid event and ignores a munged event', (done) => { + loadICalendarWith(multipleEventsOneMunged, () => { + setTimeout(() => { + assertEventCount(1) + done() + }, 100) + }) + }) + + it('defaultAllDayEventDuration overrides ical default all day length of one day', (done) => { + loadICalendarWith( + alldayEvent, + () => { + setTimeout(() => { + assertEventCount(1) + const event = currentCalendar.getEvents()[0] + expect(event.end.getDate()).toEqual(event.start.getDate() + 2) + done() + }, 100) + }, + (source) => { + initCalendar({ + forceEventDuration: true, + defaultAllDayEventDuration: { days: 2 }, + }).addEventSource(source) + }, + ) + }) + + it('calling refetchEvents request ical feed again', (done) => { + let requestCnt = 0 + + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, () => { + requestCnt++ + return { + headers: { 'content-type': ICAL_MIME_TYPE }, + body: oneHourMeeting, + } + }) + + const calendar = initCalendar({ + events: { + url: givenUrl, + format: 'ics', + }, + }) + + setTimeout(() => { + expect(requestCnt).toBe(1) + expect(calendar.getEvents().length).toBe(1) + calendar.refetchEvents() + + setTimeout(() => { + expect(requestCnt).toBe(2) + expect(calendar.getEvents().length).toBe(1) + done() + }, 100) + }, 100) + }) + + function loadICalendarWith(rawICal: string, assertions: () => void, calendarSetup?: (source: EventSourceInput) => void) { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { + headers: { 'content-type': ICAL_MIME_TYPE }, + body: rawICal, + }) + + const source = { url: givenUrl, format: 'ics' } as EventSourceInput + + if (calendarSetup) { + calendarSetup(source) + } else { + initCalendar().addEventSource(source) + } + + const [requestUrl] = fetchMock.lastCall() + const requestParamStr = new URL(requestUrl).searchParams.toString() + expect(requestParamStr).toBe('') + + assertions() + } + + // Checks to make sure all events have been rendered and that the calendar + // has internal info on all the events. + // TODO: don't use currentCalendar + function assertEventCount(expectedCount: number) { + expect(currentCalendar.getEvents().length).toEqual(expectedCount) + + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(expectedCount) + } +}) diff --git a/fullcalendar-main/tests/src/index.global.js.tpl b/fullcalendar-main/tests/src/index.global.js.tpl new file mode 100644 index 0000000..f008d60 --- /dev/null +++ b/fullcalendar-main/tests/src/index.global.js.tpl @@ -0,0 +1,5 @@ +import './index.js' + +{{#each extensionlessTestPaths}} + import '{{this}}.js' +{{/each}} diff --git a/fullcalendar-main/tests/src/index.ts b/fullcalendar-main/tests/src/index.ts new file mode 100644 index 0000000..0b3d9d6 --- /dev/null +++ b/fullcalendar-main/tests/src/index.ts @@ -0,0 +1,6 @@ + +// NOTE: there are many jquery-related libs that our karma config implicitly includes +// They were being difficult with CJS/ESM + +import './lib/global.css' +import './lib/global.js' diff --git a/fullcalendar-main/tests/src/legacy/DayGrid-events.ts b/fullcalendar-main/tests/src/legacy/DayGrid-events.ts new file mode 100644 index 0000000..53fbc4a --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/DayGrid-events.ts @@ -0,0 +1,251 @@ +import { directionallyTestSeg } from '../lib/DayGridEventRenderUtils.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('DayGrid event rendering', () => { + pushOptions({ + initialDate: '2014-08-01', // 2014-07-27 - 2014-10-07 (excl) + initialView: 'dayGridMonth', + }) + + describe('when LTR', () => { + initMonthTesting('ltr') + }) + describe('when RTL', () => { + initMonthTesting('rtl') + }) + + function initMonthTesting(direction) { + it('correctly renders an event starting before view\'s start', () => { + let options = { + events: [ + { start: '2014-07-26', end: '2014-07-30' }, + ], + } + let testSegOptions = { + firstCol: 0, + lastCol: 2, + isStart: false, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event starting at view\'s start', () => { + let options = { + events: [ + { start: '2014-07-27', end: '2014-07-29' }, + ], + } + let testSegOptions = { + firstCol: 0, + lastCol: 1, + isStart: true, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event starting after view\'s start', () => { + let options = { + events: [ + { start: '2014-08-01', end: '2014-08-02' }, + ], + } + let testSegOptions = { + firstCol: 5, + lastCol: 5, + isStart: true, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event starting on a hidden day at week start', () => { + let options = { + weekends: false, + events: [ + { start: '2014-07-27', end: '2014-07-30' }, + ], + } + let testSegOptions = { + firstCol: 0, + lastCol: 1, + isStart: false, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event starting on a hidden day in middle of week', () => { + let options = { + hiddenDays: [2], // hide Tues + events: [ + { start: '2014-07-29', end: '2014-08-01' }, + ], + } + let testSegOptions = { + firstCol: 2, + lastCol: 3, + isStart: false, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending before view\'s end', () => { + let options = { + events: [ + { start: '2014-09-02', end: '2014-09-05' }, + ], + } + let testSegOptions = { + row: 5, + firstCol: 2, + lastCol: 4, + isStart: true, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending at view\'s end', () => { + let options = { + events: [ + { start: '2014-09-04', end: '2014-09-07' }, + ], + } + let testSegOptions = { + row: 5, + firstCol: 4, + lastCol: 6, + isStart: true, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending after view\'s end', () => { + let options = { + events: [ + { start: '2014-09-04', end: '2014-09-08' }, + ], + } + let testSegOptions = { + row: 5, + firstCol: 4, + lastCol: 6, + isStart: true, + isEnd: false, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending at a week\'s end', () => { + let options = { + events: [ + { start: '2014-08-28', end: '2014-08-31' }, + ], + } + let testSegOptions = { + row: 4, + firstCol: 4, + lastCol: 6, + isStart: true, + isEnd: true, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending on a hidden day at week end', () => { + let options = { + weekends: false, + events: [ + { start: '2014-07-30', end: '2014-08-03' }, + ], + } + let testSegOptions = { + firstCol: 2, + lastCol: 4, + isStart: true, + isEnd: false, + } + testSeg(options, testSegOptions) + }) + + it('correctly renders an event ending on a hidden day in middle of week', () => { + let options = { + hiddenDays: [4], // Thurs + events: [ + { start: '2014-07-28', end: '2014-08-01' }, + ], + } + let testSegOptions = { + firstCol: 1, + lastCol: 3, + isStart: true, + isEnd: false, + } + testSeg(options, testSegOptions) + }) + + function testSeg(calendarOptions, testSegOptions) { + calendarOptions.direction = direction + initCalendar(calendarOptions) + directionallyTestSeg(testSegOptions) + } + } + + it('rendering of events across weeks stays consistent', () => { + let calendar = initCalendar({ + events: [ + { + title: 'event1', + start: '2014-08-01', + end: '2014-08-04', + className: 'event1', + }, + { + title: 'event2', + start: '2014-08-02', + end: '2014-08-05', + className: 'event2', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + let row0 = dayGridWrapper.getRowEl(0) + let row0event1 = row0.querySelector('.event1') + let row0event2 = row0.querySelector('.event2') + let row1 = dayGridWrapper.getRowEl(1) + let row1event1 = row1.querySelector('.event1') + let row1event2 = row1.querySelector('.event2') + + expect($(row0event1).offset().top).toBeLessThan($(row0event2).offset().top) + expect($(row1event1).offset().top).toBeLessThan($(row1event2).offset().top) + }) + + it('renders an event with no url with no <a> href', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-01', + }], + }) + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + expect(eventEl).not.toHaveAttr('href') + }) + + it('renders an event with a url with an <a> href', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-01', + url: 'http://google.com/', + }], + }) + let eventEl = new CalendarWrapper(calendar).getFirstEventEl() + expect(eventEl).toHaveAttr('href') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/ListView.ts b/fullcalendar-main/tests/src/legacy/ListView.ts new file mode 100644 index 0000000..de5a20f --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/ListView.ts @@ -0,0 +1,489 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { ListViewWrapper } from '../lib/wrappers/ListViewWrapper.js' + +describe('ListView rendering', () => { + pushOptions({ + initialView: 'listWeek', + now: '2016-08-20', + }) + + describe('with all-day events', () => { + describe('when single-day', () => { + pushOptions({ + events: [ + { + title: 'event 1', + start: '2016-08-15', + }, + { + title: 'event 2', + start: '2016-08-17', + }, + ], + }) + + it('renders only days with events', () => { + let calendar = initCalendar() + + let viewWrapper = new ListViewWrapper(calendar) + let days = viewWrapper.getDayInfo() + let events = viewWrapper.getEventInfo() + + expect(days.length).toBe(2) + expect(days[0].date).toEqualDate('2016-08-15') + expect(days[1].date).toEqualDate('2016-08-17') + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('all-day') + expect(events[1].title).toBe('event 2') + expect(events[1].timeText).toBe('all-day') + }) + + it('filters events through event hook', () => { + let eventMountCnt = 0 + + initCalendar({ + eventDidMount() { + eventMountCnt += 1 + }, + }) + + expect(eventMountCnt).toBe(2) + }) + + it('filters events through eventWillUnmount', () => { + let callCnt = 0 + + initCalendar({ + eventWillUnmount() { + callCnt += 1 + }, + }) + + currentCalendar.destroy() + expect(callCnt).toBe(2) + }) + }) + + describe('when multi-day', () => { + pushOptions({ + events: [ + { + title: 'event 1', + start: '2016-08-15', + end: '2016-08-18', // 3 days + }, + ], + }) + + it('renders all-day for every day', () => { + let calendar = initCalendar() + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(3) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('all-day') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('event 1') + expect(events[2].timeText).toBe('all-day') + }) + }) + }) + + describe('with timed events', () => { + describe('when single-day', () => { + pushOptions({ + events: [ + { + title: 'event 1', + start: '2016-08-15T07:00', + }, + { + title: 'event 2', + start: '2016-08-17T09:00', + end: '2016-08-17T11:00', + }, + ], + }) + + it('renders times', () => { + let calendar = initCalendar() + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am') + expect(events[1].title).toBe('event 2') + expect(events[1].timeText).toBe('9:00am - 11:00am') + }) + + it('doesn\'t render times when displayEventTime is false', () => { + let calendar = initCalendar({ + displayEventTime: false, + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('') + expect(events[1].title).toBe('event 2') + expect(events[1].timeText).toBe('') + }) + + it('doesn\'t render end times when displayEventEnd is false', () => { + let calendar = initCalendar({ + displayEventEnd: false, + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am') + expect(events[1].title).toBe('event 2') + expect(events[1].timeText).toBe('9:00am') + }) + + // regression test for when localized event dates get unlocalized and leak into view rendering + it('renders dates and times in locale', () => { + let calendar = initCalendar({ + locale: frLocale, + }) + let viewWrapper = new ListViewWrapper(calendar) + let days = viewWrapper.getDayInfo() + let events = viewWrapper.getEventInfo() + + expect(days.length).toBe(2) + expect(days[0].date).toEqualDate('2016-08-15') + expect(days[0].mainText).toEqual('lundi') + expect(days[0].altText).toEqual('15 août 2016') + expect(days[1].date).toEqualDate('2016-08-17') + expect(days[1].mainText).toEqual('mercredi') + expect(days[1].altText).toEqual('17 août 2016') + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toMatch(/^0?7:00$/) + expect(events[1].title).toBe('event 2') + expect(events[1].timeText).toMatch(/^0?9:00 - 11:00$/) + }) + }) + + describe('when multi-day', () => { + pushOptions({ + nextDayThreshold: '00:00', + }) + + it('renders partial and full days', () => { + let calendar = initCalendar({ + events: [ + { + title: 'event 1', + start: '2016-08-15T07:00', + end: '2016-08-17T11:00', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(3) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am - 12:00am') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('event 1') + expect(events[2].timeText).toBe('12:00am - 11:00am') + }) + + it('truncates an out-of-range start', () => { + let calendar = initCalendar({ + events: [ + { + title: 'event 1', + start: '2016-08-13T07:00', + end: '2016-08-16T11:00', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(3) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('all-day') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('event 1') + expect(events[2].timeText).toBe('12:00am - 11:00am') + }) + + it('truncates an out-of-range start', () => { + let calendar = initCalendar({ + events: [ + { + title: 'event 1', + start: '2016-08-18T07:00', + end: '2016-08-21T11:00', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(3) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am - 12:00am') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('event 1') + expect(events[2].timeText).toBe('all-day') + }) + }) + + it('renders same days when equal to nextDayThreshold', () => { + let calendar = initCalendar({ + nextDayThreshold: '09:00', + events: [ + { + title: 'event 1', + start: '2016-08-15T07:00', + end: '2016-08-17T09:00', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(3) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am - 12:00am') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('event 1') + expect(events[2].timeText).toBe('12:00am - 9:00am') + }) + + it('renders fewer days when before nextDayThreshold', () => { + let calendar = initCalendar({ + nextDayThreshold: '09:00', + events: [ + { + title: 'event 1', + start: '2016-08-15T07:00', + end: '2016-08-17T08:00', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(2) + expect(events[0].title).toBe('event 1') + expect(events[0].timeText).toBe('7:00am - 12:00am') + expect(events[1].title).toBe('event 1') + expect(events[1].timeText).toBe('12:00am - 8:00am') + }) + }) + + describe('when an event has no title', () => { + it('renders no text for its title', () => { + let calendar = initCalendar({ + events: [ + { + start: '2016-08-15', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let events = viewWrapper.getEventInfo() + + expect(events.length).toBe(1) + expect(events[0].title).toBe('') + expect(events[0].timeText).toBe('all-day') + }) + }) + + describe('when no events', () => { + it('renders an empty message', () => { + let calendar = initCalendar() + let viewWrapper = new ListViewWrapper(calendar) + expect(viewWrapper.hasEmptyMessage()).toBe(true) + }) + }) + + describe('with lots of events', () => { + pushOptions({ + now: '2016-08-29', + events: [ + { + title: 'All Day Event', + start: '2016-08-29', + }, + { + title: 'Long Event', + start: '2016-08-28', + end: '2016-09-04', + }, + { + title: 'Meeting', + start: '2016-08-29T10:30:00', + }, + { + title: 'Lunch', + start: '2016-08-30T12:00:00', + }, + { + title: 'Meeting', + start: '2016-08-30T14:30:00', + }, + { + title: 'Happy Hour', + start: '2014-11-12T17:30:00', + }, + { + title: 'Dinner', + start: '2014-11-12T20:00:00', + }, + { + title: 'Birthday Party', + start: '2016-08-29T07:00:00', + }, + { + title: 'Click for Google', + url: 'http://google.com/', + start: '2016-08-31', + }, + ], + }) + + it('sorts events correctly', () => { + let calendar = initCalendar() + let viewWrapper = new ListViewWrapper(calendar) + let days = viewWrapper.getDayInfo() + let events = viewWrapper.getEventInfo() + + expect(days.length).toBe(7) + expect(days[0].date).toEqualDate('2016-08-28') + expect(days[1].date).toEqualDate('2016-08-29') + expect(days[2].date).toEqualDate('2016-08-30') + expect(days[3].date).toEqualDate('2016-08-31') + expect(days[4].date).toEqualDate('2016-09-01') + expect(days[5].date).toEqualDate('2016-09-02') + expect(days[6].date).toEqualDate('2016-09-03') + + expect(events.length).toBe(13) + expect(events[0].title).toBe('Long Event') + expect(events[0].timeText).toBe('all-day') + expect(events[1].title).toBe('Long Event') + expect(events[1].timeText).toBe('all-day') + expect(events[2].title).toBe('All Day Event') + expect(events[2].timeText).toBe('all-day') + expect(events[3].title).toBe('Birthday Party') + expect(events[3].timeText).toBe('7:00am') + expect(events[4].title).toBe('Meeting') + expect(events[4].timeText).toBe('10:30am') + expect(events[5].title).toBe('Long Event') + expect(events[5].timeText).toBe('all-day') + expect(events[6].title).toBe('Lunch') + expect(events[6].timeText).toBe('12:00pm') + expect(events[7].title).toBe('Meeting') + expect(events[7].timeText).toBe('2:30pm') + expect(events[8].title).toBe('Long Event') + expect(events[8].timeText).toBe('all-day') + expect(events[9].title).toBe('Click for Google') + expect(events[9].timeText).toBe('all-day') + expect(events[10].title).toBe('Long Event') + expect(events[10].timeText).toBe('all-day') + expect(events[11].title).toBe('Long Event') + expect(events[11].timeText).toBe('all-day') + expect(events[12].title).toBe('Long Event') + expect(events[12].timeText).toBe('all-day') + }) + + it('can sort events with non-date property first', () => { + let calendar = initCalendar({ + now: '2016-08-29', + eventOrder: 'title', + events: [ + { + title: 'Sup', + start: '2016-08-29T00:00:00', + }, + { + title: 'Dude', + start: '2016-08-29T10:30:00', + }, + { + title: 'Hello', + start: '2016-08-30', + }, + ], + }) + let viewWrapper = new ListViewWrapper(calendar) + let days = viewWrapper.getDayInfo() + let events = viewWrapper.getEventInfo() + + expect(days.length).toBe(2) + expect(days[0].date).toEqualDate('2016-08-29') + expect(days[1].date).toEqualDate('2016-08-30') + + expect(events.length).toBe(3) + expect(events[0].title).toBe('Dude') + expect(events[1].title).toBe('Sup') + expect(events[2].title).toBe('Hello') + }) + + it('makes scrollbars', () => { + let $el = $('<div style="width:300px" />').appendTo('body') + let calendar = initCalendar({ headerToolbar: false }, $el) + let viewWrapper = new ListViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + expect( + scrollEl.scrollHeight, + ).toBeGreaterThan( + scrollEl.clientHeight + 100, + ) + + $el.remove() + }) + + it('doesn\'t have scrollbars when height is \'auto\'', () => { + let $el = $('<div style="width:300px" />').appendTo('body') + let calendar = initCalendar({ + headerToolbar: false, + height: 'auto', + }, $el) + let viewWrapper = new ListViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + expect( + Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight), + ).toBeLessThan(2) + $el.remove() + }) + }) + + it('updates rendered events despite fetch range being lazy', () => { + let calendar = initCalendar({ + now: '2016-09-12', + initialView: 'dayGridMonth', + events: [ + { title: 'event1', start: '2016-09-12' }, + ], + }) + + calendar.changeView('listWeek') + + let viewWrapper = new ListViewWrapper(calendar) + expect(viewWrapper.getEventEls().length).toBe(1) + calendar.prev() + expect(viewWrapper.getEventEls().length).toBe(0) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/TimeGrid-events.ts b/fullcalendar-main/tests/src/legacy/TimeGrid-events.ts new file mode 100644 index 0000000..04f44c3 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/TimeGrid-events.ts @@ -0,0 +1,88 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('TimeGrid event rendering', () => { + pushOptions({ + initialDate: '2014-08-23', + initialView: 'timeGridWeek', + scrollTime: '00:00:00', + }) + + it('renders the start and end time of an event that spans only 1 day', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-18T02:00:00', + end: '2014-08-18T22:00:00', + }], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + let eventText = calendarWrapper.getEventElInfo(eventEl).timeText + + expect(eventText).toBe('2:00 - 10:00') + }) + + it('renders time to/from midnight for an event that spans two days', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-18T02:00:00', + end: '2014-08-19T22:00:00', + }], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEls = calendarWrapper.getEventEls() + let eventText0 = calendarWrapper.getEventElInfo(eventEls[0]).timeText + let eventText1 = calendarWrapper.getEventElInfo(eventEls[1]).timeText + + expect(eventText0).toBe('2:00 - 12:00') + expect(eventText1).toBe('12:00 - 10:00') + }) + + it('renders no time on an event segment that spans through an entire day', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-18T02:00:00', + end: '2014-08-20T22:00:00', + }], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEls = calendarWrapper.getEventEls() + let eventText1 = calendarWrapper.getEventElInfo(eventEls[1]).timeText + + expect(eventText1).toBe('') + }) + + it('renders an event with no url with no <a> href', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-18T02:00:00', + }], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + + expect(eventEl).not.toHaveAttr('href') + }) + + it('renders an event with a url with an <a> href', () => { + let calendar = initCalendar({ + events: [{ + title: 'event1', + start: '2014-08-18T02:00:00', + url: 'http://google.com/', + }], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + + expect(eventEl).toHaveAttr('href') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/TimeGrid-render.ts b/fullcalendar-main/tests/src/legacy/TimeGrid-render.ts new file mode 100644 index 0000000..0e5b9d3 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/TimeGrid-render.ts @@ -0,0 +1,53 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('Agenda view rendering', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('when LTR', () => { + pushOptions({ + direction: 'ltr', + }) + + it('renders the axis on the left', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + let headerWrapper = viewWrapper.header + let dayGridWrapper = viewWrapper.dayGrid + let timeGridWrapper = viewWrapper.timeGrid + + expect(viewWrapper.getHeaderAxisEl()) + .toBeLeftOf(headerWrapper.getCellEls()[0]) + + expect(viewWrapper.getAllDayAxisEl()) + .toBeLeftOf(dayGridWrapper.getAllDayEls()[0]) + + expect(timeGridWrapper.getSlotAxisEls()[0]) + .toBeLeftOf(timeGridWrapper.getSlotLaneEls()[0]) + }) + }) + + describe('when RTL', () => { + pushOptions({ + direction: 'rtl', + }) + + it('renders the axis on the right', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + let headerWrapper = viewWrapper.header + let dayGridWrapper = viewWrapper.dayGrid + let timeGridWrapper = viewWrapper.timeGrid + + expect(viewWrapper.getHeaderAxisEl()) + .toBeRightOf(headerWrapper.getCellEls()[0]) + + expect(viewWrapper.getAllDayAxisEl()) + .toBeRightOf(dayGridWrapper.getAllDayEls()[0]) + + expect(timeGridWrapper.getSlotAxisEls()[0]) + .toBeRightOf(timeGridWrapper.getSlotLaneEls()[0]) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/View.ts b/fullcalendar-main/tests/src/legacy/View.ts new file mode 100644 index 0000000..e1836fb --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/View.ts @@ -0,0 +1,25 @@ +describe('View object', () => { + /* + TODO: move tests from eventLimitClick.js about view.name/type into here + */ + + pushOptions({ + initialDate: '2015-01-01', + }) + + describe('title', () => { + it('is a correctly defined string', () => { + initCalendar() + let view = currentCalendar.view + expect(view.title).toBe('January 2015') + }) + + it('is available in the viewDidMount callback', () => { + let viewDidMountSpy = spyOnCalendarCallback('viewDidMount', (arg) => { + expect(arg.view.title).toBe('January 2015') + }) + initCalendar() + expect(viewDidMountSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/addEventSource.ts b/fullcalendar-main/tests/src/legacy/addEventSource.ts new file mode 100644 index 0000000..53e151c --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/addEventSource.ts @@ -0,0 +1,101 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('addEventSource', () => { + let eventArray = [ + { id: '0', title: 'event zero', start: '2014-06-24', classNames: 'event-zero' }, + { id: '1', title: 'event one', start: '2014-06-24', classNames: 'event-non-zero event-one' }, + { id: '2', title: 'event two', start: '2014-06-24', classNames: 'event-non-zero event-two' }, + ] + + pushOptions({ + initialDate: '2014-06-24', + initialView: 'dayGridMonth', + }) + + it('correctly adds an array source', (done) => { + go( + () => { + currentCalendar.addEventSource(eventArray) + }, + null, + done, + ) + }) + + it('correctly adds a function source', (done) => { + go( + () => { + currentCalendar.addEventSource((arg, callback) => { + callback(eventArray) + }) + }, + null, + done, + ) + }) + + it('correctly adds an extended array source', (done) => { + go( + () => { + currentCalendar.addEventSource({ + classNames: 'arraysource', + events: eventArray, + }) + }, + () => { + expect($('.arraysource').length).toEqual(3) + }, + done, + ) + }) + + it('correctly adds an extended func source', (done) => { + go( + () => { + currentCalendar.addEventSource({ + classNames: 'funcsource', + events(arg, callback) { + callback(eventArray) + }, + }) + }, + () => { + expect($('.funcsource').length).toEqual(3) + }, + done, + ) + }) + + function go(addFunc, extraTestFunc, doneFunc) { + initCalendar() + addFunc() + + checkAllEvents() + if (extraTestFunc) { + extraTestFunc() + } + + // move the calendar back out of view, then back in (for issue 2191) + currentCalendar.next() + currentCalendar.prev() + + // otherwise, prev/next would be cancelled out by doneFunc's calendar destroy + setTimeout(() => { + checkAllEvents() + if (extraTestFunc) { + extraTestFunc() + } + + doneFunc() + }, 0) + } + + // Checks to make sure all events have been rendered and that the calendar + // has internal info on all the events. + function checkAllEvents() { + expect(currentCalendar.getEvents().length).toEqual(3) + + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(3) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/allDayDefault.ts b/fullcalendar-main/tests/src/legacy/allDayDefault.ts new file mode 100644 index 0000000..58e9195 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/allDayDefault.ts @@ -0,0 +1,194 @@ +describe('defaultAllDay', () => { // TODO: rename file + describe('when undefined', () => { + it('guesses false if T in ISO8601 start date', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-01T06:00:00', + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('guesses false if T in ISO8601 end date', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-01', + end: '2014-05-01T08:00:00', + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('guesses true if ISO8601 start date with no time and unspecified end date', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-01', + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(true) + }) + + it('guesses true if ISO8601 start and end date with no times', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-01', + end: '2014-05-03', + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(true) + }) + + it('guesses false if start is a unix timestamp (which implies it has a time)', () => { + initCalendar({ + events: [ + { + id: '1', + start: 1398902400000, + end: '2014-05-03', + }, + ], + }) + + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('guesses false if end is a unix timestamp (which implies it has a time)', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-01', + end: 1399075200000, + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + }) + + describe('when specified', () => { + it('has an effect when an event\'s allDay is not specified', () => { + initCalendar({ + defaultAllDay: false, + events: [ + { + id: '1', + start: '2014-05-01', + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('has no effect when an event\'s allDay is specified', () => { + initCalendar({ + defaultAllDay: false, + events: [ + { + id: '1', + start: '2014-05-01T00:00:00', + allDay: true, + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(true) + }) + }) +}) + +describe('source.defaultAllDay', () => { + it('has an effect when an event\'s allDay is not specified', () => { + initCalendar({ + eventSources: [ + { + defaultAllDay: false, + events: [ + { + id: '1', + start: '2014-05-01', + }, + ], + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('a true value can override the global defaultAllDay', () => { + initCalendar({ + defaultAllDay: false, + eventSources: [ + { + defaultAllDay: true, + events: [ + { + id: '1', + start: '2014-05-01T06:00:00', + }, + ], + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(true) + }) + + it('a false value can override the global defaultAllDay', () => { + initCalendar({ + defaultAllDay: true, + eventSources: [ + { + defaultAllDay: false, + events: [ + { + id: '1', + start: '2014-05-01', + }, + ], + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) + + it('has no effect when an event\'s allDay is specified', () => { + initCalendar({ + eventSources: [ + { + defaultAllDay: true, + events: [ + { + id: '1', + start: '2014-05-01', + allDay: false, + }, + ], + }, + ], + }) + let eventObj = currentCalendar.getEventById('1') + expect(eventObj.allDay).toEqual(false) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/allDaySlot.ts b/fullcalendar-main/tests/src/legacy/allDaySlot.ts new file mode 100644 index 0000000..14a255a --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/allDaySlot.ts @@ -0,0 +1,70 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('allDaySlots', () => { + describe('when allDaySlots is not set', () => { + describe('in week', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeTruthy() + }) + }) + describe('in day', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeTruthy() + }) + }) + }) + + describe('when allDaySlots is set true', () => { + describe('in week', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + allDaySlot: true, + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeTruthy() + }) + }) + describe('in day', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + allDaySlot: true, + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeTruthy() + }) + }) + }) + + describe('when allDaySlots is set false', () => { + describe('in week', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + allDaySlot: false, + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeFalsy() + }) + }) + describe('in day', () => { + it('should default to having an allDaySlots table', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + allDaySlot: false, + }) + let dayGrid = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGrid).toBeFalsy() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/allDayText.ts b/fullcalendar-main/tests/src/legacy/allDayText.ts new file mode 100644 index 0000000..916b3c8 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/allDayText.ts @@ -0,0 +1,95 @@ +import ptBrLocale from '@fullcalendar/core/locales/pt-br' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('allDayText', () => { + describe('when allDaySlots is not set', () => { + describe('in week', () => { + it('should default allDayText to using \'all-day\'', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + }) + expectAllDayTextToBe(calendar, 'all-day') + }) + }) + describe('in day', () => { + it('should default allDayText to using \'all-day\'', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + }) + expectAllDayTextToBe(calendar, 'all-day') + }) + }) + }) + + describe('when allDaySlots is set true', () => { + describe('in week', () => { + it('should default allDayText to using \'all-day\'', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + allDaySlot: true, + }) + expectAllDayTextToBe(calendar, 'all-day') + }) + }) + describe('in day', () => { + it('should default allDayText to using \'all-day\'', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + allDaySlot: true, + }) + expectAllDayTextToBe(calendar, 'all-day') + }) + }) + }) + + describe('when allDaySlots is set true and locale is not default', () => { + describe('in week', () => { + it('should use the locale\'s all-day value', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + allDaySlot: true, + locale: ptBrLocale, + }) + expectAllDayTextToBe(calendar, 'dia inteiro') + }) + }) + describe('in day', () => { + it('should use the locale\'s all-day value', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + allDaySlot: true, + locale: ptBrLocale, + }) + expectAllDayTextToBe(calendar, 'dia inteiro') + }) + }) + }) + + describe('when allDaySlots is set true and allDayText is specified', () => { + describe('in week', () => { + it('should show specified all day text', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + allDaySlot: true, + allDayText: 'axis-phosy', + }) + expectAllDayTextToBe(calendar, 'axis-phosy') + }) + }) + describe('in day', () => { + it('should show specified all day text', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + allDayText: 'axis-phosy', + }) + expectAllDayTextToBe(calendar, 'axis-phosy') + }) + }) + }) + + function expectAllDayTextToBe(calendar, text) { + let viewWrapper = new TimeGridViewWrapper(calendar) + let allDayText = viewWrapper.getAllDayAxisElText() + expect(allDayText).toBe(text) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/aspectRatio.ts b/fullcalendar-main/tests/src/legacy/aspectRatio.ts new file mode 100644 index 0000000..48b58a6 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/aspectRatio.ts @@ -0,0 +1,169 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('aspectRatio', () => { + function getCalendarElement(width) { + return $('<div id="calendar" style="max-width:none">').appendTo('body').width(width)[0] + } + + describe('when default settings are used', () => { + const elementWidth = 675 + + it('view div should use the ratio 1:35 to set height', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.height)).toEqual(500) + }) + + it('view div should have width of div', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + }) + + describe('when initializing the aspectRatio', () => { + const elementWidth = 1000 + + describe('to 2', () => { + pushOptions({ + aspectRatio: 2, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should set the height to width sizes very close to ratio of 2', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + let ratio = Math.round((rect.width / rect.height) * 100) + expect(Math.round(ratio)).toEqual(200) + }) + }) + + describe('to 1', () => { + pushOptions({ + aspectRatio: 1, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should set the height to width sizes very close to ratio of 2', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + let ratio = Math.round((rect.width / rect.height) * 100) + expect(Math.round(ratio)).toEqual(100) + }) + }) + + describe('to less than 0.5', () => { + pushOptions({ + aspectRatio: 0.4, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should set the height to width ratio to 0.5', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + let ratio = Math.round((rect.width / rect.height) * 100) + expect(Math.round(ratio)).toEqual(50) + }) + }) + + describe('to negative', () => { + pushOptions({ + aspectRatio: -2, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should set the height to width ratio to 0.5', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + let ratio = Math.round((rect.width / rect.height) * 100) + expect(Math.round(ratio)).toEqual(50) + }) + }) + + describe('to zero', () => { + pushOptions({ + aspectRatio: 0, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should set the height to width ratio to 0.5', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + let ratio = Math.round((rect.width / rect.height) * 100) + expect(Math.round(ratio)).toEqual(50) + }) + }) + + describe('to very large', () => { + pushOptions({ + aspectRatio: 4000, + }) + + it('should not change the width', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let rect = viewContainerEl.getBoundingClientRect() + expect(Math.round(rect.width)).toEqual(elementWidth) + }) + + it('should cause rows to be natural height', () => { + let calendar = initCalendar({}, getCalendarElement(elementWidth)) + let viewContainerEl = new CalendarWrapper(calendar).getViewContainerEl() + + let actualHeight = viewContainerEl.getBoundingClientRect().height + let naturalHeight = viewContainerEl.getBoundingClientRect().height + expect(Math.round(actualHeight)).toEqual(Math.round(naturalHeight)) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/background-events.ts b/fullcalendar-main/tests/src/legacy/background-events.ts new file mode 100644 index 0000000..05e8112 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/background-events.ts @@ -0,0 +1,777 @@ +import { RED_REGEX } from '../lib/dom-misc.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +// SEE ALSO: event-color.js + +describe('background events', () => { + pushOptions({ + initialDate: '2014-11-04', + scrollTime: '00:00', + }) + + describe('when in month view', () => { + pushOptions({ initialView: 'dayGridMonth' }) + + describe('when LTR', () => { + it('render correctly on a single day', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + display: 'background', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let allBgEls = dayGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(allBgEls[0]).toBeLeftOf(dayGridWrapper.getDayEl('2014-11-05')) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('render correctly spanning multiple weeks', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + end: '2014-11-11', + display: 'background', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let allBgEls = dayGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(2) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(allBgEls[0]).toBeRightOf(dayGridWrapper.getDayEl('2014-11-03')) + expect(allBgEls[1]).toBeLeftOf(dayGridWrapper.getDayEl('2014-11-12')) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('render correctly when two span on top of each other', () => { + let calendar = initCalendar({ + events: [ + { + start: '2014-11-04', + end: '2014-11-07', + display: 'background', + }, + { + start: '2014-11-05', + end: '2014-11-08', + display: 'background', + }, + ], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let allBgEls = dayGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(2) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(2) + expect(allBgEls[0]).toBeRightOf(dayGridWrapper.getDayEl('2014-11-02')) + expect(allBgEls[1]).toBeLeftOf(dayGridWrapper.getDayEl('2014-11-08')) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('renders "business hours" on whole days', () => { + let calendar = initCalendar({ + businessHours: true, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getNonBusinessDayEls().length).toBe(12) // there are 6 weeks. 2 weekend days each + }) + }) + + describe('when RTL', () => { + pushOptions({ direction: 'rtl' }) + + it('render correctly on a single day', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + display: 'background', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let allBgEls = dayGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(allBgEls[0]).toBeRightOf(dayGridWrapper.getDayEl('2014-11-06')) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('render correctly spanning multiple weeks', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + end: '2014-11-11', + display: 'background', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let allBgEls = dayGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(2) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(allBgEls[0]).toBeLeftOf(dayGridWrapper.getDayEl('2014-11-02')) + expect(allBgEls[1]).toBeRightOf(dayGridWrapper.getDayEl('2014-11-12')) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + }) + + describe('when inverse', () => { + describe('when LTR', () => { + it('render correctly on a single day', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + display: 'inverse-background', + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(7) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(2) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(1) + + let secondRowBgEls = dayGridWrapper.getBgEventEls(1) + + expect(secondRowBgEls[0]) + .toBeLeftOf(dayGridWrapper.getDayEl('2014-11-05')) + + expect(secondRowBgEls[1]) + .toBeRightOf(dayGridWrapper.getDayEl('2014-11-03')) + + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('render correctly spanning multiple weeks', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + end: '2014-11-11', + display: 'inverse-background', + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(6) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(1) + + expect(dayGridWrapper.getBgEventEls(1)[0]) + .toBeLeftOf(dayGridWrapper.getDayEl('2014-11-05')) + + expect(dayGridWrapper.getBgEventEls(2)[0]) + .toBeRightOf(dayGridWrapper.getDayEl('2014-11-09')) + + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + + it('render correctly when starts before start of month', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-10-24', + end: '2014-11-06', + display: 'inverse-background', + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(5) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(0) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(1) + + expect(dayGridWrapper.getBgEventEls(1)) + .toBeRightOf(dayGridWrapper.getDayEl('2014-11-04')) + }) + + it('render correctly when ends after end of month', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-27', + end: '2014-12-08', + display: 'inverse-background', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(5) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(0) + + expect(dayGridWrapper.getBgEventEls(4)) + .toBeLeftOf(dayGridWrapper.getDayEl('2014-11-28')) + }) + + it('render correctly with two related events, in reverse order', () => { + let calendar = initCalendar({ + events: [ + { + groupId: 'hi', + start: '2014-11-06', + display: 'inverse-background', + }, + { + groupId: 'hi', + start: '2014-11-04', + display: 'inverse-background', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(8) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(3) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(1) + }) + }) + + describe('when RTL', () => { + pushOptions({ direction: 'rtl' }) + + it('render correctly on a single day', () => { + let calendar = initCalendar({ + events: [{ + title: 'hi', + start: '2014-11-04', + display: 'inverse-background', + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(7) + expect(dayGridWrapper.getBgEventEls(0).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(1).length).toBe(2) + expect(dayGridWrapper.getBgEventEls(2).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(3).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(4).length).toBe(1) + expect(dayGridWrapper.getBgEventEls(5).length).toBe(1) + }) + }) + }) + + describe('when in month view', () => { + it('can be activated when rendering set on the source', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + eventSources: [{ + display: 'background', + events: [{ + start: '2014-11-04', + }], + }], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getBgEventEls().length).toBe(1) + expect(dayGridWrapper.getEventEls().length).toBe(0) + }) + }) + + describe('when in timeGrid view and timed event', () => { + it('can be activated when rendering set on the source', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + eventSources: [{ + display: 'background', + events: [{ + start: '2014-11-04T01:00:00', + }], + }], + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + + expect(viewWrapper.dayGrid.getEventEls().length).toBe(0) + expect(viewWrapper.timeGrid.getBgEventEls().length).toBe(1) + }) + }) + }) + + describe('when in week view', () => { + pushOptions({ initialView: 'timeGridWeek' }) + + describe('when LTR', () => { + it('render correctly on one day', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-04T05:00:00', + display: 'background', + }], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let allBgEvents = timeGridWrapper.getBgEventEls() + + expect(allBgEvents.length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) // column + expect(timeGridWrapper.getEventEls().length).toBe(0) // no fg events + + let rect = allBgEvents[0].getBoundingClientRect() + let topDiff = Math.abs(rect.top - timeGridWrapper.getTimeTop('01:00:00')) // TODO: make more exact + let bottomDiff = Math.abs(rect.bottom - timeGridWrapper.getTimeTop('05:00:00')) + + expect(topDiff).toBeLessThanOrEqual(1) + expect(bottomDiff).toBeLessThanOrEqual(1) + }) + + it('render correctly spanning multiple days', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-05T05:00:00', + display: 'background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + }) + + it('render correctly when two span on top of each other', () => { + let calendar = initCalendar({ + events: [ + { + start: '2014-11-04T01:00:00', + end: '2014-11-05T05:00:00', + display: 'background', + }, + { + start: '2014-11-04T03:00:00', + end: '2014-11-05T08:00:00', + display: 'background', + }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(4) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(2) + // TODO: maybe check y coords + }) + + describe('when businessHours', () => { + it('renders correctly if assumed default', () => { + let calendar = initCalendar({ + businessHours: true, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + expect(viewWrapper.dayGrid.getNonBusinessDayEls().length).toBe(2) // whole days in the day area + expect(viewWrapper.timeGrid.getNonBusinessDayEls().length).toBe(12) // strips of gray on the timed area + }) + + it('renders correctly if custom', () => { + let calendar = initCalendar({ + businessHours: { + startTime: '02:00', + endTime: '06:00', + daysOfWeek: [1, 2, 3, 4], // Mon-Thu + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + + // whole days + expect(viewWrapper.dayGrid.getNonBusinessDayEls().length).toBe(2) // each multi-day stretch is one element + + // time area + let timeGridWrapper = viewWrapper.timeGrid + expect(timeGridWrapper.getNonBusinessDayEls().length).toBe(11) + expect(timeGridWrapper.queryNonBusinessSegsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryNonBusinessSegsInCol(1).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(2).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(3).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(4).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryNonBusinessSegsInCol(6).length).toBe(1) + }) + }) + }) + + describe('when RTL', () => { + pushOptions({ + direction: 'rtl', + }) + + it('render correctly on one day', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-04T05:00:00', + display: 'background', + }], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let allBgEls = timeGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + + let rect = allBgEls[0].getBoundingClientRect() + let topDiff = Math.abs(rect.top - timeGridWrapper.getTimeTop('01:00:00')) + let bottomDiff = Math.abs(rect.bottom - timeGridWrapper.getTimeTop('05:00:00')) + + expect(topDiff).toBeLessThanOrEqual(1) // TODO: tighten up + expect(bottomDiff).toBeLessThanOrEqual(1) + }) + + it('render correctly spanning multiple days', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-05T05:00:00', + display: 'background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + }) + + describe('when businessHours', () => { + it('renders correctly if custom', () => { + let calendar = initCalendar({ + businessHours: { + startTime: '02:00', + endTime: '06:00', + daysOfWeek: [1, 2, 3, 4], // Mon-Thu + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + + // whole days + let dayGridWrapper = viewWrapper.dayGrid + expect(dayGridWrapper.getNonBusinessDayEls().length).toBe(2) // each stretch of days is one element + + // time area + let timeGridWrapper = viewWrapper.timeGrid + expect(timeGridWrapper.getNonBusinessDayEls().length).toBe(11) + expect(timeGridWrapper.queryNonBusinessSegsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryNonBusinessSegsInCol(1).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(2).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(3).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(4).length).toBe(2) + expect(timeGridWrapper.queryNonBusinessSegsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryNonBusinessSegsInCol(6).length).toBe(1) + }) + }) + }) + + describe('when inverse', () => { + describe('when LTR', () => { + it('render correctly on one day', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-04T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(8) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + // TODO: maybe check y coords + }) + + it('render correctly spanning multiple days', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-05T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(7) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + // TODO: maybe check y coords + }) + + it('render correctly when starts before start of week', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-10-30T01:00:00', + end: '2014-11-04T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(5) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(0) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(0) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + // TODO: maybe check y coords + }) + + it('render correctly when ends after end of week', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-12T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(3) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + // TODO: maybe check y coords + }) + + it('render correctly with two related events, in reverse order', () => { + let calendar = initCalendar({ + events: [ + { + groupId: 'hello', + start: '2014-11-05T01:00:00', + end: '2014-11-05T05:00:00', + display: 'inverse-background', + }, + { + groupId: 'hello', + start: '2014-11-03T01:00:00', + end: '2014-11-03T05:00:00', + display: 'inverse-background', + }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(9) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + // TODO: maybe check y coords + }) + + it('render correctly with two related events, nested', () => { + let calendar = initCalendar({ + events: [ + { + groupId: 'hello', + start: '2014-11-05T01:00:00', + end: '2014-11-05T05:00:00', + display: 'inverse-background', + }, + { + groupId: 'hello', + start: '2014-11-05T02:00:00', + end: '2014-11-05T04:00:00', + display: 'inverse-background', + }, + ], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let allBgEls = timeGridWrapper.getBgEventEls() + + expect(allBgEls.length).toBe(8) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + + expect(allBgEls[3].getBoundingClientRect().top) + .toBeLessThan(timeGridWrapper.getTimeTop('01:00:00')) + expect(allBgEls[4].getBoundingClientRect().bottom) + .toBeGreaterThan(timeGridWrapper.getTimeTop('05:00:00')) + }) + }) + + describe('when RTL', () => { + pushOptions({ + direction: 'rtl', + }) + + it('render correctly on one day', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + end: '2014-11-04T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getBgEventEls().length).toBe(8) + expect(timeGridWrapper.queryBgEventsInCol(0).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(1).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(2).length).toBe(2) + expect(timeGridWrapper.queryBgEventsInCol(3).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(4).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(5).length).toBe(1) + expect(timeGridWrapper.queryBgEventsInCol(6).length).toBe(1) + // TODO: maybe check y coords + }) + }) + + describe('when out of view range', () => { + it('should still render', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-01-01T01:00:00', + end: '2014-01-01T05:00:00', + display: 'inverse-background', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.getBgEventEls().length).toBe(7) + }) + }) + }) + + it('can have custom Event Object color', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + color: 'red', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + + it('can have custom Event Object backgroundColor', () => { + let calendar = initCalendar({ + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + backgroundColor: 'red', + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + + it('can have custom Event Source color', () => { + let calendar = initCalendar({ + eventSources: [{ + color: 'red', + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + }], + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + + it('can have custom Event Source backgroundColor', () => { + let calendar = initCalendar({ + eventSources: [{ + backgroundColor: 'red', + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + }], + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + + it('is affected by global eventColor', () => { + let calendar = initCalendar({ + eventColor: 'red', + eventSources: [{ + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + }], + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + + it('is affected by global eventBackgroundColor', () => { + let calendar = initCalendar({ + eventBackgroundColor: 'red', + eventSources: [{ + events: [{ + start: '2014-11-04T01:00:00', + display: 'background', + }], + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let bgEl = timeGridWrapper.getBgEventEls()[0] + expect($(bgEl).css('background-color')).toMatch(RED_REGEX) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/businessHours.ts b/fullcalendar-main/tests/src/legacy/businessHours.ts new file mode 100644 index 0000000..2bb388b --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/businessHours.ts @@ -0,0 +1,182 @@ +// most other businessHours tests are in background-events.js + +import { doElsMatchSegs } from '../lib/segs.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('businessHours', () => { + pushOptions({ + timeZone: 'UTC', + initialDate: '2014-11-25', + initialView: 'dayGridMonth', + businessHours: true, + }) + + it('doesn\'t break when starting out in a larger month time range', () => { + let calendar = initCalendar() // start out in the month range + + currentCalendar.changeView('timeGridWeek') + currentCalendar.next() // move out of the original month range... + currentCalendar.next() // ... out. should render correctly. + + // whole days + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getNonBusinessDayEls().length).toBe(2) // each multi-day stretch is one element + + // timed area + expect(isTimeGridNonBusinessSegsRendered(calendar, [ + // sun + { start: '2014-12-07T00:00', end: '2014-12-08T00:00' }, + // mon + { start: '2014-12-08T00:00', end: '2014-12-08T09:00' }, + { start: '2014-12-08T17:00', end: '2014-12-09T00:00' }, + // tue + { start: '2014-12-09T00:00', end: '2014-12-09T09:00' }, + { start: '2014-12-09T17:00', end: '2014-12-10T00:00' }, + // wed + { start: '2014-12-10T00:00', end: '2014-12-10T09:00' }, + { start: '2014-12-10T17:00', end: '2014-12-11T00:00' }, + // thu + { start: '2014-12-11T00:00', end: '2014-12-11T09:00' }, + { start: '2014-12-11T17:00', end: '2014-12-12T00:00' }, + // fri + { start: '2014-12-12T00:00', end: '2014-12-12T09:00' }, + { start: '2014-12-12T17:00', end: '2014-12-13T00:00' }, + // sat + { start: '2014-12-13T00:00', end: '2014-12-14T00:00' }, + ])).toBe(true) + }) + + describe('when used as a dynamic option', () => { + ['timeGridWeek', 'dayGridMonth'].forEach((viewName) => { + it('allows dynamic turning on', () => { + let calendar = initCalendar({ + initialView: viewName, + businessHours: false, + }) + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getNonBusinessDayEls().length).toBe(0) + currentCalendar.setOption('businessHours', true) + expect(calendarWrapper.getNonBusinessDayEls().length).toBeGreaterThan(0) + }) + + it('allows dynamic turning off', () => { + let calendar = initCalendar({ + initialView: viewName, + businessHours: true, + }) + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getNonBusinessDayEls().length).toBeGreaterThan(0) + currentCalendar.setOption('businessHours', false) + expect(calendarWrapper.getNonBusinessDayEls().length).toBe(0) + }) + }) + }) + + describe('for multiple day-of-week definitions', () => { + it('rendes two day-of-week groups', () => { + let calendar = initCalendar({ + initialDate: '2014-12-07', + initialView: 'timeGridWeek', + businessHours: [ + { + daysOfWeek: [1, 2, 3], // mon, tue, wed + startTime: '08:00', + endTime: '18:00', + }, + { + daysOfWeek: [4, 5], // thu, fri + startTime: '10:00', + endTime: '16:00', + }, + ], + }) + + // timed area + expect(isTimeGridNonBusinessSegsRendered(calendar, [ + // sun + { start: '2014-12-07T00:00', end: '2014-12-08T00:00' }, + // mon + { start: '2014-12-08T00:00', end: '2014-12-08T08:00' }, + { start: '2014-12-08T18:00', end: '2014-12-09T00:00' }, + // tue + { start: '2014-12-09T00:00', end: '2014-12-09T08:00' }, + { start: '2014-12-09T18:00', end: '2014-12-10T00:00' }, + // wed + { start: '2014-12-10T00:00', end: '2014-12-10T08:00' }, + { start: '2014-12-10T18:00', end: '2014-12-11T00:00' }, + // thu + { start: '2014-12-11T00:00', end: '2014-12-11T10:00' }, + { start: '2014-12-11T16:00', end: '2014-12-12T00:00' }, + // fri + { start: '2014-12-12T00:00', end: '2014-12-12T10:00' }, + { start: '2014-12-12T16:00', end: '2014-12-13T00:00' }, + // sat + { start: '2014-12-13T00:00', end: '2014-12-14T00:00' }, + ])).toBe(true) + }) + + it('wont\'t process businessHour items that omit dow', () => { + let calendar = initCalendar({ + initialDate: '2014-12-07', + initialView: 'timeGridWeek', + businessHours: [ + { + // invalid + startTime: '08:00', + endTime: '18:00', + }, + { + daysOfWeek: [4, 5], // thu, fri + startTime: '10:00', + endTime: '16:00', + }, + ], + }) + + // timed area + expect(isTimeGridNonBusinessSegsRendered(calendar, [ + // sun + { start: '2014-12-07T00:00', end: '2014-12-08T00:00' }, + // mon + { start: '2014-12-08T00:00', end: '2014-12-09T00:00' }, + // tue + { start: '2014-12-09T00:00', end: '2014-12-10T00:00' }, + // wed + { start: '2014-12-10T00:00', end: '2014-12-11T00:00' }, + // thu + { start: '2014-12-11T00:00', end: '2014-12-11T10:00' }, + { start: '2014-12-11T16:00', end: '2014-12-12T00:00' }, + // fri + { start: '2014-12-12T00:00', end: '2014-12-12T10:00' }, + { start: '2014-12-12T16:00', end: '2014-12-13T00:00' }, + // sat + { start: '2014-12-13T00:00', end: '2014-12-14T00:00' }, + ])).toBe(true) + }) + }) + + it('will grey-out a totally non-business-hour view', () => { + let calendar = initCalendar({ + initialDate: '2016-07-23', // sat + initialView: 'timeGridDay', + businessHours: true, + }) + + // timed area + expect(isTimeGridNonBusinessSegsRendered(calendar, [ + { start: '2016-07-23T00:00', end: '2016-07-24T00:00' }, + ])).toBe(true) + }) + + function isTimeGridNonBusinessSegsRendered(calendar, segs) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + return doElsMatchSegs( + timeGridWrapper.getNonBusinessDayEls(), + segs, + timeGridWrapper.getRect.bind(timeGridWrapper), + ) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/buttonIcons.ts b/fullcalendar-main/tests/src/legacy/buttonIcons.ts new file mode 100644 index 0000000..facffb6 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/buttonIcons.ts @@ -0,0 +1,70 @@ +import bootstrapPlugin from '@fullcalendar/bootstrap' +import dayGridPlugin from '@fullcalendar/daygrid' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('buttonIcons', () => { + pushOptions({ + plugins: [dayGridPlugin, bootstrapPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'prevYear, nextYear', + }, + }) + + describe('when buttonIcons is not set', () => { + it('should have default values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + let prevBtn = toolbarWrapper.getButtonInfo('prev') + let nextBtn = toolbarWrapper.getButtonInfo('next') + let nextYearBtn = toolbarWrapper.getButtonInfo('nextYear') + let prevYearBtn = toolbarWrapper.getButtonInfo('prevYear') + + expect(prevBtn.iconName).toBe('chevron-left') + expect(nextBtn.iconName).toBe('chevron-right') + expect(nextYearBtn.iconName).toBe('chevrons-right') + expect(prevYearBtn.iconName).toBe('chevrons-left') + }) + }) + + describe('when buttonIcons is set and theme is falsy', () => { + pushOptions({ + buttonIcons: { + prev: 'some-icon-left', + next: 'some-icon-right', + prevYear: 'some-icon-leftYear', + nextYear: 'some-icon-rightYear', + }, + }) + + it('should have the set values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + let prevBtn = toolbarWrapper.getButtonInfo('prev') + let nextYearBtn = toolbarWrapper.getButtonInfo('nextYear') + let prevYearBtn = toolbarWrapper.getButtonInfo('prevYear') + + expect(prevBtn.iconName).toBe('some-icon-left') + expect(prevBtn.iconName).toBe('some-icon-left') + expect(prevYearBtn.iconName).toBe('some-icon-leftYear') + expect(nextYearBtn.iconName).toBe('some-icon-rightYear') + }) + }) + + describe('when theme is set', () => { + pushOptions({ + themeSystem: 'bootstrap', + }) + + it('buttonIcons is ignored', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let prevButtonInfo = toolbarWrapper.getButtonInfo('prev') // NOT called with 'fa' + + expect(prevButtonInfo.iconName).toBeFalsy() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/buttonText.ts b/fullcalendar-main/tests/src/legacy/buttonText.ts new file mode 100644 index 0000000..394161b --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/buttonText.ts @@ -0,0 +1,214 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('button text', () => { + pushOptions({ + headerToolbar: { + left: 'prevYear,prev,today,next,nextYear', + center: '', + right: 'dayGridMonth,dayGridWeek,dayGridDay,timeGridWeek,timeGridDay', + }, + }) + + describe('with default locale', () => { + describe('with default buttonIcons', () => { + it('should contain default text values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + // will have button icons, to text will be empty + expect(toolbarWrapper.getButtonInfo('next').text).toBe('') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('today') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('month') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('week') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('week') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('day') + }) + + it('should contain specified text values', () => { + let calendar = initCalendar({ + buttonText: { + prev: '<-', + next: '->', + prevYear: '<--', + nextYear: '-->', + today: 'tidei', + month: 'mun', + week: 'wiki', + day: 'dei', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getButtonInfo('next').text).toBe('->') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('-->') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('<-') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('<--') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('tidei') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('mun') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('wiki') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('dei') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('wiki') + }) + }) + + describe('with buttonIcons turned off', () => { + pushOptions({ + buttonIcons: false, + }) + + it('should contain default text values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + // will have actual text now + expect(toolbarWrapper.getButtonInfo('next').text).toBe('next') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('next year') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('prev') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('prev year') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('today') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('month') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('week') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('day') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('week') + }) + + it('should contain specified text values', () => { + let calendar = initCalendar({ + buttonText: { + prev: '<-', + next: '->', + prevYear: '<--', + nextYear: '-->', + today: 'tidei', + month: 'mun', + week: 'wiki', + day: 'dei', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getButtonInfo('next').text).toBe('->') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('-->') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('<-') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('<--') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('tidei') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('mun') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('wiki') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('dei') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('wiki') + }) + }) + }) + + describe('when locale is not default', () => { + pushOptions({ + locale: frLocale, + }) + + describe('with default buttonIcons', () => { + it('should contain default text values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + // will contain icons, so will contain no text + expect(toolbarWrapper.getButtonInfo('next').text).toBe('') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('Aujourd\'hui') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('Mois') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('Semaine') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('Jour') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('Semaine') + }) + + it('should contain specified text values', () => { + let calendar = initCalendar({ + buttonText: { + prev: '<-', + next: '->', + prevYear: '<--', + nextYear: '-->', + today: 'tidei', + month: 'mun', + week: 'wiki', + day: 'dei', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getButtonInfo('next').text).toBe('->') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('-->') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('<-') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('<--') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('tidei') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('mun') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('wiki') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('dei') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('wiki') + }) + }) + + describe('with buttonIcons turned off', () => { + pushOptions({ + buttonIcons: false, + }) + + it('should contain default text values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + // will have the locale's actual text now + expect(toolbarWrapper.getButtonInfo('next').text).toBe('Suivant') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('Précédent') + /// / locales files don't have data for prev/next *year* + // expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('Suivant'); + // expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('Précédent'); + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('Aujourd\'hui') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('Mois') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('Semaine') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('Semaine') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('Jour') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('Jour') + }) + + it('should contain specified text values', () => { + let calendar = initCalendar({ + buttonText: { + prev: '<-', + next: '->', + prevYear: '<--', + nextYear: '-->', + today: 'tidei', + month: 'mun', + week: 'wiki', + day: 'dei', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getButtonInfo('next').text).toBe('->') + expect(toolbarWrapper.getButtonInfo('nextYear').text).toBe('-->') + expect(toolbarWrapper.getButtonInfo('prev').text).toBe('<-') + expect(toolbarWrapper.getButtonInfo('prevYear').text).toBe('<--') + + expect(toolbarWrapper.getButtonInfo('today').text).toBe('tidei') + expect(toolbarWrapper.getButtonInfo('dayGridMonth').text).toBe('mun') + expect(toolbarWrapper.getButtonInfo('dayGridWeek').text).toBe('wiki') + expect(toolbarWrapper.getButtonInfo('dayGridDay').text).toBe('dei') + expect(toolbarWrapper.getButtonInfo('timeGridWeek').text).toBe('wiki') + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/columnHeaderFormat.ts b/fullcalendar-main/tests/src/legacy/columnHeaderFormat.ts new file mode 100644 index 0000000..ed64373 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/columnHeaderFormat.ts @@ -0,0 +1,195 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import enGbLocale from '@fullcalendar/core/locales/en-gb' +import koLocale from '@fullcalendar/core/locales/ko' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dayHeaderFormat', () => { // TODO: rename file + describe('when not set', () => { + pushOptions({ + initialDate: '2014-05-11', + }) + + const VIEWS_WITH_FORMAT = [ + { view: 'dayGridMonth', expected: /^Sun$/ }, + { view: 'dayGridWeek', expected: /^Sun 5[/ ]11$/ }, + { view: 'timeGridWeek', expected: /^Sun 5[/ ]11$/ }, + { view: 'dayGridDay', expected: /^Sunday$/ }, + { view: 'timeGridDay', expected: /^Sunday$/ }, + ] + + it('should have default values', () => { + let calendar = initCalendar() + + for (let viewWithFormat of VIEWS_WITH_FORMAT) { + calendar.changeView(viewWithFormat.view) + let header = new (viewWithFormat.view.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper)(calendar).header + expect(header.getCellText(0)).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when dayHeaderFormat is set on a per-view basis', () => { + pushOptions({ + initialDate: '2014-05-11', + views: { + month: { dayHeaderFormat: { weekday: 'long' } }, + day: { dayHeaderFormat: { weekday: 'long', month: 'long', day: 'numeric' } }, + dayGridWeek: { dayHeaderFormat: { weekday: 'long', month: 'numeric', day: 'numeric' } }, + }, + }) + + const VIEWS_WITH_FORMAT = [ + { view: 'dayGridMonth', expected: /^Sunday$/ }, + { view: 'timeGridDay', expected: /^Sunday, May 11$/ }, + { view: 'dayGridWeek', expected: /^Sunday, 5[/ ]11$/ }, + ] + + it('should have the correct values', () => { + let calendar = initCalendar() + + for (let viewWithFormat of VIEWS_WITH_FORMAT) { + calendar.changeView(viewWithFormat.view) + let header = new (viewWithFormat.view.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper)(calendar).header + expect(header.getCellText(0)).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when locale is French', () => { + pushOptions({ + initialDate: '2014-05-11', + locale: frLocale, + }) + + const VIEWS_WITH_FORMAT = [ + { view: 'dayGridMonth', expected: /^dim\.$/ }, + { view: 'dayGridWeek', expected: /^dim\. 11[/ ]0?5$/ }, + { view: 'timeGridWeek', expected: /^dim\. 11[/ ]0?5$/ }, + { view: 'dayGridDay', expected: /^dimanche$/ }, + { view: 'timeGridDay', expected: /^dimanche$/ }, + ] + + it('should have the translated dates', () => { + let calendar = initCalendar() + + for (let viewWithFormat of VIEWS_WITH_FORMAT) { + calendar.changeView(viewWithFormat.view) + let header = new (viewWithFormat.view.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper)(calendar).header + expect(header.getCellText(0)).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when locale is en-gb', () => { + pushOptions({ + initialDate: '2014-05-11', + locale: enGbLocale, + }) + + const VIEWS_WITH_FORMAT = [ + { view: 'dayGridMonth', expected: /^Sun$/ }, + { view: 'dayGridWeek', expected: /^Sun 11[/ ]0?5$/ }, + { view: 'timeGridWeek', expected: /^Sun 11[/ ]0?5$/ }, + { view: 'dayGridDay', expected: /^Sunday$/ }, + { view: 'timeGridDay', expected: /^Sunday$/ }, + ] + + it('should have the translated dates', () => { + let calendar = initCalendar() + + for (let viewWithFormat of VIEWS_WITH_FORMAT) { + calendar.changeView(viewWithFormat.view) + let header = new (viewWithFormat.view.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper)(calendar).header + expect(header.getCellText(0)).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when locale is Korean', () => { + pushOptions({ + initialDate: '2014-05-11', + locale: koLocale, + }) + + const VIEWS_WITH_FORMAT = [ + { view: 'dayGridMonth', expected: /^일$/ }, + { view: 'dayGridWeek', expected: /^5[.월] 11[.일] \(?일\)?$/ }, + { view: 'timeGridWeek', expected: /^5[.월] 11[.일] \(?일\)?$/ }, + { view: 'dayGridDay', expected: /^일요일$/ }, + { view: 'timeGridDay', expected: /^일요일$/ }, + ] + + it('should have the translated dates and dayHeaderFormat should be computed differently', () => { + let calendar = initCalendar() + + for (let viewWithFormat of VIEWS_WITH_FORMAT) { + calendar.changeView(viewWithFormat.view) + let header = new (viewWithFormat.view.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper)(calendar).header + expect(header.getCellText(0)).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('using custom views', () => { + it('multi-year default only displays day-of-week', () => { + let calendar = initCalendar({ + views: { + dayGridTwoYear: { + type: 'dayGrid', + duration: { years: 2 }, + }, + }, + initialView: 'dayGridTwoYear', + initialDate: '2014-12-25', + }) + let header = new DayGridViewWrapper(calendar).header + expect(header.getCellText(0)).toBe('Sun') + }) + + it('multi-month default only displays day-of-week', () => { + let calendar = initCalendar({ + views: { + dayGridTwoMonth: { + type: 'dayGrid', + duration: { months: 2 }, + }, + }, + initialView: 'dayGridTwoMonth', + initialDate: '2014-12-25', + }) + let header = new DayGridViewWrapper(calendar).header + expect(header.getCellText(0)).toBe('Sun') + }) + + it('multi-week default only displays day-of-week', () => { + let calendar = initCalendar({ + views: { + dayGridTwoWeek: { + type: 'dayGrid', + duration: { weeks: 2 }, + }, + }, + initialView: 'dayGridTwoWeek', + initialDate: '2014-12-25', + }) + let header = new DayGridViewWrapper(calendar).header + expect(header.getCellText(0)).toBe('Sun') + }) + + it('multi-day default displays short full date', () => { + let calendar = initCalendar({ + views: { + multiDay: { + type: 'dayGrid', + duration: { days: 2 }, + }, + }, + initialView: 'multiDay', + initialDate: '2014-12-25', + }) + let header = new DayGridViewWrapper(calendar).header + expect(header.getCellText('2014-12-25')).toMatch(/^Thu 12[/ ]25$/) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/computeEdges.ts b/fullcalendar-main/tests/src/legacy/computeEdges.ts new file mode 100644 index 0000000..b73ebbd --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/computeEdges.ts @@ -0,0 +1,71 @@ +import { computeEdges } from '@fullcalendar/core/internal' +import { getStockScrollbarWidths } from '../lib/dom-misc.js' + +describe('computeEdges', () => { + defineTests( + 'when margin', + { margin: '5px 10px' }, + ) + defineTests( + 'when padding', + { padding: '5px 10px' }, + ) + + defineTests( + 'when border', + { border: '5px solid red' }, + ) + defineTests( + 'when border and padding', + { border: '5px solid red', padding: '5px 10px' }, + ) + + function defineTests(description, cssProps) { + describe(description, () => { + describe('when no scrolling', () => { + describe('when LTR', () => { + defineTest(false, 'ltr', cssProps) + }) + describe('when RTL', () => { + defineTest(false, 'rtl', cssProps) + }) + }) + describe('when scrolling', () => { + describe('when LTR', () => { + defineTest(true, 'ltr', cssProps) + }) + describe('when RTL', () => { + defineTest(true, 'rtl', cssProps) + }) + }) + }) + } + + function defineTest(isScrolling, direction, cssProps) { + it('computes correct widths', () => { + let el = $( + '<div style="position:absolute" />', + ) + .css('overflow', isScrolling ? 'scroll' : 'hidden') + .css('direction', direction) + .css(cssProps) + .append('<div style="position:relative;width:100px;height:100px" />') + .appendTo('body') + + let edges = computeEdges(el[0]) + let correctWidths + + if (isScrolling) { + correctWidths = getStockScrollbarWidths(direction) + } else { + correctWidths = { left: 0, right: 0, bottom: 0 } + } + + expect(edges.scrollbarLeft).toBe(correctWidths.left) + expect(edges.scrollbarRight).toBe(correctWidths.right) + expect(edges.scrollbarBottom).toBe(correctWidths.bottom) + + el.remove() + }) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/computerInnerRect.ts b/fullcalendar-main/tests/src/legacy/computerInnerRect.ts new file mode 100644 index 0000000..f88bdd9 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/computerInnerRect.ts @@ -0,0 +1,103 @@ +import { computeInnerRect } from '@fullcalendar/core/internal' +import { getStockScrollbarWidths } from '../lib/dom-misc.js' + +describe('computeInnerRect', () => { + let INNER_WIDTH = 150 + let INNER_HEIGHT = 100 + let BORDER_LEFT = 1 + let BORDER_RIGHT = 2 + let BORDER_TOP = 3 + let BORDER_BOTTOM = 4 + let PADDING_LEFT = 5 + let PADDING_RIGHT = 6 + let PADDING_TOP = 7 + let PADDING_BOTTOM = 8 + + describeValues({ + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, (direction) => { + let el + + beforeEach(() => { + el = $('<div/>') + .css({ + direction, + position: 'absolute', + top: 0, + left: 0, + borderStyle: 'solid', + borderColor: 'black', + borderLeftWidth: BORDER_LEFT, + borderRightWidth: BORDER_RIGHT, + borderTopWidth: BORDER_TOP, + borderBottomWidth: BORDER_BOTTOM, + paddingLeft: PADDING_LEFT, + paddingRight: PADDING_RIGHT, + paddingTop: PADDING_TOP, + paddingBottom: PADDING_BOTTOM, + }) + .append( + $('<div/>').css({ + width: INNER_WIDTH, + height: INNER_HEIGHT, + }), + ) + .appendTo('body') + }) + + afterEach(() => { + el.remove() + }) + + describe('when no scrolling', () => { + beforeEach(() => { + el.css('overflow', 'hidden') + }) + + it('goes within border', () => { + expect(computeInnerRect(el[0])).toEqual({ + left: BORDER_LEFT, + right: BORDER_LEFT + PADDING_LEFT + INNER_WIDTH + PADDING_RIGHT, + top: BORDER_TOP, + bottom: BORDER_TOP + PADDING_TOP + INNER_HEIGHT + PADDING_BOTTOM, + }) + }) + + it('can go within padding', () => { + expect(computeInnerRect(el[0], true)).toEqual({ + left: BORDER_LEFT + PADDING_LEFT, + right: BORDER_LEFT + PADDING_LEFT + INNER_WIDTH, + top: BORDER_TOP + PADDING_TOP, + bottom: BORDER_TOP + PADDING_TOP + INNER_HEIGHT, + }) + }) + }) + + describe('when scrolling', () => { + beforeEach(() => { + el.css('overflow', 'scroll') + }) + + let stockScrollbars = getStockScrollbarWidths(direction) + + it('goes within border and scrollbars', () => { + expect(computeInnerRect(el[0])).toEqual({ + left: BORDER_LEFT + stockScrollbars.left, + right: BORDER_LEFT + stockScrollbars.left + PADDING_LEFT + INNER_WIDTH + PADDING_RIGHT, + top: BORDER_TOP, + bottom: BORDER_TOP + PADDING_TOP + INNER_HEIGHT + PADDING_BOTTOM, + }) + }) + + it('can go within padding', () => { + expect(computeInnerRect(el[0], true)).toEqual({ + left: BORDER_LEFT + stockScrollbars.left + PADDING_LEFT, + right: BORDER_LEFT + stockScrollbars.left + PADDING_LEFT + INNER_WIDTH, + top: BORDER_TOP + PADDING_TOP, + bottom: BORDER_TOP + PADDING_TOP + INNER_HEIGHT, + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/constraint.ts b/fullcalendar-main/tests/src/legacy/constraint.ts new file mode 100644 index 0000000..290ec25 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/constraint.ts @@ -0,0 +1,885 @@ +import { testEventDrag, testEventResize, testSelection } from '../lib/dnd-resize-utils.js' + +describe('event constraint', () => { + pushOptions({ + initialDate: '2014-11-10', + initialView: 'timeGridWeek', + scrollTime: '00:00', + }) + describe('when used with a specific date range', () => { + describe('when an event is being dragged', () => { + describe('to the middle of the constraint range', () => { + it('allows a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T04:00:00', true, done) + }) + + describe('when in month view with timed event', () => { + it('allows a drag, respects time of day', (done) => { + let options = { + initialView: 'dayGridMonth', + events: [{ + start: '2014-11-10T05:00:00', + end: '2014-11-10T07:00:00', + constraint: { + start: '04:00', + end: '20:00', + }, + }], + } + testEventDrag(options, '2014-11-14', true, () => { + let event = currentCalendar.getEvents()[0] + expect(event.start).toEqualDate('2014-11-14T05:00:00Z') + expect(event.end).toEqualDate('2014-11-14T07:00:00Z') + done() + }) + }) + }) + }) + + describe('to the start of the constraint range', () => { + it('allows a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T03:00:00', true, done) + }) + }) + + describe('to the end of the constraint range', () => { + describe('when the event has an explicit end', () => { + it('allows a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T05:00:00', true, done) + }) + }) + describe('when the event has an implied end', () => { + it('allows a drag', (done) => { + let options = { + defaultTimedEventDuration: '01:30:00', + events: [{ + start: '2014-11-10T01:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T04:30:00', true, done) + }) + }) + }) + + describe('before a constraint range', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T02:00:00', false, done) + }) + }) + + describe('after a constraint range', () => { + describe('using an event object\'s constraint', () => { + describe('when in week view with timed events', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T06:00:00', false, done) + }) + }) + describe('when in month view', () => { + pushOptions({ initialView: 'dayGridMonth' }) + describe('with timed event and all-day constraint', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10', + end: '2014-11-11', + }, + }], + } + testEventDrag(options, '2014-11-12', false, done) + }) + }) + describe('with timed event and timed constraint', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + constraint: { + start: '2014-11-10T00:00:00', + end: '2014-11-11T12:00:00', + }, + }], + } + testEventDrag(options, '2014-11-12', false, done) + }) + }) + describe('with all-day event and all-day constraint', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10', + end: '2014-11-12', + constraint: { + start: '2014-11-09', + end: '2014-11-13', + }, + }], + } + testEventDrag(options, '2014-11-13', false, done) + }) + }) + describe('with all-day event and timed constraint', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10', + end: '2014-11-12', + constraint: { + start: '2014-11-09T01:00:00', + end: '2014-11-12T23:00:00', + }, + }], + } + testEventDrag(options, '2014-11-13', false, done) + }) + }) + }) + }) + describe('using an event source\'s constraint', () => { + it('does not allow a drag', (done) => { + let options = { + eventSources: [{ + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + }], + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T06:00:00', false, done) + }) + }) + describe('using eventConstraint', () => { + it('does not allow a drag and doesnt call eventDataTransform', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T02:00:00', + }], + eventConstraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + eventDataTransform(inData) { + return inData + }, + } + + spyOn(options, 'eventDataTransform').and.callThrough() + + testEventDrag(options, '2014-11-10T06:00:00', false, () => { + expect(options.eventDataTransform.calls.count()).toBe(1) // only initial parse + done() + }) + }) + }) + }) + + describe('intersecting the constraint start', () => { + describe('with no timezone', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T03:00:00', + end: '2014-11-10T05:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T02:00:00', false, done) + }) + }) + describe('with UTC timezone', () => { + it('does not allow a drag', (done) => { + let options = { + timeZone: 'UTC', + events: [{ + start: '2014-11-10T03:00:00+00:00', + end: '2014-11-10T05:00:00+00:00', + constraint: { + start: '2014-11-10T03:00:00+00:00', + end: '2014-11-10T06:00:00+00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T02:00:00+00:00', false, done) + }) + }) + }) + + describe('intersecting the constraint end', () => { + describe('when the event has an explicit end', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T03:00:00', + end: '2014-11-10T05:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T06:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T05:00:00', false, done) + }) + }) + describe('when the event has an implied end', () => { + it('does not allow a drag', (done) => { + let options = { + defaultTimedEventDuration: '02:30', + events: [{ + start: '2014-11-10T03:00:00', + constraint: { + start: '2014-11-10T03:00:00', + end: '2014-11-10T12:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T10:00:00', false, done) + }) + }) + describe('with UTC timezone', () => { + it('does not allow a drag', (done) => { + let options = { + timeZone: 'UTC', + events: [{ + start: '2014-11-10T03:00:00+00:00', + end: '2014-11-10T05:00:00+00:00', + constraint: { + start: '2014-11-10T03:00:00+00:00', + end: '2014-11-10T06:00:00+00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T05:00:00+00:00', false, done) + }) + }) + }) + + describe('into a constraint it encompasses', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-10T01:00:00', + end: '2014-11-10T05:00:00', + constraint: { + start: '2014-11-10T12:00:00', + end: '2014-11-10T14:00:00', + }, + }], + } + testEventDrag(options, '2014-11-10T10:00:00', false, done) + }) + }) + }) + + describe('when an event is being resized', () => { + describe('when the start is already outside the constraint', () => { + it('does not allow a resize', (done) => { + let options = { + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: { + start: '2014-11-12T02:00:00', + end: '2014-11-12T22:00:00', + }, + }], + } + testEventResize(options, '2014-11-12T10:00:00', false, done) + }) + }) + + describe('when resized well within the constraint', () => { + it('allows a resize', (done) => { + let options = { + events: [{ + start: '2014-11-12T02:00:00', + end: '2014-11-12T04:00:00', + constraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T22:00:00', + }, + }], + } + testEventResize(options, '2014-11-12T10:00:00', true, done) + }) + }) + + describe('when resized to the end of the constraint', () => { + it('allows a resize', (done) => { + let options = { + events: [{ + start: '2014-11-12T02:00:00', + end: '2014-11-12T04:00:00', + constraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T06:00:00', + }, + }], + } + testEventResize(options, '2014-11-12T06:00:00', true, done) + }) + }) + + describe('when resized past the end of the constraint', () => { + it('does not allow a resize', (done) => { + let options = { + events: [{ + start: '2014-11-12T02:00:00', + end: '2014-11-12T04:00:00', + constraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T06:00:00', + }, + }], + } + testEventResize(options, '2014-11-12T07:00:00', false, done) + }) + }) + }) + }) + + describe('when used with a recurring date range', () => { + describe('when an event is being dragged', () => { + describe('to the middle of the constraint range', () => { + it('allows a drag', (done) => { + let options = { + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: { + startTime: '04:00:00', + endTime: '08:00:00', + }, + }], + } + testEventDrag(options, '2014-11-12T05:00:00', true, done) + }) + }) + + describe('outside of a constraint range', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: { + startTime: '04:00:00', + endTime: '08:00:00', + }, + }], + } + testEventDrag(options, '2014-11-12T07:00:00', false, done) + }) + }) + + describe('on an off-day of a constraint range', () => { + it('does not allow a drag', (done) => { + let options = { + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: { + startTime: '04:00:00', + endTime: '08:00:00', + daysOfWeek: [0, 1, 2, 3, 5, 6], // except Thursday + }, + }], + } + testEventDrag(options, '2014-11-13T05:00:00', false, done) // drag to Thursday + }) + }) + }) + }) + + describe('when used with businessHours', () => { + describe('when an event is being dragged', () => { + describe('to the middle of the constraint range', () => { + it('allows a drag', (done) => { + let options = { + businessHours: { + startTime: '02:00', + endTime: '06:00', + }, + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T02:00:00', + constraint: 'businessHours', + }], + } + testEventDrag(options, '2014-11-12T03:00:00', true, done) + }) + }) + + describe('outside of a constraint range', () => { + it('does not allow a drag', (done) => { + let options = { + businessHours: { + startTime: '02:00', + endTime: '06:00', + }, + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T02:30:00', + constraint: 'businessHours', + }], + } + testEventDrag(options, '2014-11-12T05:00:00', false, done) + }) + }) + + describe('on an off-day of a constraint range', () => { + it('does not allow a drag', (done) => { + let options = { + businessHours: { + startTime: '02:00', + endTime: '06:00', + daysOfWeek: [1, 2, 3, 4], // Mon - Thurs + }, + events: [{ + start: '2014-11-12T01:00:00', + end: '2014-11-12T02:30:00', + constraint: 'businessHours', + }], + } + testEventDrag(options, '2014-11-14T03:00:00', false, done) // Friday + }) + }) + }) + }) + + describe('when used with an event group ID', () => { + describe('when an event is being dragged', () => { + describe('to the middle of the constraint range', () => { + it('allows a drag', (done) => { + let options = { + events: [ + { + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + className: 'dragging-event', + constraint: 'yo', + }, + { + groupId: 'yo', + start: '2014-11-13T01:00:00', + end: '2014-11-13T05:00:00', + }, + ], + } + testEventDrag(options, '2014-11-13T02:00:00', true, done, 'dragging-event') + }) + }) + + describe('outside of a foreground event constraint', () => { + describe('with an explicit end time', () => { + it('does not allow a drag', (done) => { + let options = { + events: [ + { + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: 'yo', + className: 'dragging-event', + }, + { + id: 'yo', + start: '2014-11-13T01:00:00', + end: '2014-11-13T04:00:00', + }, + ], + } + testEventDrag(options, '2014-11-13T04:00:00', false, done, 'dragging-event') + }) + }) + describe('when an implied end time', () => { + it('does not allow a drag', (done) => { + let options = { + defaultTimedEventDuration: '01:00:00', + events: [ + { + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: 'yo', + className: 'dragging-event', + }, + { + id: 'yo', + start: '2014-11-13T01:00:00', + }, + ], + } + testEventDrag(options, '2014-11-13T01:00:00', false, done, 'dragging-event') + }) + }) + }) + + describe('outside of a background-event constraint', () => { + it('does not allow a drag', (done) => { + let options = { + events: [ + { + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: 'yo', + className: 'dragging-event', + }, + { + id: 'yo', + start: '2014-11-13T01:00:00', + end: '2014-11-13T04:00:00', + display: 'background', + }, + ], + } + testEventDrag(options, '2014-11-13T04:00:00', false, done, 'dragging-event') + }) + }) + + describe('when the event ID constraint matches no events', () => { + it('does not allow a drag', (done) => { + let options = { + events: [ + { + start: '2014-11-12T01:00:00', + end: '2014-11-12T03:00:00', + constraint: 'yo', + }, + ], + } + testEventDrag(options, '2014-11-13T04:00:00', false, done) + }) + }) + + describe('when in month view', () => { + pushOptions({ initialView: 'dayGridMonth' }) + describe('when the event ID constraint matches no events', () => { + it('does not allow a drag', (done) => { + let options = { + events: [ + { + start: '2014-11-12', + end: '2014-11-12', + constraint: 'yo', + }, + ], + } + testEventDrag(options, '2014-11-13', false, done) + }) + }) + }) + }) + }) +}) + +describe('selectConstraint', () => { + pushOptions({ + initialDate: '2014-11-10', + initialView: 'timeGridWeek', + scrollTime: '00:00', + }) + + describe('when used with a specific date range', () => { + describe('when dragged clearly within', () => { + it('allows a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T20:00:00', + }, + } + testSelection(options, '2014-11-12T03:00:00Z', '2014-11-12T10:00:00Z', true, done) + }) + }) + + describe('when dragged within, starting with the constraint start', () => { + it('allows a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T20:00:00', + }, + } + testSelection(options, '2014-11-12T01:00:00Z', '2014-11-12T05:00:00Z', true, done) + }) + }) + + describe('when dragged within, ending with the constraint end', () => { + it('allows a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-12T05:00:00', + }, + } + testSelection(options, '2014-11-12T03:00:00Z', '2014-11-12T05:00:00Z', true, done) + }) + }) + + describe('when dragged intersecting the constraint start', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T03:00:00', + end: '2014-11-12T20:00:00', + }, + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T04:00:00Z', false, done) + }) + }) + + describe('when dragged intersecting the constraint end', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T03:00:00', + end: '2014-11-12T07:00:00', + }, + } + testSelection(options, '2014-11-12T04:00:00Z', '2014-11-12T08:00:00Z', false, done) + }) + }) + + describe('when dragged after the constraint', () => { + describe('when in week view with timed events', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T03:00:00', + end: '2014-11-12T05:00:00', + }, + } + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T07:00:00Z', false, done) + }) + }) + describe('when in month view', () => { + pushOptions({ initialView: 'dayGridMonth' }) + describe('when an all-day constraint', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-13', + end: '2014-11-14', + }, + } + testSelection(options, '2014-11-12', '2014-11-14', false, done) + }) + }) + describe('when a timed constraint, out of bounds', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T01:00:00', + end: '2014-11-14T00:00:00', + }, + } + testSelection(options, '2014-11-12', '2014-11-14', false, done) + }) + }) + describe('when a timed constraint, in bounds', () => { + it('allows a selection', (done) => { + let options = { + selectConstraint: { + start: '2014-11-12T00:00:00', + end: '2014-11-14T00:00:00', + }, + } + testSelection(options, '2014-11-12', '2014-11-14', true, done) + }) + }) + }) + }) + }) + + describe('when used with a recurring date range', () => { + describe('to the middle of the constraint range', () => { + it('allows a selection when in bounds', (done) => { + let options = { + selectConstraint: { + startTime: '01:00:00', + endTime: '05:00:00', + }, + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T04:00:00Z', true, done) + }) + }) + + describe('outside of a constraint range', () => { + it('does not allow a selection when single day', (done) => { + let options = { + selectConstraint: { + startTime: '01:00:00', + endTime: '05:00:00', + }, + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T06:00:00Z', false, done) + }) + it('does not allow a selection when multiday', (done) => { + let options = { + selectConstraint: { + startTime: '01:00:00', + endTime: '05:00:00', + }, + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-14T04:00:00Z', false, done) + }) + }) + }) + + describe('when used with businessHours', () => { + describe('to the middle of the constraint range', () => { + it('allows a selection', (done) => { + let options = { + businessHours: { + startTime: '01:00:00', + endTime: '05:00:00', + }, + selectConstraint: 'businessHours', + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T04:00:00Z', true, done) + }) + }) + + describe('outside of a constraint range', () => { + it('does not allow a selection', (done) => { + let options = { + businessHours: { + startTime: '01:00:00', + endTime: '05:00:00', + }, + selectConstraint: 'businessHours', + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T06:00:00Z', false, done) + }) + }) + + describe('with a custom dow when dragged to a dead day', () => { + it('does not allow a selection', (done) => { + let options = { + businessHours: { + startTime: '01:00:00', + endTime: '05:00:00', + daysOfWeek: [1, 2, 4, 5], // Mon,Tue,Thu,Fri + }, + selectConstraint: 'businessHours', + } + testSelection(options, '2014-11-12T02:00:00Z', '2014-11-12T04:00:00Z', false, done) // Wed + }) + }) + }) + + describe('when used with an event group ID', () => { + describe('to the middle of the constraint range', () => { + it('allows a selection', (done) => { + let options = { + events: [{ + groupId: 'yo', + start: '2014-11-12T02:00:00', + end: '2014-11-12T05:00:00', + display: 'background', + }], + selectConstraint: 'yo', + } + testSelection(options, '2014-11-12T03:00:00Z', '2014-11-12T04:00:00Z', true, done) + }) + }) + + describe('outside of a constraint range', () => { + it('does not allow a selection', (done) => { + let options = { + events: [{ + groupId: 'yo', + start: '2014-11-12T02:00:00', + end: '2014-11-12T05:00:00', + display: 'background', + }], + selectConstraint: 'yo', + } + testSelection(options, '2014-11-12T03:00:00Z', '2014-11-12T06:00:00Z', false, done) + }) + }) + + describe('when event ID does not match any events', () => { + describe('when in week view', () => { + it('does not allow a selection', (done) => { + let options = { + selectConstraint: 'yooo', + } + testSelection(options, '2014-11-12T03:00:00Z', '2014-11-12T06:00:00Z', false, done) + }) + }) + describe('when in month view', () => { + it('does not allow a selection', (done) => { + let options = { + initialView: 'dayGridMonth', + selectConstraint: 'yooo', + } + testSelection(options, '2014-11-12', '2014-11-15', false, done) + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/constructor.ts b/fullcalendar-main/tests/src/legacy/constructor.ts new file mode 100644 index 0000000..a34fc05 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/constructor.ts @@ -0,0 +1,62 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('constructor', () => { + it('should not modify the options object', () => { + let options = { + initialView: 'timeGridWeek', + scrollTime: '09:00:00', + slotDuration: { minutes: 45 }, + } + let optionsCopy = $.extend({}, options, true) + initCalendar(options) + expect(options).toEqual(optionsCopy) + }) + + it('should not modify the events array', () => { + let options = { + initialView: 'dayGridMonth', + initialDate: '2014-05-27', + events: [ + { + title: 'mytitle', + start: '2014-05-27', + }, + ], + } + let optionsCopy = $.extend(true, {}, options) // recursive copy + initCalendar(options) + expect(options).toEqual(optionsCopy) + }) + + it('should not modify the eventSources array', () => { + let options = { + initialView: 'dayGridMonth', + initialDate: '2014-05-27', + eventSources: [ + { events: [ + { + title: 'mytitle', + start: '2014-05-27', + }, + ] }, + ], + } + let optionsCopy = $.extend(true, {}, options) // recursive copy + initCalendar(options) + expect(options).toEqual(optionsCopy) + }) + + describe('when called on a div', () => { + it('should contain a toolbar', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + expect(calendarWrapper.toolbar).toBeTruthy() + }) + + it('should contain a view-container el', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + expect(calendarWrapper.getViewContainerEl()).toBeTruthy() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/current-date.ts b/fullcalendar-main/tests/src/legacy/current-date.ts new file mode 100644 index 0000000..cf6cdb9 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/current-date.ts @@ -0,0 +1,298 @@ +import { FormatRangeOptions } from '@fullcalendar/core' +import { addDays } from '@fullcalendar/core/internal' +import { parseUtcDate } from '../lib/date-parsing.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('current date', () => { + const TITLE_FORMAT: FormatRangeOptions = { + month: 'long', + day: 'numeric', + year: 'numeric', + separator: ' - ', + isEndExclusive: true, + } + + pushOptions({ + titleFormat: TITLE_FORMAT, + titleRangeSeparator: ' - ', + initialDate: '2014-06-01', + timeZone: 'UTC', + }) + + describe('initialDate & getDate', () => { // keep getDate + describeWhenInMonth(() => { + it('should initialize at the date', () => { + let calendar = initCalendar({ + initialDate: '2011-03-10', + }) + expectViewDates(calendar, '2011-02-27', '2011-04-10', '2011-03-01', '2011-04-01') + let currentDate = calendar.getDate() + expect(currentDate instanceof Date).toEqual(true) // test the type, but only here + expect(currentDate).toEqualDate('2011-03-10') + }) + }) + describeWhenInWeek(() => { + it('should initialize at the date, given a date string', () => { + let calendar = initCalendar({ + initialDate: '2011-03-10', + }) + expectViewDates(calendar, '2011-03-06', '2011-03-13') + expect(calendar.getDate()).toEqualDate('2011-03-10') + }) + it('should initialize at the date, given a Date object', () => { + let calendar = initCalendar({ + initialDate: parseUtcDate('2011-03-10'), + }) + expectViewDates(calendar, '2011-03-06', '2011-03-13') + expect(calendar.getDate()).toEqualDate('2011-03-10') + }) + }) + describeWhenInDay(() => { + it('should initialize at the date', () => { + let calendar = initCalendar({ + initialDate: '2011-03-10', + }) + expectViewDates(calendar, '2011-03-10') + expect(calendar.getDate()).toEqualDate('2011-03-10') + }) + }) + }) + + describe('gotoDate', () => { + describeWhenInMonth(() => { + it('should go to a date when given a date string', () => { + let calendar = initCalendar() + calendar.gotoDate('2015-04-01') + expectViewDates(calendar, '2015-03-29', '2015-05-10', '2015-04-01', '2015-05-01') + }) + }) + describeWhenInWeek(() => { + it('should go to a date when given a date string', () => { + let calendar = initCalendar() + calendar.gotoDate('2015-04-01') + expectViewDates(calendar, '2015-03-29', '2015-04-05') + }) + it('should go to a date when given a date string with a time', () => { + let calendar = initCalendar() + calendar.gotoDate('2015-04-01T12:00:00') + expectViewDates(calendar, '2015-03-29', '2015-04-05') + }) + it('should go to a date when given a Date object', () => { + let calendar = initCalendar() + calendar.gotoDate(parseUtcDate('2015-04-01')) + expectViewDates(calendar, '2015-03-29', '2015-04-05') + }) + }) + describeWhenInDay(() => { + it('should go to a date when given a date string', () => { + let calendar = initCalendar() + calendar.gotoDate('2015-04-01') + expectViewDates(calendar, '2015-04-01') + }) + }) + }) + + describe('incrementDate', () => { + describeWhenInMonth(() => { + it('should increment the date when given a Duration object', () => { + let calendar = initCalendar() + calendar.incrementDate({ months: -1 }) + expectViewDates(calendar, '2014-04-27', '2014-06-08', '2014-05-01', '2014-06-01') + }) + }) + describeWhenInWeek(() => { + it('should increment the date when given a Duration object', () => { + let calendar = initCalendar() + calendar.incrementDate({ weeks: -2 }) + expectViewDates(calendar, '2014-05-18', '2014-05-25') + }) + }) + describeWhenInDay(() => { + it('should increment the date when given a Duration object', () => { + let calendar = initCalendar() + calendar.incrementDate({ days: 2 }) + expectViewDates(calendar, '2014-06-03') + }) + it('should increment the date when given a Duration string', () => { + let calendar = initCalendar() + calendar.incrementDate('2.00:00:00') + expectViewDates(calendar, '2014-06-03') + }) + it('should increment the date when given a Duration string with a time', () => { + let calendar = initCalendar() + calendar.incrementDate('2.05:30:00') + expectViewDates(calendar, '2014-06-03') + }) + }) + }) + + describe('prevYear', () => { + describeWhenInMonth(() => { + it('should move the calendar back a year', () => { + let calendar = initCalendar() + calendar.prevYear() + expectViewDates(calendar, '2013-05-26', '2013-07-07', '2013-06-01', '2013-07-01') + }) + }) + describeWhenInWeek(() => { + it('should move the calendar back a year', () => { + let calendar = initCalendar() + calendar.prevYear() + expectViewDates(calendar, '2013-05-26', '2013-06-02') + }) + }) + describeWhenInDay(() => { + it('should move the calendar back a year', () => { + let calendar = initCalendar() + calendar.prevYear() + expectViewDates(calendar, '2013-06-01') + }) + }) + }) + + describe('nextYear', () => { + describeWhenInMonth(() => { + it('should move the calendar forward a year', () => { + let calendar = initCalendar() + calendar.nextYear() + expectViewDates(calendar, '2015-05-31', '2015-07-12', '2015-06-01', '2015-07-01') + }) + }) + describeWhenInWeek(() => { + it('should move the calendar forward a year', () => { + let calendar = initCalendar() + calendar.nextYear() + expectViewDates(calendar, '2015-05-31', '2015-06-07') + }) + }) + describeWhenInDay(() => { + it('should move the calendar forward a year', () => { + let calendar = initCalendar() + calendar.nextYear() + expectViewDates(calendar, '2015-06-01') + }) + }) + }) + + describe('when current date is a hidden day', () => { + describeWhenInMonth(() => { + it('should display the current month even if first day of month', () => { + let calendar = initCalendar({ + now: '2014-06-01', // a Sunday + initialDate: '2014-06-01', // a Sunday + weekends: false, + }) + let view = calendar.view + expect(view.activeStart).toEqualDate('2014-06-02') + expect(view.activeEnd).toEqualDate('2014-07-12') + expect(view.currentStart).toEqualDate('2014-06-01') + expect(view.currentEnd).toEqualDate('2014-07-01') + }) + it('should display the current month', () => { + let calendar = initCalendar({ + now: '2014-05-04', // a Sunday + initialDate: '2014-05-04', // a Sunday + weekends: false, + }) + let view = calendar.view + expect(view.activeStart).toEqualDate('2014-04-28') + expect(view.activeEnd).toEqualDate('2014-06-07') + expect(view.currentStart).toEqualDate('2014-05-01') + expect(view.currentEnd).toEqualDate('2014-06-01') + }) + describe('when navigating back a month', () => { + it('should not skip months', () => { + let calendar = initCalendar({ + initialDate: '2014-07-07', + weekends: false, + }) + let view = calendar.view + expect(view.currentStart).toEqualDate('2014-07-01') + expect(view.currentEnd).toEqualDate('2014-08-01') + calendar.prev() // will move to Jun 1, which is a Sunday + view = calendar.view + expect(view.currentStart).toEqualDate('2014-06-01') + expect(view.currentEnd).toEqualDate('2014-07-01') + }) + }) + }) + describeWhenInDay(() => { + it('should display the next visible day', () => { + let calendar = initCalendar({ + now: '2014-06-01', // a Sunday + initialDate: '2014-06-01', // a Sunday + weekends: false, + }) + let view = calendar.view + expect(view.activeStart).toEqualDate('2014-06-02') + expect(view.activeEnd).toEqualDate('2014-06-03') + expect(view.currentStart).toEqualDate('2014-06-02') + expect(view.currentEnd).toEqualDate('2014-06-03') + }) + }) + }) + + // UTILS + // ----- + + function describeWhenInMonth(func) { + describeWhenIn('dayGridMonth', func) + } + + function describeWhenInWeek(func) { + describeWhenIn('dayGridWeek', func) + describeWhenIn('timeGridWeek', func) + } + + function describeWhenInDay(func) { + describeWhenIn('dayGridDay', func) + describeWhenIn('timeGridDay', func) + } + + function describeWhenIn(viewName, func) { + describe('when in ' + viewName, () => { + pushOptions({ initialView: viewName }) + func() + }) + } + + function expectViewDates(calendar, start, end?, titleStart?, titleEnd?) { + let view = calendar.view + let calculatedEnd + let title + + if (typeof start === 'string') { + start = new Date(start) + } + if (typeof end === 'string') { + end = new Date(end) + } + if (typeof titleStart === 'string') { + titleStart = new Date(titleStart) + } + if (typeof titleEnd === 'string') { + titleEnd = new Date(titleEnd) + } + + calculatedEnd = end || addDays(start, 1) + + expect(start).toEqualDate(view.activeStart) + expect(calculatedEnd).toEqualDate(view.activeEnd) + + titleStart = titleStart || start + titleEnd = titleEnd || calculatedEnd + + if (titleEnd) { + title = calendar.formatRange( + titleStart, + titleEnd, + TITLE_FORMAT, + ) + } else { + title = calendar.formatDate(titleStart, TITLE_FORMAT) + } + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe(title) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/custom-view-class.ts b/fullcalendar-main/tests/src/legacy/custom-view-class.ts new file mode 100644 index 0000000..f88140a --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/custom-view-class.ts @@ -0,0 +1,98 @@ +import { createPlugin } from '@fullcalendar/core' +import { sliceEvents } from '@fullcalendar/core' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('custom view class', () => { // TODO: rename file + it('calls all standard methods with correct parameters', () => { + const CustomViewConfig = { + classNames: 'awesome-view', + didMount() {}, + willUnmount() {}, + + content(props) { + expect(props.dateProfile.activeRange.start instanceof Date).toBe(true) + expect(props.dateProfile.activeRange.end instanceof Date).toBe(true) + + let eventRanges = sliceEvents(props, true) // allDay=true + expect(Array.isArray(eventRanges)).toBe(true) + expect(eventRanges.length).toBe(1) + expect(typeof eventRanges[0].def).toBe('object') + expect(typeof eventRanges[0].ui).toBe('object') + expect(typeof eventRanges[0].instance).toBe('object') + expect(eventRanges[0].isStart).toBe(true) + expect(eventRanges[0].isEnd).toBe(true) + expect(eventRanges[0].range.start instanceof Date).toBe(true) + expect(eventRanges[0].range.end instanceof Date).toBe(true) + + let dateSelection = props.dateSelection + if (!dateSelection) { + expect(dateSelection).toBe(null) + } else { + expect(typeof dateSelection).toBe('object') + expect(dateSelection.allDay).toBe(true) + expect(dateSelection.range.start instanceof Date).toBe(true) + expect(dateSelection.range.end instanceof Date).toBe(true) + } + + return { html: '<div class="hello-world">hello world</div>' } + }, + } + + spyOn(CustomViewConfig, 'didMount').and.callThrough() + spyOn(CustomViewConfig, 'content').and.callThrough() + spyOn(CustomViewConfig, 'willUnmount').and.callThrough() + + function resetCounts() { + CustomViewConfig.didMount.calls.reset() + CustomViewConfig.content.calls.reset() + CustomViewConfig.willUnmount.calls.reset() + } + + let calendar = initCalendar({ + plugins: [ + createPlugin({ + name: 'test-plugin', + views: { + custom: CustomViewConfig, + }, + }), + ], + initialView: 'custom', + initialDate: '2014-12-25', // will end up being a single-day view + events: [ + { + title: 'Holidays', + start: '2014-12-25T09:00:00', + end: '2014-12-25T11:00:00', + }, + ], + }) + let calendarWrapper = new CalendarWrapper(calendar) + + let viewEl = calendarWrapper.getViewEl() + expect(viewEl).toHaveClass('awesome-view') + expect($(viewEl).find('.hello-world').length).toBe(1) + + expect(CustomViewConfig.didMount.calls.count()).toBe(1) + expect(CustomViewConfig.content.calls.count()).toBe(1) + expect(CustomViewConfig.willUnmount.calls.count()).toBe(0) + + resetCounts() + calendar.select('2014-12-25', '2014-01-01') + expect(CustomViewConfig.didMount.calls.count()).toBe(0) + expect(CustomViewConfig.content.calls.count()).toBe(1) + expect(CustomViewConfig.willUnmount.calls.count()).toBe(0) + + resetCounts() + calendar.unselect() + expect(CustomViewConfig.didMount.calls.count()).toBe(0) + expect(CustomViewConfig.content.calls.count()).toBe(1) + expect(CustomViewConfig.willUnmount.calls.count()).toBe(0) + + resetCounts() + calendar.destroy() + expect(CustomViewConfig.didMount.calls.count()).toBe(0) + expect(CustomViewConfig.content.calls.count()).toBe(0) + expect(CustomViewConfig.willUnmount.calls.count()).toBe(1) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/custom-view-duration.ts b/fullcalendar-main/tests/src/legacy/custom-view-duration.ts new file mode 100644 index 0000000..df8d1cf --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/custom-view-duration.ts @@ -0,0 +1,454 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { createPlugin } from '@fullcalendar/core' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('custom view', () => { + it('renders a 4 day dayGrid view', () => { + let calendar = initCalendar({ + views: { + dayGridFourDay: { + type: 'dayGrid', + duration: { days: 4 }, + }, + }, + initialView: 'dayGridFourDay', + initialDate: '2014-12-25', + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dayEls = dayGridWrapper.getAllDayEls() + + expect(dayGridWrapper.getRowEls().length).toBe(1) + expect(dayEls.length).toBe(4) + expect(dayEls[0].getAttribute('data-date')).toBe('2014-12-25') // starts on initialDate + }) + + it('renders a 2 week dayGrid view', () => { + let calendar = initCalendar({ + views: { + dayGridTwoWeek: { + type: 'dayGrid', + duration: { weeks: 2 }, + }, + }, + initialView: 'dayGridTwoWeek', + initialDate: '2014-12-25', + firstDay: 2, // Tues + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dayEls = dayGridWrapper.getAllDayEls() + + expect(dayGridWrapper.getRowEls().length).toBe(2) + expect(dayEls.length).toBe(14) + expect(dayEls[0]).toHaveClass(CalendarWrapper.DOW_CLASSNAMES[2]) // respects start-of-week + expect(dayEls[0].getAttribute('data-date')).toBe('2014-12-23') // week start. tues + }) + + it('will use the provided options', () => { + let calendar = initCalendar({ + views: { + dayGridFourDay: { + type: 'dayGrid', + duration: { days: 4 }, + titleFormat() { return 'special' }, + }, + }, + initialView: 'dayGridFourDay', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('special') + }) + + it('will inherit options from the parent view type', () => { + let calendar = initCalendar({ + views: { + dayGrid: { + titleFormat() { return 'dayGridtitle' }, + }, + dayGridFourDay: { + type: 'dayGrid', + duration: { days: 4 }, + }, + }, + initialView: 'dayGridFourDay', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('dayGridtitle') + }) + + it('will override an option from the parent view type', () => { + let calendar = initCalendar({ + views: { + dayGrid: { + titleFormat() { return 'dayGridtitle' }, + }, + dayGridFourDay: { + type: 'dayGrid', + duration: { days: 4 }, + titleFormat() { return 'dayGridfourweekttitle' }, + }, + }, + initialView: 'dayGridFourDay', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('dayGridfourweekttitle') + }) + + it('will inherit options from generic "week" type', () => { + let calendar = initCalendar({ + views: { + week: { + titleFormat() { return 'weektitle' }, + }, + dayGridOneWeek: { + type: 'dayGrid', + duration: { weeks: 1 }, + }, + }, + initialView: 'dayGridOneWeek', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('weektitle') + }) + + it('generic type options for "dayGrid" will override generic "week" options', () => { + let calendar = initCalendar({ + views: { + week: { + titleFormat() { return 'weektitle' }, + }, + dayGrid: { + titleFormat() { return 'dayGridtitle' }, + }, + dayGridOneWeek: { + type: 'dayGrid', + duration: { weeks: 1 }, + }, + }, + initialView: 'dayGridOneWeek', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('dayGridtitle') + }) + + it('will not inherit "week" options if more than a single week', () => { + let calendar = initCalendar({ + titleFormat() { return 'defaultitle' }, + initialView: 'dayGridTwoWeek', + views: { + week: { + titleFormat() { return 'weektitle' }, + }, + dayGridTwoWeek: { + type: 'dayGrid', + duration: { weeks: 2 }, + }, + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('defaultitle') + }) + + it('renders a 4 day timeGrid view', () => { + let calendar = initCalendar({ + initialView: 'timeGridFourDay', + initialDate: '2014-12-25', + views: { + timeGridFourDay: { + type: 'timeGrid', + duration: { days: 4 }, + }, + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let timeGridDayEls = viewWrapper.timeGrid.getAllDayEls() + + expect(viewWrapper.dayGrid.getRowEls().length).toBe(1) + expect(viewWrapper.dayGrid.getAllDayEls().length).toBe(4) + expect(timeGridDayEls.length).toBe(4) + expect(timeGridDayEls[0].getAttribute('data-date')).toBe('2014-12-25') // starts on initialDate + }) + + it('renders a two week timeGrid view', () => { + let calendar = initCalendar({ + initialView: 'timeGridTwoWeek', + initialDate: '2014-12-25', + views: { + timeGridTwoWeek: { + type: 'timeGrid', + duration: { weeks: 2 }, + }, + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let timeGridDayEls = viewWrapper.timeGrid.getAllDayEls() + + expect(viewWrapper.dayGrid.getRowEls().length).toBe(1) + expect(viewWrapper.dayGrid.getAllDayEls().length).toBe(14) + expect(timeGridDayEls.length).toBe(14) + expect(timeGridDayEls[0].getAttribute('data-date')).toBe('2014-12-21') // week start + }) + + it('renders a two month timeGrid view', () => { + let calendar = initCalendar({ + initialView: 'timeGridTwoWeek', + initialDate: '2014-11-27', + views: { + timeGridTwoWeek: { + type: 'timeGrid', + duration: { months: 2 }, + }, + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let timeGridDayEls = viewWrapper.timeGrid.getAllDayEls() + + expect(viewWrapper.dayGrid.getRowEls().length).toBe(1) + expect(viewWrapper.dayGrid.getAllDayEls().length).toBe(61) + expect(timeGridDayEls.length).toBe(61) + expect(timeGridDayEls[0].getAttribute('data-date')).toBe('2014-11-01') + expect(timeGridDayEls[timeGridDayEls.length - 1].getAttribute('data-date')).toBe('2014-12-31') // last + }) + + it('renders a two month dayGrid view', () => { + let calendar = initCalendar({ + initialView: 'dayGridTwoWeek', + initialDate: '2014-11-27', + views: { + dayGridTwoWeek: { + type: 'dayGrid', + duration: { months: 2 }, + }, + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dayEls = dayGridWrapper.getAllDayEls() + + expect(dayGridWrapper.getRowEls().length).toBe(10) + expect(dayGridWrapper.getDayElsInRow(0).length).toBe(7) + expect(dayEls[0].getAttribute('data-date')).toBe('2014-10-26') + expect(dayEls[dayEls.length - 1].getAttribute('data-date')).toBe('2015-01-03') + }) + + it('renders a one year dayGrid view', () => { + let options = { + initialView: 'dayGridYear', + initialDate: '2014-11-27', + views: { + dayGridYear: { + type: 'dayGrid', + duration: { years: 1 }, + }, + }, + } + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dayEls = dayGridWrapper.getAllDayEls() + + expect(dayEls[0]).toBeMatchedBy('[data-date="2013-12-29"]') + expect(dayEls[dayEls.length - 1]).toBeMatchedBy('[data-date="2015-01-03"]') + }) + + describe('buttonText', () => { + it('accepts buttonText exact-match override', () => { + let options = { + buttonText: { + custom: 'over-ridden', + }, + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 4 }, + buttonText: 'awesome', + }, + }, + } + let calendar = initCalendar(options) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('over-ridden') + }) + + it('accepts buttonText single-unit-match override', () => { + let options = { + buttonText: { + day: '1day-over-ridden', + }, + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 1 }, + buttonText: 'awesome', + }, + }, + } + let calendar = initCalendar(options) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('1day-over-ridden') + }) + + it('does not accept buttonText unit-match override when unit is more than one', () => { + let options = { + buttonText: { + day: '1day!!!???', + }, + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 2 }, + buttonText: 'awesome', + }, + }, + } + let calendar = initCalendar(options) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('awesome') + }) + + it('accepts locale\'s single-unit-match override', () => { + let calendar = initCalendar({ + locale: frLocale, + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 1 }, + }, + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('Jour') + }) + + it('accepts explicit View-Specific buttonText, overriding locale\'s single-unit-match override', () => { + let calendar = initCalendar({ + locale: frLocale, + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 1 }, + buttonText: 'awesome', + }, + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('awesome') + }) + + it('respects custom view\'s value', () => { + let options = { + headerToolbar: { + center: 'custom,dayGridMonth', + }, + initialView: 'custom', + views: { + custom: { + type: 'dayGrid', + duration: { days: 4 }, + buttonText: 'awesome', + }, + }, + } + let calendar = initCalendar(options) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('custom') + + expect(buttonInfo.text).toBe('awesome') + }) + + it('respects custom view\'s value, even when a "smart" property name', () => { + let options = { + headerToolbar: { + center: 'dayGridFourDay,dayGridMonth', + }, + initialView: 'dayGridFourDay', + views: { + dayGridFourDay: { // "dayGridFourDay" is a pitfall for smartProperty + type: 'dayGrid', + duration: { days: 4 }, + buttonText: 'awesome', + }, + }, + } + let calendar = initCalendar(options) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('dayGridFourDay') + + expect(buttonInfo.text).toBe('awesome') + }) + + it('falls back to view name when view lacks metadata', () => { + // also sorta tests plugin system + + let calendar = initCalendar({ + plugins: [ + createPlugin({ + name: 'test-plugin', + views: { + crazy: { + content: 'hello world', + }, + }, + }), + ], + headerToolbar: { + center: 'crazy,dayGridMonth', + }, + initialView: 'crazy', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('crazy') + + expect(buttonInfo.text).toBe('crazy') + }) + }) + + it('throws an error when type is self', () => { + let error = null + + try { + initCalendar({ + initialView: 'month', + views: { + month: { + type: 'month', + }, + }, + }) + } catch (_error) { + error = _error + } + + expect(error).toBeTruthy() + expect(error.message).toBe('Can\'t have a custom view type that references itself') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/dateClick.ts b/fullcalendar-main/tests/src/legacy/dateClick.ts new file mode 100644 index 0000000..0592006 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/dateClick.ts @@ -0,0 +1,193 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dateClick', () => { + pushOptions({ + initialDate: '2014-05-27', + selectable: false, + timeZone: 'UTC', + }) + + describeOptions('direction', { + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, () => { + describeOptions('selectable', { + 'when NOT selectable': false, + 'when selectable': true, + }, () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('fires correctly when clicking on a cell', (done) => { + let calendar = initCalendar({ + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.date).toEqualDate('2014-05-07') + expect(arg.dateStr).toEqual('2014-05-07') + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + dayGridWrapper.clickDate('2014-05-07') + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('fires correctly when clicking on an all-day slot', (done) => { + let calendar = initCalendar({ + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.date).toEqualDate('2014-05-28') + expect(arg.dateStr).toEqual('2014-05-28') + done() + }, + }) + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + dayGridWrapper.clickDate('2014-05-28') + }) + + it('fires correctly when clicking on a timed slot', (done) => { + let calendar = initCalendar({ + contentHeight: 500, // make sure the click slot will be in scroll view + scrollTime: '07:00:00', + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.date).toEqualDate('2014-05-28T09:00:00Z') + expect(arg.dateStr).toEqual('2014-05-28T09:00:00Z') + done() + }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + timeGridWrapper.clickDate('2014-05-28T09:00:00') + }) + + // issue 2217 + it('fires correctly when clicking on a timed slot, with slotMinTime set', (done) => { + let calendar = initCalendar({ + contentHeight: 500, // make sure the click slot will be in scroll view + scrollTime: '07:00:00', + slotMinTime: '02:00:00', + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.date).toEqualDate('2014-05-28T11:00:00Z') + expect(arg.dateStr).toEqual('2014-05-28T11:00:00Z') + done() + }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + timeGridWrapper.clickDate('2014-05-28T11:00:00') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4539 + it('fires correctly when clicking on a timed slot NEAR END', (done) => { + let calendar = initCalendar({ + contentHeight: 500, // make sure the click slot will be in scroll view + scrollTime: '23:00:00', + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.date).toEqualDate('2014-05-28T23:30:00Z') + expect(arg.dateStr).toEqual('2014-05-28T23:30:00Z') + done() + }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + timeGridWrapper.clickDate('2014-05-28T23:30:00') + }) + }) + }) + }) + + it('will still fire if clicked on background event', (done) => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + events: [{ + start: '2014-05-06', + display: 'background', + }], + dateClick(info) { + expect(info.dateStr).toBe('2014-05-06') + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $.simulateMouseClick(dayGridWrapper.getBgEventEls()[0]) + }) + + describe('when touch', () => { + it('fires correctly when simulated short drag on a cell', (done) => { + let calendar = initCalendar({ + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.date).toEqualDate('2014-05-07') + expect(arg.dateStr).toEqual('2014-05-07') + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $.simulateTouchClick(dayGridWrapper.getDayEl('2014-05-07')) + }) + + it('won\'t fire if touch moves outside of date cell', (done) => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + let startCell = dayGridWrapper.getDayEl('2014-05-07') + let endCell = dayGridWrapper.getDayEl('2014-05-08') + + $(startCell).simulate('drag', { + // FYI, when debug:true, not a good representation because the minimal delay is required + // to recreate bug #3332 + isTouch: true, + end: endCell, + callback() { + expect(dateClickSpy).not.toHaveBeenCalled() + done() + }, + }) + }) + + it('fires correctly when simulated click on a cell', (done) => { + let calendar = initCalendar({ + dateClick(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.date).toEqualDate('2014-05-07') + expect(arg.dateStr).toEqual('2014-05-07') + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + let dayCell = dayGridWrapper.getDayEl('2014-05-07') + $.simulateTouchClick(dayCell) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/dayNames.ts b/fullcalendar-main/tests/src/legacy/dayNames.ts new file mode 100644 index 0000000..64d9a2e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/dayNames.ts @@ -0,0 +1,61 @@ +import { addDays } from '@fullcalendar/core/internal' +import { removeLtrCharCodes } from '../lib/string.js' +import { parseUtcDate } from '../lib/date-parsing.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('day names', () => { + let sundayDate = parseUtcDate('2019-03-17') + let locales = ['es', 'fr', 'de', 'zh-cn', 'nl'] + + pushOptions({ + now: sundayDate, + }) + + describe('when view is dayGridDay', () => { + pushOptions({ + initialView: 'dayGridDay', + }) + + describe('when locale is default', () => { + pushOptions({ + locale: 'en', + }) + + CalendarWrapper.DOW_CLASSNAMES.forEach((dowClassName, index) => { + let dayDate = addDays(sundayDate, index) + let dayText = removeLtrCharCodes( + dayDate.toLocaleString('en', { weekday: 'long', timeZone: 'UTC' }), + ) + + it('should be ' + dayText, () => { + let calendar = initCalendar({ + now: dayDate, + }) + let headerWrapper = new DayGridViewWrapper(calendar).header + expect(headerWrapper.el.querySelector(`.${dowClassName}`)).toHaveText(dayText) + }) + }) + }) + + $.each(locales, (localeIndex, locale) => { + describe('when locale is ' + locale, () => { + CalendarWrapper.DOW_CLASSNAMES.forEach((dowClassName, index) => { + let dayDate = addDays(sundayDate, index) + let dayText = removeLtrCharCodes( + dayDate.toLocaleString(locale, { weekday: 'long', timeZone: 'UTC' }), + ) + + it('should be the translation for ' + dayText, () => { + let calendar = initCalendar({ + locale, + now: dayDate, + }) + let headerWrapper = new DayGridViewWrapper(calendar).header + expect(headerWrapper.el.querySelector(`.${dowClassName}`)).toHaveText(dayText) + }) + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/dayPopoverFormat.ts b/fullcalendar-main/tests/src/legacy/dayPopoverFormat.ts new file mode 100644 index 0000000..b7d2e4e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/dayPopoverFormat.ts @@ -0,0 +1,55 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('dayPopoverFormat', () => { + pushOptions({ + initialDate: '2014-08-01', + dayMaxEventRows: 3, + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30', className: 'event1' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31', className: 'event2' }, + { title: 'event3', start: '2014-07-29', className: 'event3' }, + { title: 'event4', start: '2014-07-29', className: 'event4' }, + ], + }) + + it('can be set to a custom value', (done) => { + let calendar = initCalendar({ + dayPopoverFormat: { month: 'long', day: 'numeric' }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getMoreEl()).simulate('click') + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverTitle()).toBe('July 29') + done() + }) + }) + + it('is affected by the current locale when the value is default', (done) => { + let calendar = initCalendar({ + locale: frLocale, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getMoreEl()).simulate('click') + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverTitle()).toBe('29 juillet 2014') + done() + }) + }) + + it('still maintains the same format when explicitly set, and there is a locale', (done) => { + let calendar = initCalendar({ + locale: frLocale, + dayPopoverFormat: { year: 'numeric' }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getMoreEl()).simulate('click') + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverTitle()).toBe('2014') + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/dayRender.ts b/fullcalendar-main/tests/src/legacy/dayRender.ts new file mode 100644 index 0000000..a9f1136 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/dayRender.ts @@ -0,0 +1,85 @@ +import { formatIsoDay } from '../lib/datelib-utils.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('dayCellDidMount', () => { // TODO: rename file + it('is triggered upon initialization of a view, with correct parameters', () => { + let options = { + initialView: 'dayGridMonth', + fixedWeekCount: true, + initialDate: '2014-05-01', + dayCellDidMount(arg) { + expect(arg.date instanceof Date).toEqual(true) + expect(formatIsoDay(arg.date)).toEqual(arg.el.getAttribute('data-date')) + expect(arg.el instanceof HTMLElement).toBe(true) + }, + } + + spyOn(options, 'dayCellDidMount').and.callThrough() + initCalendar(options) + expect(options.dayCellDidMount.calls.count()).toEqual(42) + }) + + it('is called when date range is changed', () => { + let options = { + initialView: 'dayGridWeek', + initialDate: '2014-05-01', + dayCellDidMount(arg) { }, + } + + spyOn(options, 'dayCellDidMount').and.callThrough() + initCalendar(options) + options.dayCellDidMount.calls.reset() + currentCalendar.gotoDate('2014-05-04') // a day in the next week + expect(options.dayCellDidMount.calls.count()).toEqual(7) + }) + + it('won\'t be called when date is navigated but remains in the current visible range', () => { + let options = { + initialView: 'dayGridWeek', + initialDate: '2014-05-01', + dayCellDidMount(arg) { }, + } + + spyOn(options, 'dayCellDidMount').and.callThrough() + initCalendar(options) + options.dayCellDidMount.calls.reset() + currentCalendar.gotoDate('2014-05-02') // a day in the same week + expect(options.dayCellDidMount.calls.count()).toEqual(0) + }) + + it('allows you to modify the element', () => { + let options = { + initialView: 'dayGridMonth', + fixedWeekCount: true, + initialDate: '2014-05-01', + dayCellDidMount(arg) { + if (formatIsoDay(arg.date) === '2014-05-01') { + arg.el.classList.add('mycustomclass') + } + }, + } + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dayEl = dayGridWrapper.getDayEl('2014-05-01') + expect(dayEl).toHaveClass('mycustomclass') + }) + + it('gets called for TimeGrid views', () => { + let callCnt = 0 + let options = { + initialView: 'timeGridWeek', + initialDate: '2014-05-01', + allDaySlot: false, // turn off. fires its own dayCellDidMount + dayCellDidMount(arg) { + expect(arg.date instanceof Date).toBe(true) + expect(arg.el instanceof HTMLElement).toBe(true) + expect(typeof arg.view).toBe('object') + callCnt += 1 + }, + } + + initCalendar(options) + expect(callCnt).toBe(7) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/defaultAllDayEventDuration.ts b/fullcalendar-main/tests/src/legacy/defaultAllDayEventDuration.ts new file mode 100644 index 0000000..0f73ceb --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/defaultAllDayEventDuration.ts @@ -0,0 +1,85 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('defaultAllDayEventDuration', () => { + pushOptions({ + initialDate: '2014-05-01', + initialView: 'dayGridMonth', + timeZone: 'UTC', + }) + + describe('when forceEventDuration is on', () => { + pushOptions({ + forceEventDuration: true, + }) + + it('correctly calculates an unspecified end when using a Duration object input', () => { + initCalendar({ + defaultAllDayEventDuration: { days: 2 }, + events: [ + { + allDay: true, + start: '2014-05-05', + }, + ], + }) + + let event = currentCalendar.getEvents()[0] + expect(event.end).toEqualDate('2014-05-07') + }) + + it('correctly calculates an unspecified end when using a string Duration input', () => { + initCalendar({ + defaultAllDayEventDuration: '3.00:00:00', + events: [ + { + allDay: true, + start: '2014-05-05', + }, + ], + }) + + let event = currentCalendar.getEvents()[0] + expect(event.end).toEqualDate('2014-05-08') + }) + }) + + describe('when forceEventDuration is off', () => { + pushOptions({ + forceEventDuration: false, + }) + + describeOptions('initialView', { + 'with dayGridWeek view': 'dayGridWeek', + 'with week view': 'timeGridWeek', + }, () => { + it('renders an all-day event with no `end` to appear to have the default duration', () => { + let calendar = initCalendar({ + defaultAllDayEventDuration: { days: 2 }, + events: [ + { + // a control. so we know how wide it should be + title: 'control event', + allDay: true, + start: '2014-04-28', + end: '2014-04-30', + }, + { + // one day after the control. no specified end + title: 'test event', + allDay: true, + start: '2014-04-28', + }, + ], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventElms = calendarWrapper.getEventEls() + + let width0 = eventElms[0].offsetWidth + let width1 = eventElms[1].offsetWidth + expect(width0).toBeGreaterThan(0) + expect(width0).toEqual(width1) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/defaultTimedEventDuration.ts b/fullcalendar-main/tests/src/legacy/defaultTimedEventDuration.ts new file mode 100644 index 0000000..70af495 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/defaultTimedEventDuration.ts @@ -0,0 +1,119 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('defaultTimedEventDuration', () => { + pushOptions({ + initialDate: '2014-05-01', + initialView: 'dayGridMonth', + timeZone: 'UTC', + }) + + describe('when forceEventDuration is on', () => { + pushOptions({ + forceEventDuration: true, + }) + + it('correctly calculates an unspecified end when using a Duration object input', () => { + initCalendar({ + defaultTimedEventDuration: { hours: 2, minutes: 30 }, + events: [ + { + allDay: false, + start: '2014-05-05T04:00:00', + }, + ], + }) + let event = currentCalendar.getEvents()[0] + expect(event.end).toEqualDate('2014-05-05T06:30:00Z') + }) + + it('correctly calculates an unspecified end when using a string Duration input', () => { + initCalendar({ + defaultTimedEventDuration: '03:15:00', + events: [ + { + allDay: false, + start: '2014-05-05T04:00:00', + }, + ], + }) + let event = currentCalendar.getEvents()[0] + expect(event.end).toEqualDate('2014-05-05T07:15:00Z') + }) + }) + + describe('when forceEventDuration is off', () => { + pushOptions({ + forceEventDuration: false, + }) + + describe('with week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('renders a timed event with no `end` to appear to have the default duration', () => { + let calendar = initCalendar({ + defaultTimedEventDuration: '01:15:00', + events: [ + { + // a control. so we know how tall it should be + title: 'control event', + allDay: false, + start: '2014-05-01T04:00:00', + end: '2014-05-01T05:15:00', + }, + { + // one day after the control. no specified end + title: 'test event', + allDay: false, + start: '2014-05-02T04:00:00', + }, + ], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventElms = calendarWrapper.getEventEls() + + let height0 = eventElms[0].offsetHeight + let height1 = eventElms[1].offsetHeight + expect(height0).toBeGreaterThan(0) + expect(height0).toEqual(height1) + }) + }) + + describe('with dayGridWeek view', () => { + pushOptions({ + initialView: 'dayGridWeek', + }) + + it('renders a timed event with no `end` to appear to have the default duration', () => { + let calendar = initCalendar({ + defaultTimedEventDuration: { days: 2 }, + events: [ + { + // a control. so we know how wide it should be + title: 'control event', + allDay: false, + start: '2014-04-28T04:00:00', + end: '2014-04-30T04:00:00', + }, + { + // one day after the control. no specified end + title: 'test event', + allDay: false, + start: '2014-04-28T04:00:00', + }, + ], + }) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventElms = calendarWrapper.getEventEls() + + let width0 = eventElms[0].offsetWidth + let width1 = eventElms[1].offsetWidth + expect(width0).toBeGreaterThan(0) + expect(width0).toEqual(width1) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/destroy.ts b/fullcalendar-main/tests/src/legacy/destroy.ts new file mode 100644 index 0000000..97634f5 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/destroy.ts @@ -0,0 +1,89 @@ +import { ListenerCounter } from '../lib/ListenerCounter.js' +import { prepareStandardListeners } from '../lib/vdom-misc.js' + +describe('destroy', () => { + describe('when calendar is LTR', () => { + it('cleans up all classNames on the root element', () => { + initCalendar({ + direction: 'ltr', + }) + currentCalendar.destroy() + expect(currentCalendar.el.className).toBe('') + }) + }) + + describe('when calendar is RTL', () => { + it('cleans up all classNames on the root element', () => { + initCalendar({ + direction: 'rtl', + }) + currentCalendar.destroy() + expect(currentCalendar.el.className).toBe('') + }) + }) + + describeOptions('themeSystem', { + 'when bootstrap theme': 'bootstrap', + }, () => { + it('cleans up all classNames on the root element', () => { + initCalendar() + currentCalendar.destroy() + expect(currentCalendar.el.className).toBe('') + }) + }) + + pushOptions({ + initialDate: '2014-12-01', + droppable: true, // likely to attach document handler + editable: true, // same + events: [ + { title: 'event1', start: '2014-12-01' }, + ], + }) + + describeOptions('initialView', { + 'when in dayGridWeek view': 'dayGridWeek', + 'when in week view': 'timeGridWeek', + 'when in listWeek view': 'listWeek', + 'when in month view': 'dayGridMonth', + }, (viewName) => { + it('leaves no handlers attached to DOM', () => { + const standardElListenerCount = prepareStandardListeners() + let $el = $('<div>').appendTo('body') + + let elHandlerCounter = new ListenerCounter($el[0]) + let docHandlerCounter = new ListenerCounter(document) + + elHandlerCounter.startWatching() + docHandlerCounter.startWatching() + + initCalendar({}, $el) + currentCalendar.destroy() + + if (viewName !== 'timeGridDay') { // hack for skipping 3rd one + expect(elHandlerCounter.stopWatching()).toBe(standardElListenerCount) + expect(docHandlerCounter.stopWatching()).toBe(0) + } + + $el.remove() + }) + + // Issue 2432 + it('preserves existing window handlers when handleWindowResize is off', () => { + let resizeHandler = () => {} + let windowListenerCounter = new ListenerCounter(window) + windowListenerCounter.startWatching() + + window.addEventListener('resize', resizeHandler) + expect(windowListenerCounter.computeDelta()).toBe(1) + + initCalendar({ + handleWindowResize: false, + }) + currentCalendar.destroy() + + expect(windowListenerCounter.stopWatching()).toBe(1) + window.removeEventListener('resize', resizeHandler) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/dir.ts b/fullcalendar-main/tests/src/legacy/dir.ts new file mode 100644 index 0000000..7d4c61d --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/dir.ts @@ -0,0 +1,29 @@ +import arLocale from '@fullcalendar/core/locales/ar' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('direction', () => { + it('has it\'s default value computed differently based off of the locale', () => { + initCalendar({ + locale: arLocale, // Arabic is RTL + }) + expect(currentCalendar.getOption('direction')).toEqual('rtl') + }) + + // NOTE: don't put tests related to other options in here! + // Put them in the test file for the individual option! + + it('adapts to dynamic option change', () => { + initCalendar({ + direction: 'ltr', + }) + let $el = $(currentCalendar.el) + + expect($el).toHaveClass(CalendarWrapper.LTR_CLASSNAME) + expect($el).not.toHaveClass(CalendarWrapper.RTL_CLASSNAME) + + currentCalendar.setOption('direction', 'rtl') + + expect($el).toHaveClass(CalendarWrapper.RTL_CLASSNAME) + expect($el).not.toHaveClass(CalendarWrapper.LTR_CLASSNAME) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/displayEventEnd.ts b/fullcalendar-main/tests/src/legacy/displayEventEnd.ts new file mode 100644 index 0000000..76e7405 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/displayEventEnd.ts @@ -0,0 +1,130 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('displayEventEnd', () => { + pushOptions({ + initialDate: '2014-06-13', + timeZone: 'UTC', + eventTimeFormat: { hour: 'numeric', minute: '2-digit' }, + }) + + describeOptions('initialView', { + 'when in month view': 'dayGridMonth', + 'when in week view': 'timeGridWeek', + }, () => { + describe('when off', () => { + pushOptions({ + displayEventEnd: false, + }) + + describe('with an all-day event', () => { + it('displays no time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13', + end: '2014-06-13', + allDay: true, + }], + }) + expectEventTimeText(calendar, '') + }) + }) + + describe('with a timed event with no end time', () => { + it('displays only the start time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13T01:00:00', + allDay: false, + }], + }) + expectEventTimeText(calendar, '1:00 AM') + }) + }) + + describe('with a timed event with an end time', () => { + it('displays only the start time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13T01:00:00', + end: '2014-06-13T02:00:00', + allDay: false, + }], + }) + expectEventTimeText(calendar, '1:00 AM') + }) + }) + }) + + describe('when on', () => { + pushOptions({ + displayEventEnd: true, + }) + + describe('with an all-day event', () => { + it('displays no time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13', + end: '2014-06-13', + allDay: true, + }], + }) + expectEventTimeText(calendar, '') + }) + }) + + describe('with a timed event with no end time', () => { + it('displays only the start time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13T01:00:00', + allDay: false, + }], + }) + expectEventTimeText(calendar, '1:00 AM') + }) + }) + + describe('with a timed event given an invalid end time', () => { + it('displays only the start time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13T01:00:00', + end: '2014-06-13T01:00:00', + allDay: false, + }], + }) + expectEventTimeText(calendar, '1:00 AM') + }) + }) + + describe('with a timed event with an end time', () => { + it('displays both the start and end time text', () => { + let calendar = initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-13T01:00:00', + end: '2014-06-13T02:00:00', + allDay: false, + }], + }) + expectEventTimeText(calendar, '1:00 AM - 2:00 AM') + }) + }) + }) + }) + + function expectEventTimeText(calendar, timeText) { + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.timeText).toBe(timeText) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/emitter.ts b/fullcalendar-main/tests/src/legacy/emitter.ts new file mode 100644 index 0000000..7405fbc --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/emitter.ts @@ -0,0 +1,56 @@ +import { Emitter } from '@fullcalendar/core/internal' + +describe('emitter', () => { + it('calls a handler', () => { + let o = new Emitter() + let handlers = { + something(arg1, arg2) { + expect(arg1).toBe(7) + expect(arg2).toBe(8) + }, + } + spyOn(handlers, 'something').and.callThrough() + + o.on('something', handlers.something) + o.trigger('something', 7, 8) + expect(handlers.something).toHaveBeenCalled() + }) + + it('unbinds with an exact reference', () => { + let o = new Emitter() + let handlers = { + something() {}, + } + spyOn(handlers, 'something') + + o.on('something', handlers.something) + o.trigger('something') + expect(handlers.something).toHaveBeenCalled() + + o.off('something', handlers.something) + o.trigger('something') + expect(handlers.something.calls.count()).toBe(1) + }) + + it('unbinds all when no reference', () => { + let o = new Emitter() + let handlers = { + something1() {}, + something2() {}, + } + spyOn(handlers, 'something1') + spyOn(handlers, 'something2') + + o.on('something', handlers.something1) + o.on('something', handlers.something2) + + o.trigger('something') + expect(handlers.something1).toHaveBeenCalled() + expect(handlers.something2).toHaveBeenCalled() + + o.off('something') + o.trigger('something') + expect(handlers.something1.calls.count()).toBe(1) + expect(handlers.something2.calls.count()).toBe(1) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/event-coloring.ts b/fullcalendar-main/tests/src/legacy/event-coloring.ts new file mode 100644 index 0000000..fbd401e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/event-coloring.ts @@ -0,0 +1,245 @@ +import { EventInput } from '@fullcalendar/core' +import { RED_REGEX } from '../lib/dom-misc.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('event coloring', () => { + pushOptions({ + initialDate: '2014-11-04', + allDaySlot: false, + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + defineViewTests(false) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + defineViewTests(true) + }) + + function defineViewTests(eventHasTime) { + describe('for foreground events', () => { + testTextColor(eventHasTime) + testBorderColor(eventHasTime) + testBackgroundColor(eventHasTime) + }) + + describe('for background events', () => { + testBackgroundColor(eventHasTime, 'background') + }) + } + + function testTextColor(eventHasTime) { + let eventOptions = getEventOptions(eventHasTime) + + it('should accept the global eventTextColor', () => { + initCalendar({ + eventTextColor: 'red', + events: [getTestEvent(eventOptions)], + }) + expect(getEventCss('color')).toMatch(RED_REGEX) + }) + + it('should accept an event source\'s textColor', () => { + initCalendar({ + eventTextColor: 'blue', // even when there's a more general setting + eventSources: [{ + textColor: 'red', + events: [getTestEvent(eventOptions)], + }], + }) + expect(getEventCss('color')).toMatch(RED_REGEX) + }) + + it('should accept an event object\'s textColor', () => { + let eventInput = getTestEvent(eventOptions, { + textColor: 'red', + }) + initCalendar({ + eventTextColor: 'blue', // even when there's a more general setting + events: [eventInput], + }) + expect(getEventCss('color')).toMatch(RED_REGEX) + }) + } + + function testBorderColor(eventHasTime) { + let eventOptions = getEventOptions(eventHasTime) + + it('should accept the global eventColor for border color', () => { + initCalendar({ + eventColor: 'red', + events: [getTestEvent(eventOptions)], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + + it('should accept the global eventBorderColor', () => { + initCalendar({ + eventColor: 'blue', + eventBorderColor: 'red', + events: [getTestEvent(eventOptions)], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + + it('should accept an event source\'s color for the border', () => { + initCalendar({ + eventBorderColor: 'blue', // even when there's a more general setting + eventSources: [{ + color: 'red', + events: [getTestEvent(eventOptions)], + }], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + + it('should accept an event source\'s borderColor', () => { + initCalendar({ + eventBorderColor: 'blue', // even when there's a more general setting + eventSources: [{ + color: 'blue', + borderColor: 'red', + events: [getTestEvent(eventOptions)], + }], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + + it('should accept an event object\'s color for the border', () => { + let eventInput = getTestEvent(eventOptions, { + color: 'red', + }) + initCalendar({ + eventSources: [{ + borderColor: 'blue', // even when there's a more general setting + events: [eventInput], + }], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + + it('should accept an event object\'s borderColor', () => { + let eventInput = getTestEvent(eventOptions, { + color: 'blue', // even when there's a more general setting + borderColor: 'red', + }) + initCalendar({ + eventSources: [{ + events: [eventInput], + }], + }) + expect(getEventCss('border-top-color')).toMatch(RED_REGEX) + }) + } + + function testBackgroundColor(eventHasTime, display?) { + let eventOptions = getEventOptions(eventHasTime) + + if (typeof display !== 'undefined') { + eventOptions.display = display + } + + it('should accept the global eventColor for background color', () => { + initCalendar({ + eventColor: 'red', + events: [getTestEvent(eventOptions)], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + + it('should accept the global eventBackgroundColor', () => { + initCalendar({ + eventColor: 'blue', // even when there's a more general setting + eventBackgroundColor: 'red', + events: [getTestEvent(eventOptions)], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + + it('should accept an event source\'s color for the background', () => { + initCalendar({ + eventBackgroundColor: 'blue', // even when there's a more general setting + eventSources: [{ + color: 'red', + events: [getTestEvent(eventOptions)], + }], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + + it('should accept an event source\'s backgroundColor', () => { + initCalendar({ + eventSources: [{ + color: 'blue', // even when there's a more general setting + backgroundColor: 'red', + events: [getTestEvent(eventOptions)], + }], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + + it('should accept an event object\'s color for the background', () => { + let eventInput = getTestEvent(eventOptions) + eventInput.color = 'red' + initCalendar({ + eventSources: [{ + backgroundColor: 'blue', // even when there's a more general setting + events: [eventInput], + }], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + + it('should accept an event object\'s backgroundColor', () => { + let eventInput = getTestEvent(eventOptions) + eventInput.color = 'blue' // even when there's a more general setting + eventInput.backgroundColor = 'red' + initCalendar({ + eventSources: [{ + events: [eventInput], + }], + }) + expect(getEventCss('background-color', display)).toMatch(RED_REGEX) + }) + } + + function getEventCss(prop, display?) { + let calendarWrapper = new CalendarWrapper(currentCalendar) + let eventEl = display === 'background' + ? calendarWrapper.getBgEventEls()[0] + : calendarWrapper.getEventEls()[0] + + if (prop === 'color') { + return $(eventEl).find('.fc-event-title').css(prop) + } + + return $(eventEl).css(prop) + } + + function getTestEvent(defaultOptions, extraOptions = {}): EventInput { + let event = {} as EventInput + $.extend(event, defaultOptions) + if (extraOptions) { + $.extend(event, extraOptions) + } + return event + } + + function getEventOptions(eventHasTime): EventInput { + let options = { + start: '2014-11-04', + } + if (eventHasTime) { + options.start += 'T01:00:00' + } + return options + } +}) diff --git a/fullcalendar-main/tests/src/legacy/event-dnd.ts b/fullcalendar-main/tests/src/legacy/event-dnd.ts new file mode 100644 index 0000000..5dc0520 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/event-dnd.ts @@ -0,0 +1,458 @@ +import { createDuration } from '@fullcalendar/core/internal' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventDrag2 } from '../lib/wrappers/interaction-util.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { queryEventElInfo } from '../lib/wrappers/TimeGridWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('eventDrop', () => { + pushOptions({ + timeZone: 'UTC', + initialDate: '2014-06-11', + editable: true, + dragScroll: false, + longPressDelay: 100, + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }); + + // TODO: test that event's dragged via touch that don't wait long enough for longPressDelay + // SHOULD NOT drag + + [false, true].forEach((isTouch) => { + describe('with ' + (isTouch ? 'touch' : 'mouse'), () => { + describe('when dragging an all-day event to another day', () => { + it('should be given correct arguments, with whole-day delta', (done) => { + let calendar = initCalendarWithSpies({ + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dragging = dayGridWrapper.dragEventToDate( + dayGridWrapper.getFirstEventEl(), + '2014-06-11', + '2014-06-20', + isTouch, + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: 9 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-20') + expect(arg.event.end).toBeNull() + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + + done() + }) + }) + }) + }) + }) + + describe('when gragging a timed event to another day', () => { + it('should be given correct arguments, with whole-day delta', (done) => { + let calendar = initCalendarWithSpies({ + events: [{ + title: 'timed event', + start: '2014-06-11T06:00:00', + allDay: false, + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dragging = dayGridWrapper.dragEventToDate( + dayGridWrapper.getFirstEventEl(), + '2014-06-11', + '2014-06-16', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: 5 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-16T06:00:00Z') + expect(arg.event.end).toBeNull() + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T06:00:00Z') + expect(event.end).toBeNull() + + done() + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4458 + describe('when dragging an event back in time when duration not editable', () => { + it('should work', (done) => { + let calendar = initCalendarWithSpies({ + initialDate: '2019-01-16', + eventDurationEditable: false, + events: [{ + title: 'event', + start: '2019-01-16T10:30:00+00:00', + end: '2019-01-16T12:30:00+00:00', + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let dragging = dayGridWrapper.dragEventToDate( + dayGridWrapper.getFirstEventEl(), + '2019-01-16', + '2019-01-14', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + expect(arg.delta).toEqual(createDuration({ day: -2 })) + expect(arg.event.start).toEqualDate('2019-01-14T10:30:00+00:00') + expect(arg.event.end).toEqualDate('2019-01-14T12:30:00+00:00') + done() + }) + }) + }) + + // TODO: tests for eventMouseEnter/eventMouseLeave firing correctly when no dragging + it('should not fire any eventMouseEnter/eventMouseLeave events while dragging', (done) => { // issue 1297 + let eventMouseEnterSpy = spyOnCalendarCallback('eventMouseEnter') + let eventMouseLeaveSpy = spyOnCalendarCallback('eventMouseLeave') + let calendar = initCalendar({ + events: [ + { + title: 'all-day event', + start: '2014-06-11', + allDay: true, + className: 'event1', + }, + { + title: 'event2', + start: '2014-06-10', + allDay: true, + className: 'event2', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $('.event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-06-20'), + moves: 10, + duration: 1000, + onRelease() { + done() + }, + }) + + setTimeout(() => { // wait until half way through drag + $('.event2') + .simulate('mouseover') + .simulate('mouseenter') + .simulate('mouseout') + .simulate('mouseleave') + + setTimeout(() => { + expect(eventMouseEnterSpy).not.toHaveBeenCalled() + expect(eventMouseLeaveSpy).not.toHaveBeenCalled() + }, 0) + }, 500) + }) + }) + + describe('when in timeGrid view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }); + + [false, true].forEach((isTouch) => { + describe('with ' + (isTouch ? 'touch' : 'mouse'), () => { + describe('when dragging a timed event to another time on a different day', () => { + it('should be given correct arguments and delta with days/time', (done) => { + let calendar = initCalendarWithSpies({ + events: [{ + title: 'timed event', + start: '2014-06-11T06:00:00', + allDay: false, + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let dragging = timeGridWrapper.dragEventToDate( + timeGridWrapper.getFirstEventEl(), + '2014-06-12T07:30:00', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: 1, hour: 1, minute: 30 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-12T07:30:00Z') + expect(arg.event.end).toBeNull() + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T06:00:00Z') + expect(event.end).toBeNull() + + done() + }) + }) + }) + }) + }) + + describe('when dragging an all-day event to another all-day', () => { + it('should be given correct arguments, with whole-day delta', (done) => { + let calendar = initCalendarWithSpies({ + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + let dragging = dayGridWrapper.dragEventToDate( + dayGridWrapper.getFirstEventEl(), + '2014-06-11', + '2014-06-13', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: 2 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-13') + expect(arg.event.end).toBeNull() + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + + done() + }) + }) + }) + + describe('when dragging an all-day event to a time slot on a different day', () => { + it('should be given correct arguments and delta with days/time', (done) => { + let calendar = initCalendarWithSpies({ + scrollTime: '01:00:00', + height: 400, // short enough to make scrolling happen + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let dragging = viewWrapper.timeGrid.dragEventToDate( + viewWrapper.dayGrid.getFirstEventEl(), + '2014-06-10T01:00:00', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: -1, hour: 1 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-10T01:00:00Z') + expect(arg.event.end).toBeNull() + expect(arg.event.allDay).toBe(false) + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + expect(event.allDay).toBe(true) + + done() + }) + }) + }) + + describe('when dragging a timed event to an all-day slot on a different day', () => { + it('should be given correct arguments, with whole-day delta', (done) => { + let calendar = initCalendarWithSpies({ + scrollTime: '01:00:00', + height: 400, // short enough to make scrolling happen + events: [{ + title: 'timed event', + start: '2014-06-11T01:00:00', + allDay: false, + }], + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let dragging = viewWrapper.dayGrid.dragEventToDate( + viewWrapper.timeGrid.getFirstEventEl(), + null, + '2014-06-10', + ) + + waitEventDrag2(calendar, dragging).then((arg) => { + let delta = createDuration({ day: -1 }) + expect(arg.delta).toEqual(delta) + + expect(arg.event.start).toEqualDate('2014-06-10') + expect(arg.event.end).toBeNull() + expect(arg.event.allDay).toBe(true) + + arg.revert() + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T01:00:00Z') + expect(event.end).toBeNull() + expect(event.allDay).toBe(false) + + done() + }) + }) + }) + + describe('when dragging a timed event with no end time', () => { + it('should continue to only show the updated start time', (done) => { + let dragged = false + let calendar = initCalendarWithSpies({ + scrollTime: '01:00:00', + height: 400, // short enough to make scrolling happen + events: [{ + title: 'timed event', + start: '2014-06-11T01:00:00', + allDay: false, + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + let dragging = timeGridWrapper.dragEventToDate( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T02:30:00', + () => { // onBeforeRelease + dragged = true + let mirrorEls = timeGridWrapper.getMirrorEls() + expect(mirrorEls.length).toBe(1) + expect(queryEventElInfo(mirrorEls[0]).timeText).toBe('2:30') + }, + ) + + waitEventDrag2(calendar, dragging).then(() => { + expect(dragged).toBe(true) + done() + }) + }) + }) + + describe('when dragging a timed event with an end time', () => { + it('should continue to show the updated start and end time', (done) => { + let dragged = false + let calendar = initCalendarWithSpies({ + scrollTime: '01:00:00', + height: 400, // short enough to make scrolling happen + events: [{ + title: 'timed event', + start: '2014-06-11T01:00:00', + end: '2014-06-11T02:00:00', + allDay: false, + }], + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + let dragging = timeGridWrapper.dragEventToDate( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T02:30:00', + () => { // onBeforeRelease + dragged = true + let mirrorEls = timeGridWrapper.getMirrorEls() + expect(mirrorEls.length).toBe(1) + expect(queryEventElInfo(mirrorEls[0]).timeText).toBe('2:30 - 3:30') + }, + ) + + waitEventDrag2(calendar, dragging).then(() => { + expect(dragged).toBe(true) + done() + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4503 + describe('when dragging to one of the last slots', () => { + it('should work', (done) => { + let calendar = initCalendarWithSpies({ + scrollTime: '23:00:00', + height: 400, // short enough to make scrolling happen + events: [{ + title: 'timed event', + start: '2014-06-11T18:00:00', // should be in view without scrolling + allDay: false, + }], + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let dragging = timeGridWrapper.dragEventToDate( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T23:30:00', + ) + + waitEventDrag2(calendar, dragging).then(() => { + let event = currentCalendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T23:30:00Z') + expect(event.end).toBeNull() + expect(event.allDay).toBe(false) + + done() + }) + }) + }) + }) + + // Initialize a calendar, run a drag, and do type-checking of all arguments for all handlers. + // TODO: more discrimination instead of just checking for 'object' + function initCalendarWithSpies(options) { + options.eventDragStart = (arg) => { + expect(arg.el instanceof Element).toBe(true) + expect(arg.el).toHaveClass(CalendarWrapper.EVENT_CLASSNAME) + expect(typeof arg.event).toBe('object') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + } + + options.eventDragStop = (arg) => { + expect(options.eventDragStart).toHaveBeenCalled() + expect(arg.el instanceof Element).toBe(true) + expect(arg.el).toHaveClass(CalendarWrapper.EVENT_CLASSNAME) + expect(typeof arg.event).toBe('object') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + } + + options.eventDrop = (arg) => { + expect(options.eventDragStop).toHaveBeenCalled() + expect(arg.el instanceof Element).toBe(true) + expect(arg.el).toHaveClass(CalendarWrapper.EVENT_CLASSNAME) + expect(typeof arg.delta).toBe('object') + expect(typeof arg.revert).toBe('function') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + } + + spyOn(options, 'eventDragStart').and.callThrough() + spyOn(options, 'eventDragStop').and.callThrough() + + return initCalendar(options) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/event-feed-param.ts b/fullcalendar-main/tests/src/legacy/event-feed-param.ts new file mode 100644 index 0000000..8cd3c66 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/event-feed-param.ts @@ -0,0 +1,57 @@ +import fetchMock from 'fetch-mock' + +describe('event feed params', () => { + pushOptions({ + initialDate: '2014-05-01', + initialView: 'dayGridMonth', + }) + + afterEach(() => { + fetchMock.restore() + }) + + it('utilizes custom startParam, endParam, and timeZoneParam names', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: givenUrl, + timeZone: 'America/Los_Angeles', + startParam: 'mystart', + endParam: 'myend', + timeZoneParam: 'currtz', + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('mystart')).toBe('2014-04-27T00:00:00') + expect(requestParams.get('myend')).toBe('2014-06-08T00:00:00') + expect(requestParams.get('currtz')).toBe('America/Los_Angeles') + }) + + it('utilizes event-source-specific startParam, endParam, and timeZoneParam names', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + timeZone: 'America/Los_Angeles', + startParam: 'mystart', + endParam: 'myend', + timeZoneParam: 'currtz', + eventSources: [ + { + url: givenUrl, + startParam: 'feedstart', + endParam: 'feedend', + timeZoneParam: 'feedctz', + }, + ], + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('feedstart')).toBe('2014-04-27T00:00:00') + expect(requestParams.get('feedend')).toBe('2014-06-08T00:00:00') + expect(requestParams.get('feedctz')).toBe('America/Los_Angeles') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/event-obj.ts b/fullcalendar-main/tests/src/legacy/event-obj.ts new file mode 100644 index 0000000..34e45b4 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/event-obj.ts @@ -0,0 +1,145 @@ +describe('event object creation', () => { + /* + + NOTE: Where possible, if there is a specific option that affects event object creation + behavior, write your tests in the individual file for that option, instead of here. + Examples of this: + defaultAllDay (tests allDay guessing behavior too) + eventDataTransform + forceEventDuration + + */ + + function init(singleEventData) { + initCalendar({ + events: [singleEventData], + }) + return currentCalendar.getEvents()[0] + } + + it('accepts `date` property as alias for `start`', () => { + let event = init({ + date: '2014-05-05', + }) + expect(event.start instanceof Date).toEqual(true) + expect(event.start).toEqualDate('2014-05-05') + }) + + it('doesn\'t produce an event when an invalid start Date object', () => { + let event = init({ + start: new Date('asdf'), + }) + expect(event).toBeUndefined() + }) + + it('doesn\'t produce an event when an invalid start string', () => { + let event = init({ + start: 'asdfasdfasdf', + }) + expect(event).toBeUndefined() + }) + + it('produces null end when given an invalid Date object', () => { + let event = init({ + start: '2014-05-01', + end: new Date('asdf'), + }) + expect(event.start).toEqualDate('2014-05-01') + expect(event.end).toBe(null) + }) + + it('produces null end when given an invalid string', () => { + let event = init({ + start: '2014-05-01', + end: 'asdfasdfasdf', + }) + expect(event.start).toEqualDate('2014-05-01') + expect(event.end).toBe(null) + }) + + it('produces null end when given a timed end before the start', () => { + let event = init({ + start: '2014-05-02T00:00:00', + end: '2014-05-01T23:00:00', + }) + expect(event.start).toEqualDate('2014-05-02T00:00:00Z') + expect(event.end).toBe(null) + }) + + it('produces null end when given a timed end equal to the start', () => { + let event = init({ + start: '2014-05-02T00:00:00', + end: '2014-05-01T00:00:00', + }) + expect(event.start).toEqualDate('2014-05-02T00:00:00Z') + expect(event.end).toBe(null) + }) + + it('produces null end when given an all-day end before the start', () => { + let event = init({ + start: '2014-05-02', + end: '2014-05-02', + }) + expect(event.start).toEqualDate('2014-05-02') + expect(event.end).toBe(null) + }) + + it('produces null end when given an all-day end equal to the start', () => { + let event = init({ + start: '2014-05-02T00:00:00', + end: '2014-05-02T00:00:00', + }) + expect(event.start).toEqualDate('2014-05-02T00:00:00Z') + expect(event.end).toBe(null) + }) + + it('strips times of dates when event is all-day', () => { + let event = init({ + start: '2014-05-01T01:00:00-12:00', + end: '2014-05-02T01:00:00-12:00', + allDay: true, + }) + expect(event.allDay).toEqual(true) + expect(event.start).toEqualDate('2014-05-01') + expect(event.end).toEqualDate('2014-05-02') + }) + + it('gives 00:00 times to ambiguously-timed dates when event is timed', () => { + let event = init({ + start: '2014-05-01', + end: '2014-05-03', + allDay: false, + }) + expect(event.allDay).toEqual(false) + expect(event.start).toEqualDate('2014-05-01T00:00:00Z') + expect(event.end).toEqualDate('2014-05-03T00:00:00Z') + }) + + it('accepts an array `className`', () => { + let event = init({ + start: '2014-05-01', + className: ['class1', 'class2'], + }) + expect($.isArray(event.classNames)).toEqual(true) + expect(event.classNames).toEqual(['class1', 'class2']) + }) + + it('accepts a string `className`', () => { + let event = init({ + start: '2014-05-01', + className: 'class1 class2', + }) + expect($.isArray(event.classNames)).toEqual(true) + expect(event.classNames).toEqual(['class1', 'class2']) + }) + + it('accepts extended properties', () => { + let event = init({ + start: '2014-05-01', + prop1: 'prop1val', + prop2: ['a', 'b'], + }) + expect(event.extendedProps.prop1).toEqual('prop1val') + expect(event.extendedProps.prop2).toEqual(['a', 'b']) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/event-resize.ts b/fullcalendar-main/tests/src/legacy/event-resize.ts new file mode 100644 index 0000000..3bf7c10 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/event-resize.ts @@ -0,0 +1,442 @@ +import { createDuration } from '@fullcalendar/core/internal' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { waitEventResize2 } from '../lib/wrappers/interaction-util.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('eventResize', () => { + pushOptions({ + initialDate: '2014-06-11', + editable: true, + longPressDelay: 100, + scrollTime: 0, + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + describe('when resizing an all-day event with mouse', () => { + it('should have correct arguments with a whole-day delta', (done) => { + let calendar = initCalendar({ + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + checkCalendarTriggers(calendar) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let resizing = dayGridWrapper.resizeEvent( + dayGridWrapper.getFirstEventEl(), '2014-06-11', '2014-06-16', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ day: 5 })) + + expect(arg.event.start).toEqualDate('2014-06-11') + expect(arg.event.end).toEqualDate('2014-06-17') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + + done() + }) + }) + }) + + describe('when resizing an all-day event via touch', () => { + // for https://github.com/fullcalendar/fullcalendar/issues/3118 + [true, false].forEach((eventStartEditable) => { + describe('when eventStartEditable is ' + eventStartEditable, () => { + pushOptions({ eventStartEditable }) + + it('should have correct arguments with a whole-day delta', (done) => { + let calendar = initCalendar({ + dragRevertDuration: 0, // so that eventDragStop happens immediately after touchend + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let resizing = dayGridWrapper.resizeEventTouch( + dayGridWrapper.getFirstEventEl(), '2014-06-11', '2014-06-16', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ day: 5 })) + + expect(arg.event.start).toEqualDate('2014-06-11') + expect(arg.event.end).toEqualDate('2014-06-17') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + + done() + }) + }) + }) + }) + }) + + describe('when rendering a timed event', () => { + it('should not have resize capabilities', () => { + initCalendar({ + events: [{ + title: 'timed event', + start: '2014-06-11T08:00:00', + allDay: false, + }], + }) + expect( + $(`.${CalendarWrapper.EVENT_CLASSNAME} .${CalendarWrapper.EVENT_RESIZER_CLASSNAME}`), + ).not.toBeInDOM() + }) + }) + }) + + describe('when in timeGrid view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('when resizing an all-day event', () => { + it('should have correct arguments with a whole-day delta', (done) => { + let calendar = initCalendar({ + events: [{ + title: 'all-day event', + start: '2014-06-11', + allDay: true, + }], + }) + + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + let resizing = dayGridWrapper.resizeEvent( + dayGridWrapper.getFirstEventEl(), '2014-06-11', '2014-06-13', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ day: 2 })) + + expect(arg.event.start).toEqualDate('2014-06-11') + expect(arg.event.end).toEqualDate('2014-06-14') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11') + expect(event.end).toBeNull() + + done() + }) + }) + }) + + describe('when resizing a timed event with an end', () => { + pushOptions({ + events: [{ + title: 'timed event event', + start: '2014-06-11T05:00:00', + end: '2014-06-11T07:00:00', + allDay: false, + }], + }) + + it('should have correct arguments with a timed delta', (done) => { + let calendar = initCalendar() + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), '2014-06-11T07:00:00', '2014-06-11T09:30:00', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ hour: 2, minute: 30 })) + + expect(arg.event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(arg.event.end).toEqualDate('2014-06-11T09:30:00Z') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(event.end).toEqualDate('2014-06-11T07:00:00Z') + + done() + }) + }) + + it('should have correct arguments with a timed delta via touch', (done) => { + let calendar = initCalendar({ + dragRevertDuration: 0, // so that eventDragStop happens immediately after touchend + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEventTouch( + timeGridWrapper.getFirstEventEl(), '2014-06-11T07:00:00Z', '2014-06-11T09:30:00Z', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ hour: 2, minute: 30 })) + + expect(arg.event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(arg.event.end).toEqualDate('2014-06-11T09:30:00Z') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(event.end).toEqualDate('2014-06-11T07:00:00Z') + + done() + }) + }) + + // TODO: test RTL + it('should have correct arguments with a timed delta when resized to a different day', (done) => { + let calendar = initCalendar() + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEventTouch( + timeGridWrapper.getFirstEventEl(), '2014-06-11T07:00:00Z', '2014-06-12T09:30:00Z', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ day: 1, hour: 2, minute: 30 })) + + expect(arg.event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(arg.event.end).toEqualDate('2014-06-12T09:30:00Z') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T05:00:00Z') + expect(event.end).toEqualDate('2014-06-11T07:00:00Z') + + done() + }) + }) + + it('should have correct arguments with a timed delta, when timezone is local', (done) => { + let calendar = initCalendar({ + timeZone: 'local', + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEventTouch( + timeGridWrapper.getFirstEventEl(), '2014-06-11T07:00:00', '2014-06-11T09:30:00', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ hour: 2, minute: 30 })) + + expect(arg.event.start).toEqualLocalDate('2014-06-11T05:00:00') + expect(arg.event.end).toEqualLocalDate('2014-06-11T09:30:00') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualLocalDate('2014-06-11T05:00:00') + expect(event.end).toEqualLocalDate('2014-06-11T07:00:00') + + done() + }) + }) + + it('should have correct arguments with a timed delta, when timezone is UTC', (done) => { + let calendar = initCalendar({ + timeZone: 'UTC', + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let resizing = timeGridWrapper.resizeEventTouch( + timeGridWrapper.getFirstEventEl(), '2014-06-11T07:00:00', '2014-06-11T09:30:00', + ) + + waitEventResize2(calendar, resizing).then((arg) => { + expect(arg.endDelta).toEqual(createDuration({ hour: 2, minute: 30 })) + + expect(arg.event.start).toEqualDate('2014-06-11T05:00:00+00:00') + expect(arg.event.end).toEqualDate('2014-06-11T09:30:00+00:00') + + arg.revert() + let event = calendar.getEvents()[0] + + expect(event.start).toEqualDate('2014-06-11T05:00:00') + expect(event.end).toEqualDate('2014-06-11T07:00:00+00:00') + + done() + }) + }) + + it('should display the correct time text while resizing', (done) => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let onBeforeReleaseCalled = false // don't trust ourselves :( + + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T07:00:00Z', + '2014-06-11T09:30:00Z', + () => { // onBeforeRelease + let $mirrorEls = $(timeGridWrapper.getMirrorEls()) + expect($mirrorEls.length).toBe(1) + expect($mirrorEls.find('.' + CalendarWrapper.EVENT_TIME_CLASSNAME)).toHaveText('5:00 - 9:30') + onBeforeReleaseCalled = true + }, + ).then(() => { + expect(onBeforeReleaseCalled).toBe(true) + done() + }) + }) + + it('should run the temporarily rendered event through eventDidMount', (done) => { + let calendar = initCalendar({ + eventDidMount(arg) { + $(arg.el).addClass('eventDidRender') + }, + }) + + let onBeforeReleaseCalled = false // don't trust ourselves :( + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T07:00:00Z', + '2014-06-11T09:30:00Z', + () => { // onBeforeRelease + let $mirrorEls = $(timeGridWrapper.getMirrorEls()) + expect($mirrorEls.length).toBe(1) + expect($mirrorEls).toHaveClass('eventDidRender') + onBeforeReleaseCalled = true + }, + ).then(() => { + expect(onBeforeReleaseCalled).toBe(true) + done() + }) + }) + + it('should not fire the windowResize handler', (done) => { // bug 1116 + // has to do this crap because PhantomJS was trigger false window resizes unrelated to the event resize + let isDragging = false + let calledWhileDragging = false + + let calendar = initCalendar({ + windowResizeDelay: 0, + windowResize(ev) { + if (isDragging) { + calledWhileDragging = true + } + }, + }) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T07:00:00Z', + '2014-06-11T09:30:00Z', + () => { // onBeforeRelease + isDragging = false + }, + ).then(() => { + expect(calledWhileDragging).toBe(false) + done() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7099 + it('should handle two consecutive resizes', (done) => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T07:00:00Z', + '2014-06-11T12:00:00Z', + ).then(() => { + let event = calendar.getEvents()[0] + expect(event.end).toEqualDate('2014-06-11T12:00:00Z') + + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T12:00:00Z', + '2014-06-11T09:00:00Z', + ).then(() => { + event = calendar.getEvents()[0] + expect(event.end).toEqualDate('2014-06-11T09:00:00Z') + done() + }) + }) + }) + }) + + describe('when resizing a timed event without an end', () => { + pushOptions({ + defaultTimedEventDuration: '02:00', + events: [{ + title: 'timed event event', + start: '2014-06-11T05:00:00', + allDay: false, + }], + }) + + // copied and pasted from other test :( + it('should display the correct time text while resizing', (done) => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let onBeforeReleaseCalled = false // don't trust ourselves :( + + timeGridWrapper.resizeEvent( + timeGridWrapper.getFirstEventEl(), + '2014-06-11T07:00:00Z', + '2014-06-11T09:30:00Z', + () => { // onBeforeRelease + let $mirrorEls = $(timeGridWrapper.getMirrorEls()) + expect($mirrorEls.length).toBe(1) + expect($mirrorEls.find('.' + CalendarWrapper.EVENT_TIME_CLASSNAME)).toHaveText('5:00 - 9:30') + onBeforeReleaseCalled = true + }, + ).then(() => { + expect(onBeforeReleaseCalled).toBe(true) + done() + }) + }) + }) + }) + + function checkCalendarTriggers(calendar) { + calendar.on('eventResizeStart', (arg) => { + expect(arg.el instanceof Element).toBe(true) + expect(typeof arg.event).toBe('object') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + }) + + calendar.on('eventResizeStop', (arg) => { + expect(arg.el instanceof Element).toBe(true) + expect(typeof arg.event).toBe('object') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + }) + + calendar.on('eventResize', (arg) => { + expect(arg.el instanceof Element).toBe(true) + expect(typeof arg.event).toBe('object') + expect(typeof arg.startDelta).toBe('object') + expect(typeof arg.endDelta).toBe('object') + expect(typeof arg.revert).toBe('function') + expect(typeof arg.jsEvent).toBe('object') + expect(typeof arg.view).toBe('object') + }) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/eventAllow.ts b/fullcalendar-main/tests/src/legacy/eventAllow.ts new file mode 100644 index 0000000..7ad6dba --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/eventAllow.ts @@ -0,0 +1,71 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { waitEventDrag } from '../lib/wrappers/interaction-util.js' + +describe('eventAllow', () => { + pushOptions({ + now: '2016-09-04', + initialView: 'timeGridWeek', + scrollTime: '00:00', + editable: true, + events: [ + { + title: 'event 1', + start: '2016-09-04T01:00', + }, + ], + }) + + it('disallows dragging when returning false', (done) => { // and given correct params + let options = { + eventAllow(dropInfo, event) { + expect(typeof dropInfo).toBe('object') + expect(dropInfo.start instanceof Date).toBe(true) + expect(dropInfo.end instanceof Date).toBe(true) + expect(typeof event).toBe('object') + expect(event.title).toBe('event 1') + return false + }, + } + spyOn(options, 'eventAllow').and.callThrough() + + let calendar = initCalendar(options) + let calendarWrapper = new CalendarWrapper(calendar) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + let dragging = timeGridWrapper.dragEventToDate( + calendarWrapper.getFirstEventEl(), + '2016-09-04T03:00:00', + ) + + waitEventDrag(calendar, dragging).then((modifiedEvent) => { + expect(modifiedEvent).toBeFalsy() // drop failure? + expect(options.eventAllow).toHaveBeenCalled() + done() + }) + }) + + it('allows dragging when returning true', (done) => { + let options = { + eventAllow() { + return true + }, + } + spyOn(options, 'eventAllow').and.callThrough() + + let calendar = initCalendar(options) + let calendarWrapper = new CalendarWrapper(calendar) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + let dragging = timeGridWrapper.dragEventToDate( + calendarWrapper.getFirstEventEl(), + '2016-09-04T03:00:00Z', + ) + + waitEventDrag(calendar, dragging).then((modifiedEvent) => { + expect(modifiedEvent.start).toEqualDate('2016-09-04T03:00:00Z') + expect(options.eventAllow).toHaveBeenCalled() + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/eventDestroy.ts b/fullcalendar-main/tests/src/legacy/eventDestroy.ts new file mode 100644 index 0000000..5201d0e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/eventDestroy.ts @@ -0,0 +1,57 @@ +describe('eventWillUnmount', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-08-01', + }) + + function testSingleEvent(singleEventData, done) { + let callCnt = 0 + + expect(singleEventData.id).toBeTruthy() + + let calendar = initCalendar({ + events: [singleEventData], + eventWillUnmount(arg) { + callCnt += 1 + if (callCnt === 1) { // only care about the first call. gets called again when calendar is destroyed + expect(arg.event.id).toBe(singleEventData.id) + done() + } + }, + }) + + calendar.getEventById(singleEventData.id).remove() + } + + describe('when in month view', () => { // for issue 2017 + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('gets called with removeEvents method', (done) => { + setTimeout(() => { // needs this or else doesn't work when run all tests together + testSingleEvent({ + id: '1', + title: 'event1', + date: '2014-08-02', + }, done) + }, 0) + }) + }) + + describe('when in week view', () => { // for issue 2017 + pushOptions({ + initialView: 'timeGridWeek', + scrollTime: '00:00:00', + }) + + it('gets called with removeEvents method', (done) => { + setTimeout(() => { // needs this or else doesn't work when run all tests together + testSingleEvent({ + id: '1', + title: 'event1', + date: '2014-08-02T02:00:00', + }, done) + }, 0) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/eventMouseEnter.ts b/fullcalendar-main/tests/src/legacy/eventMouseEnter.ts new file mode 100644 index 0000000..75e78fc --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/eventMouseEnter.ts @@ -0,0 +1,67 @@ +describe('eventMouseEnter', () => { + pushOptions({ + initialDate: '2014-08-01', + scrollTime: '00:00:00', + }); + + ['dayGridMonth', 'timeGridWeek'].forEach((viewName) => { + describe('for ' + viewName + ' view', () => { + pushOptions({ + initialView: viewName, + }) + + it('doesn\'t trigger a eventMouseLeave when updating an event', (done) => { + let options = { + events: [{ + title: 'event', + start: '2014-08-02T01:00:00', + className: 'event', + }], + eventMouseEnter(arg) { + expect(typeof arg.event).toBe('object') + expect(typeof arg.jsEvent).toBe('object') + arg.event.setProp('title', 'YO') + }, + eventMouseLeave(arg) {}, + } + + spyOn(options, 'eventMouseEnter') + spyOn(options, 'eventMouseLeave') + + initCalendar(options) + $('.event').simulate('mouseover') + + setTimeout(() => { + expect(options.eventMouseEnter).toHaveBeenCalled() + expect(options.eventMouseLeave).not.toHaveBeenCalled() + done() + }, 100) + }) + }) + }) + + it('gets fired for background events', (done) => { + let mouseoverCalled = false + + initCalendar({ + events: [{ + start: '2014-08-02', + display: 'background', + className: 'event', + }], + eventMouseEnter(arg) { + expect(arg.event.display).toBe('background') + mouseoverCalled = true + }, + eventMouseLeave() { + expect(mouseoverCalled).toBe(true) + done() + }, + }) + + $('.event') + .simulate('mouseover') + .simulate('mouseout') + .simulate('mouseleave') // helps out listenBySelector + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/eventRender.ts b/fullcalendar-main/tests/src/legacy/eventRender.ts new file mode 100644 index 0000000..c16ccc1 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/eventRender.ts @@ -0,0 +1,109 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('eventDidMount+eventContent', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-11-12', + scrollTime: '00:00:00', + events: [{ + title: 'my event', + start: '2014-11-12T09:00:00', + }], + }) + + describeOptions('initialView', { + 'when in day-grid': 'dayGridMonth', + 'when in time-grid': 'timeGridWeek', + }, () => { + describe('with foreground event', () => { + it('receives correct args AND can modify the element', () => { + let options = { + eventContent(arg) { + expect(typeof arg.event).toBe('object') + expect(arg.event.display).toBe('auto') + expect(arg.event.start).toBeDefined() + expect(typeof arg.view).toBe('object') + expect(arg.isMirror).toBe(false) + }, + eventDidMount(arg) { + $(arg.el).css('font-size', '20px') + }, + } + spyOn(options, 'eventContent').and.callThrough() + spyOn(options, 'eventDidMount').and.callThrough() + + let calendar = initCalendar(options) + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + + expect(options.eventContent).toHaveBeenCalled() + expect(options.eventDidMount).toHaveBeenCalled() + expect($(eventEl).css('font-size')).toBe('20px') + }) + }) + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + events: [{ + title: 'my event', + start: '2014-11-12', + }], + }) + + describe('with a foreground event', () => { + it('can return a new element', () => { + let options = { + eventContent() { + let domNodes = $('<div class="sup" style="background-color:green">sup g</div>').get() + return { domNodes } + }, + } + spyOn(options, 'eventContent').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEl = dayGridWrapper.getFirstEventEl() + + expect(eventEl.querySelector('.sup')).toBeTruthy() + expect(options.eventContent).toHaveBeenCalled() + }) + }) + + describe('with an all-day background event', () => { + pushOptions({ + events: [{ + title: 'my event', + start: '2014-11-12', + display: 'background', + }], + }) + + it('receives correct args AND can modify the element', () => { + let options = { + eventContent(arg) { + expect(typeof arg.event).toBe('object') + expect(arg.event.display).toBe('background') + expect(arg.event.start).toBeDefined() + expect(typeof arg.view).toBe('object') + }, + eventDidMount(arg) { + $(arg.el).css('font-size', '20px') + }, + } + spyOn(options, 'eventContent').and.callThrough() + spyOn(options, 'eventDidMount').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let bgEventEls = dayGridWrapper.getBgEventEls() + + expect(bgEventEls.length).toBe(1) + expect(options.eventContent).toHaveBeenCalled() + expect(options.eventDidMount).toHaveBeenCalled() + expect($(bgEventEls).css('font-size')).toBe('20px') + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/eventTimeFormat.ts b/fullcalendar-main/tests/src/legacy/eventTimeFormat.ts new file mode 100644 index 0000000..40b27dd --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/eventTimeFormat.ts @@ -0,0 +1,98 @@ +import enGbLocale from '@fullcalendar/core/locales/en-gb' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('eventTimeFormat', () => { + pushOptions({ + initialDate: '2014-06-04', + events: [{ + title: 'my event', + start: '2014-06-04T15:00:00', + end: '2014-06-04T17:00:00', + }], + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('renders correctly when default', () => { + let calendar = initCalendar() + expectEventTimeText(calendar, '3p') + }) + + it('renders correctly when default and the locale is customized', () => { + let calendar = initCalendar({ + locale: enGbLocale, + }) + expectEventTimeText(calendar, '15') + }) + + it('renders correctly when customized', () => { + let calendar = initCalendar({ + eventTimeFormat: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }, + }) + expectEventTimeText(calendar, '15:00:00') + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('renders correctly when default', () => { + let calendar = initCalendar() + expectEventTimeText(calendar, '3:00 - 5:00') + }) + + it('renders correctly when default and the locale is customized', () => { + let calendar = initCalendar({ + locale: enGbLocale, + }) + expectEventTimeText(calendar, '15:00 - 17:00') + }) + + it('renders correctly when customized', () => { + let calendar = initCalendar({ + eventTimeFormat: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }, + }) + expectEventTimeText(calendar, '15:00:00 - 17:00:00') + }) + }) + + describe('when in multi-day custom dayGrid view', () => { + pushOptions({ + views: { + dayGridTwoDay: { + type: 'dayGrid', + duration: { days: 2 }, + }, + }, + initialView: 'dayGridTwoDay', + }) + + it('defaults to no end time', () => { + let calendar = initCalendar() + expectEventTimeText(calendar, '3p') + }) + }) + + describe('when in dayGridDay view', () => { + pushOptions({ + initialView: 'dayGridDay', + }) + + it('defaults to showing the end time', () => { + let calendar = initCalendar() + expectEventTimeText(calendar, '3p - 5p') + }) + }) + + function expectEventTimeText(calendar, expected) { + let calendarWrapper = new CalendarWrapper(calendar) + let firstEventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(firstEventEl) + expect(eventInfo.timeText).toBe(expected) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/events-array.ts b/fullcalendar-main/tests/src/legacy/events-array.ts new file mode 100644 index 0000000..7636106 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/events-array.ts @@ -0,0 +1,55 @@ +describe('events as an array', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-05-01', + }) + + function getEventArray() { + return [ + { + title: 'my event', + start: '2014-05-21', + }, + ] + } + + it('accepts an event using dayGrid form', (done) => { + initCalendar({ + events: getEventArray(), + eventDidMount(arg) { + expect(arg.event.title).toEqual('my event') + done() + }, + }) + }) + + it('accepts an event using extended form', (done) => { + initCalendar({ + eventSources: [ + { + classNames: 'customeventclass', + events: getEventArray(), + }, + ], + eventDidMount(arg) { + expect(arg.event.title).toEqual('my event') + expect(arg.el).toHaveClass('customeventclass') + done() + }, + }) + }) + + it('doesn\'t mutate the original array', (done) => { + let eventArray = getEventArray() + let origArray = eventArray + let origEvent = eventArray[0] + initCalendar({ + events: eventArray, + eventDidMount() { + expect(origArray).toEqual(eventArray) + expect(origEvent).toEqual(eventArray[0]) + done() + }, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/events-function.ts b/fullcalendar-main/tests/src/legacy/events-function.ts new file mode 100644 index 0000000..5e31794 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/events-function.ts @@ -0,0 +1,134 @@ +describe('events as a function', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-05-01', + }) + + function testEventFunctionParams(arg, callback) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof callback).toEqual('function') + } + + it('requests correctly when local timezone', (done) => { + initCalendar({ + timeZone: 'local', + events(arg, callback) { + testEventFunctionParams(arg, callback) + expect(arg.timeZone).toEqual('local') + expect(arg.start).toEqualLocalDate('2014-04-27T00:00:00') + expect(arg.startStr).toMatch(/^2014-04-27T00:00:00[-+]/) + expect(arg.end).toEqualLocalDate('2014-06-08T00:00:00') + expect(arg.endStr).toMatch(/^2014-06-08T00:00:00[-+]/) + callback([]) + setTimeout(done) // :( + }, + }) + }) + + it('requests correctly when UTC timezone', (done) => { + initCalendar({ + timeZone: 'UTC', + events(arg, callback) { + testEventFunctionParams(arg, callback) + expect(arg.timeZone).toEqual('UTC') + expect(arg.start).toEqualDate('2014-04-27T00:00:00Z') + expect(arg.startStr).toEqual('2014-04-27T00:00:00Z') + expect(arg.end).toEqualDate('2014-06-08T00:00:00Z') + expect(arg.endStr).toEqual('2014-06-08T00:00:00Z') + callback([]) + setTimeout(done) // :( + }, + }) + }) + + it('requests correctly when custom timezone', (done) => { + initCalendar({ + timeZone: 'America/Chicago', + events(arg, callback) { + testEventFunctionParams(arg, callback) + expect(arg.timeZone).toEqual('America/Chicago') + expect(arg.start).toEqualDate('2014-04-27T00:00:00Z') + expect(arg.startStr).toEqual('2014-04-27T00:00:00') // no Z + expect(arg.end).toEqualDate('2014-06-08T00:00:00Z') + expect(arg.endStr).toEqual('2014-06-08T00:00:00') // no Z + callback([]) + setTimeout(done) // :( + }, + }) + }) + + it('requests correctly when timezone changed dynamically', (done) => { + let callCnt = 0 + let options = { + timeZone: 'America/Chicago', + events(arg, callback) { + testEventFunctionParams(arg, callback) + callCnt += 1 + if (callCnt === 1) { + expect(arg.timeZone).toEqual('America/Chicago') + expect(arg.start).toEqualDate('2014-04-27') + expect(arg.end).toEqualDate('2014-06-08') + setTimeout(() => { + currentCalendar.setOption('timeZone', 'UTC') + }, 0) + } else if (callCnt === 2) { + expect(arg.timeZone).toEqual('UTC') + expect(arg.start).toEqualDate('2014-04-27') + expect(arg.end).toEqualDate('2014-06-08') + setTimeout(done) // :( + } + }, + } + + initCalendar(options) + }) + + it('requests correctly with event source extended form', (done) => { + let eventSource = { + className: 'customeventclass', + events(arg, callback) { + testEventFunctionParams(arg, callback) + expect(arg.timeZone).toEqual('UTC') + expect(arg.start).toEqualDate('2014-04-27') + expect(arg.end).toEqualDate('2014-06-08') + callback([ + { + title: 'event1', + start: '2014-05-10', + }, + ]) + }, + } + spyOn(eventSource, 'events').and.callThrough() + + initCalendar({ + timeZone: 'UTC', + eventSources: [eventSource], + eventDidMount(arg) { + expect(eventSource.events.calls.count()).toEqual(1) + expect(arg.el).toHaveClass('customeventclass') + setTimeout(done) // :( + }, + }) + }) + + it('can return a promise-like object', (done) => { + let calendar = initCalendar({ + events() { + let deferred = $.Deferred() // we want tests to run in IE11, which doesn't have native promises + setTimeout(() => { + deferred.resolve([ + { start: '2018-09-04' }, + ]) + }, 100) + return deferred.promise() + }, + }) + + setTimeout(() => { + expect(calendar.getEvents().length).toBe(1) + setTimeout(done) // :( + }, 101) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/events-gcal.ts b/fullcalendar-main/tests/src/legacy/events-gcal.ts new file mode 100644 index 0000000..7df23e1 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/events-gcal.ts @@ -0,0 +1,262 @@ +import googleCalendarPlugin from '@fullcalendar/google-calendar' +import dayGridPlugin from '@fullcalendar/daygrid' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +// In our CI setup, requests to the google-calendar api were failing for some reason +// (requests to other services were working however) +const noGCal = (window as any).__karma__.config.cliArgs.includes('--no-gcal') +if (noGCal) { + console.log('skipping google-calendar') // eslint-disable-line no-console +} + +// eslint-disable-next-line +noGCal || +describe('Google Calendar plugin', () => { + const API_KEY = 'AIzaSyDcnW6WejpTOCffshGDDb4neIrXVUA1EAE' + const HOLIDAY_CALENDAR_ID = 'en.usa#holiday@group.v.calendar.google.com' + + // Google sometimes stops returning old events. Will need to update this sometimes. + const DEFAULT_MONTH = '2023-05' + const NUM_EVENTS = 5 // number of holidays + + pushOptions({ + plugins: [googleCalendarPlugin, dayGridPlugin], + initialView: 'dayGridMonth', + initialDate: DEFAULT_MONTH + '-01', + }) + + it('request/receives correctly when local timezone', (done) => { + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + events: { googleCalendarId: HOLIDAY_CALENDAR_ID }, + timeZone: 'local', + }) + + afterEventsLoaded(calendar, () => { + let events = calendar.getEvents() + let i + + expect(events.length).toBe(NUM_EVENTS) + for (i = 0; i < events.length; i += 1) { + expect(events[i].url).not.toMatch('ctz=') + } + + done() + }) + }) + + it('request/receives correctly when UTC timezone', (done) => { + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + events: { googleCalendarId: HOLIDAY_CALENDAR_ID }, + timeZone: 'UTC', + }) + + afterEventsLoaded(calendar, () => { + let events = calendar.getEvents() + let i + + expect(events.length).toBe(NUM_EVENTS) + for (i = 0; i < events.length; i += 1) { + expect(events[i].url).toMatch('ctz=UTC') + } + + done() + }) + }) + + it('request/receives correctly when named timezone, defaults to not editable', (done) => { + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + events: { googleCalendarId: HOLIDAY_CALENDAR_ID }, + timeZone: 'America/New_York', + }) + + afterEventsLoaded(calendar, () => { + let events = calendar.getEvents() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let i + + expect(events.length).toBe(NUM_EVENTS) + for (i = 0; i < events.length; i += 1) { + expect(events[i].url).toMatch('ctz=America/New_York') + } + + expect(eventEls.length).toBe(NUM_EVENTS) + expect($('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME, eventEls[0]).length).toBe(0) // not editable + + done() + }) + }) + + it('allows editable to explicitly be set to true', (done) => { + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + events: { + googleCalendarId: HOLIDAY_CALENDAR_ID, + editable: true, + }, + }) + + afterEventsLoaded(calendar, () => { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + + expect(eventEls.length).toBe(NUM_EVENTS) + + for (let eventEl of eventEls) { + expect($('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME, eventEl).length).toBeGreaterThan(0) // editable! + } + + done() + }) + }) + + it('fetches events correctly when API key is in the event source', (done) => { + let calendar = initCalendar({ + events: { + googleCalendarId: HOLIDAY_CALENDAR_ID, + googleCalendarApiKey: API_KEY, + }, + }) + + afterEventsLoaded(calendar, () => { + let events = calendar.getEvents() + expect(events.length).toBe(NUM_EVENTS) // 5 holidays in November 2016 (and end of Oct) + done() + }) + }) + + describe('when not given an API key', () => { + it('calls error handlers, raises warning, and receives no events', (done) => { + let options = { + events: { + failure(err) { + expect(typeof err).toBe('object') + }, + googleCalendarId: HOLIDAY_CALENDAR_ID, + }, + eventSourceFailure(err) { + expect(typeof err).toBe('object') + + setTimeout(() => { // wait for potential render + let events = this.getEvents() + expect(events.length).toBe(0) + expect(options.events.failure).toHaveBeenCalled() + done() + }, 0) + }, + } + + spyOn(options.events, 'failure').and.callThrough() + initCalendar(options) + }) + }) + + describe('when given a bad API key', () => { + it('calls error handlers, raises warning, and receives no event', (done) => { + let options = { + googleCalendarApiKey: 'asdfasdfasdf', + events: { + failure(err) { + expect(typeof err).toBe('object') + }, + googleCalendarId: HOLIDAY_CALENDAR_ID, + }, + eventSourceFailure(err) { + expect(typeof err).toBe('object') + + setTimeout(() => { // wait for potential render + let events = this.getEvents() + expect(events.length).toBe(0) + expect(options.events.failure).toHaveBeenCalled() + done() + }, 0) + }, + } + + spyOn(options.events, 'failure').and.callThrough() + initCalendar(options) + }) + }) + + it('calls loading with true then false', (done) => { + let cmds = [] + + initCalendar({ + googleCalendarApiKey: API_KEY, + events: { googleCalendarId: HOLIDAY_CALENDAR_ID }, + loading(bool) { + cmds.push(bool) + + if (cmds.length === 1) { + expect(cmds).toEqual([true]) + } else if (cmds.length === 2) { + expect(cmds).toEqual([true, false]) + done() + } + }, + }) + }) + + describe('EventSource::remove', () => { + it('works when specifying only the Google Calendar ID', (done) => { + let called = false + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + eventSources: [{ googleCalendarId: HOLIDAY_CALENDAR_ID }], + }) + + afterEventsLoaded(calendar, () => { + let events + + if (called) { return } // only the first time + called = true + + events = calendar.getEvents() + expect(events.length).toBe(NUM_EVENTS) // 5 holidays in November 2016 (and end of Oct) + + setTimeout(() => { + calendar.getEventSources()[0].remove() + events = calendar.getEvents() + expect(events.length).toBe(0) + done() + }, 0) + }) + }) + + it('works when specifying a raw Google Calendar source object', (done) => { + let googleSource = { googleCalendarId: HOLIDAY_CALENDAR_ID } + let called = false + let calendar = initCalendar({ + googleCalendarApiKey: API_KEY, + eventSources: [googleSource], + }) + + afterEventsLoaded(calendar, () => { + let events + + if (called) { return } // only the first time + called = true + + events = calendar.getEvents() + expect(events.length).toBe(NUM_EVENTS) // 5 holidays in November 2016 (and end of Oct) + + setTimeout(() => { + calendar.getEventSources()[0].remove() + events = calendar.getEvents() + expect(events.length).toBe(0) + done() + }, 0) + }) + }) + }) + + function afterEventsLoaded(calendar, callback: () => void) { + calendar.on('eventsSet', () => { + setTimeout(callback) // because nothing is rendered yet when eventSourceSuccess fires + }) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/events-json-feed.ts b/fullcalendar-main/tests/src/legacy/events-json-feed.ts new file mode 100644 index 0000000..6cbb4ab --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/events-json-feed.ts @@ -0,0 +1,244 @@ +import fetchMock from 'fetch-mock' +import { JsonRequestError } from '@fullcalendar/core' +import { formatIsoTimeZoneOffset } from '../lib/datelib-utils.js' + +describe('events as a json feed', () => { + pushOptions({ + initialDate: '2014-05-01', + initialView: 'dayGridMonth', + }) + + afterEach(() => { + fetchMock.restore() + }) + + it('requests correctly when local timezone', () => { + const START = '2014-04-27T00:00:00' + const END = '2014-06-08T00:00:00' + + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: givenUrl, + timeZone: 'local', + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe(START + formatIsoTimeZoneOffset(new Date(START))) + expect(requestParams.get('end')).toBe(END + formatIsoTimeZoneOffset(new Date(END))) + }) + + it('requests correctly when UTC timezone', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: givenUrl, + timeZone: 'UTC', + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe('2014-04-27T00:00:00Z') + expect(requestParams.get('end')).toBe('2014-06-08T00:00:00Z') + expect(requestParams.get('timeZone')).toBe('UTC') + }) + + it('requests correctly when named timezone', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: givenUrl, + timeZone: 'America/Chicago', + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe('2014-04-27T00:00:00') + expect(requestParams.get('end')).toBe('2014-06-08T00:00:00') + expect(requestParams.get('timeZone')).toBe('America/Chicago') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5485 + it('processes new events under updated time zone', (done) => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, (requestUrl) => { + const requestParams = new URL(requestUrl).searchParams + let reqTimeZone = requestParams.get('timeZone') + return { + body: [ + reqTimeZone === 'America/Chicago' + ? { start: '2014-06-08T01:00:00' } + : { start: '2014-06-08T03:00:00' }, + ], + } + }) + + let calendar = initCalendar({ + events: givenUrl, + timeZone: 'America/Chicago', + }) + + setTimeout(() => { + let eventStartStr = calendar.getEvents()[0].startStr + expect(eventStartStr).toBe('2014-06-08T01:00:00') + + calendar.setOption('timeZone', 'America/New_York') + setTimeout(() => { + eventStartStr = calendar.getEvents()[0].startStr + expect(eventStartStr).toBe('2014-06-08T03:00:00') + done() + }, 100) + }, 100) + }) + + it('requests correctly with event source extended form', (done) => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { + body: [ + { + title: 'my event', + start: '2014-05-21', + }, + ], + }) + + initCalendar({ + eventSources: [{ + url: givenUrl, + classNames: 'customeventclass', + }], + timeZone: 'America/Chicago', + eventDidMount(arg) { + expect(arg.el).toHaveClass('customeventclass') + done() + }, + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe('2014-04-27T00:00:00') + expect(requestParams.get('end')).toBe('2014-06-08T00:00:00') + expect(requestParams.get('timeZone')).toBe('America/Chicago') + }) + + it('requests POST correctly', (done) => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.post(/my-feed\.php/, (url, options) => { + const paramStrGet = new URL(url).searchParams.toString() + const paramStrPost = options.body.toString() + expect(paramStrGet).toBe('') + expect(paramStrPost).toBe('start=2014-04-27T00%3A00%3A00Z&end=2014-06-08T00%3A00%3A00Z&timeZone=UTC') + done() + return { body: [] } + }) + + initCalendar({ + events: { + url: givenUrl, + method: 'POST', + }, + timeZone: 'UTC', + }) + }) + + it('accepts a extraParams object', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + eventSources: [{ + url: givenUrl, + extraParams: { + customParam: 'yes', + }, + }], + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe('2014-04-27T00:00:00Z') + expect(requestParams.get('end')).toBe('2014-06-08T00:00:00Z') + expect(requestParams.get('timeZone')).toBe('UTC') + expect(requestParams.get('customParam')).toBe('yes') + }) + + it('accepts a dynamic extraParams function', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + eventSources: [{ + url: givenUrl, + extraParams() { + return { + customParam: 'heckyeah', + } + }, + }], + }) + + const [requestUrl] = fetchMock.lastCall() + const requestParams = new URL(requestUrl).searchParams + expect(requestParams.get('start')).toBe('2014-04-27T00:00:00Z') + expect(requestParams.get('end')).toBe('2014-06-08T00:00:00Z') + expect(requestParams.get('timeZone')).toBe('UTC') + expect(requestParams.get('customParam')).toBe('heckyeah') + }) + + it('calls loading callback', (done) => { + const loadingCallArgs = [] + + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: { url: givenUrl }, + loading(bool) { + loadingCallArgs.push(bool) + }, + }) + + setTimeout(() => { + expect(loadingCallArgs).toEqual([true, false]) + done() + }, 100) + }) + + it('has and Event Source object with certain props', () => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: [] }) + + initCalendar({ + events: { url: givenUrl }, + }) + expect(currentCalendar.getEventSources()[0].url).toBe(givenUrl) + }) + + it('throws JsonRequestError if mangled JSON', (done) => { + const givenUrl = window.location.href + '/my-feed.php' + fetchMock.get(/my-feed\.php/, { body: '[{title:' }) + + let eventSourceFailureCalled = false + + initCalendar({ + events: { url: givenUrl }, + eventSourceFailure(error) { + let isJsonRequestFailure = error instanceof JsonRequestError + if (isJsonRequestFailure) { + expect(typeof error.response.url).toBe('string') // NOTE: fetchMock mangles exact url + } + expect(isJsonRequestFailure).toBe(true) + eventSourceFailureCalled = true + }, + }) + + setTimeout(() => { + expect(eventSourceFailureCalled).toBe(true) + done() + }, 100) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/external-dnd-advanced.ts b/fullcalendar-main/tests/src/legacy/external-dnd-advanced.ts new file mode 100644 index 0000000..2cd924b --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/external-dnd-advanced.ts @@ -0,0 +1,386 @@ +import { ThirdPartyDraggable } from '@fullcalendar/interaction' +import { testEventDrag } from '../lib/dnd-resize-utils.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +// TODO: Use the built-in Draggable for some of these tests + +describe('advanced external dnd', () => { + let dragEl + let thirdPartyDraggable + + beforeEach(() => { + dragEl = $('<div class="drag">yo</div>') + .css({ + width: 200, + background: 'blue', + color: 'white', + }) + .appendTo('body') + .draggable() + + thirdPartyDraggable = new ThirdPartyDraggable({ + itemSelector: '.drag', + }) + }) + + afterEach(() => { + thirdPartyDraggable.destroy() + dragEl.remove() + dragEl = null + }) + + pushOptions({ + initialDate: '2014-11-13', + scrollTime: '00:00:00', + droppable: true, + }) + + describe('in timeGrid slots', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('when no element event data', () => { + describe('when given duration through defaultTimedEventDuration', () => { + pushOptions({ + defaultTimedEventDuration: '2:30', + }) + defineTests() + }) + + describe('when given duration through data attribute', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + duration: '2:30', + create: false, // only an external element, won't create or render as an event + })) + }) + defineTests() + }) + + function defineTests() { + it('fires correctly', (done) => { + testExternalElDrag({}, '2014-11-13T03:00:00Z', '2014-11-13T03:00:00Z', true, done) + }) + + it('is not affected by eventOverlap:false', (done) => { + let options = { + eventOverlap: false, + events: [{ + start: '2014-11-13T01:00:00', + end: '2014-11-13T05:00:00', + }], + } + testExternalElDrag(options, '2014-11-13T03:00:00Z', '2014-11-13T03:00:00Z', true, done) + }) + + it('is not affected by an event object\'s overlap:false', (done) => { + let options = { + events: [{ + start: '2014-11-13T01:00:00', + end: '2014-11-13T05:00:00', + overlap: false, + }], + } + testExternalElDrag(options, '2014-11-13T03:00:00Z', '2014-11-13T03:00:00Z', true, done) + }) + + it('is not affected by eventConstraint', (done) => { + let options = { + eventConstraint: { + start: '03:00', + end: '10:00', + }, + } + testExternalElDrag(options, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', true, done) + }) + + describe('with selectOverlap:false', () => { + pushOptions({ + selectOverlap: false, + events: [{ + start: '2014-11-13T04:00:00', + end: '2014-11-13T08:00:00', + }], + }) + + it('is not allowed to overlap an event', (done) => { + testExternalElDrag({}, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', false, done) + }) + }) + + describe('with a selectConstraint', () => { + pushOptions({ + selectConstraint: { + startTime: '04:00', + endTime: '08:00', + }, + }) + + it('can be dropped within', (done) => { + testExternalElDrag({}, '2014-11-13T05:30:00Z', '2014-11-13T05:30:00Z', true, done) + }) + + it('cannot be dropped when not fully contained', (done) => { + testExternalElDrag({}, '2014-11-13T06:00:00Z', '2014-11-13T06:00:00Z', false, done) + }) + }) + } + }) + + describe('when event data is given', () => { + it('fires correctly', (done) => { + dragEl.attr('data-event', JSON.stringify({ + title: 'hey', + })) + testExternalEventDrag({}, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', true, done) + }) + + describe('when given a start time', () => { + describe('through the event object\'s time property', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + startTime: '05:00', + })) + }) + + it('voids the given time when dropped on a timed slot', (done) => { + testExternalEventDrag({}, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', true, done) + // will test the resulting event object's start + }) + }) + }) + + describe('when given a duration', () => { + describe('through the event object\'s duration property', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + duration: '05:00', + })) + }) + + it('accepts the given duration when dropped on a timed slot', (done) => { + testExternalEventDrag({}, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', true, () => { + let event = currentCalendar.getEvents()[0] + expect(event.start).toEqualDate('2014-11-13T02:00:00Z') + expect(event.end).toEqualDate('2014-11-13T07:00:00Z') + done() + }) + }) + }) + }) + + describe('when given stick:true', () => { + describe('through the event object', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + stick: true, + })) + }) + + it('keeps the event when navigating away and back', (done) => { + testExternalEventDrag({}, '2014-11-13T02:00:00Z', '2014-11-13T02:00:00Z', true, () => { + setTimeout(() => { // make sure to escape handlers + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toBe(1) + currentCalendar.next() + expect(calendarWrapper.getEventEls().length).toBe(0) + currentCalendar.prev() + expect(calendarWrapper.getEventEls().length).toBe(1) + done() + }, 0) + }) + }) + }) + }) + + describe('when an overlap is specified', () => { + describe('via eventOverlap', () => { + pushOptions({ + eventOverlap: false, + events: [{ + start: '2014-11-13T05:00:00', + end: '2014-11-13T08:00:00', + }], + }) + + beforeEach(() => { + dragEl.attr('data-event', '{}') + }) + + defineTests() + }) + + describe('via an overlap on this event', () => { + pushOptions({ + events: [{ + start: '2014-11-13T05:00:00', + end: '2014-11-13T08:00:00', + }], + }) + + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + overlap: false, + })) + }) + + defineTests() + }) + + describe('via an overlap on the other event', () => { + pushOptions({ + events: [{ + start: '2014-11-13T05:00:00', + end: '2014-11-13T08:00:00', + overlap: false, + }], + }) + + beforeEach(() => { + dragEl.attr('data-event', '{}') + }) + + defineTests() + }) + + function defineTests() { + it('allows a drop when not colliding with the other event', (done) => { + testExternalEventDrag({}, '2014-11-13T08:00:00Z', '2014-11-13T08:00:00Z', true, done) + }) + it('prevents a drop when colliding with the other event', (done) => { + testExternalEventDrag({}, '2014-11-13T06:00:00Z', '2014-11-13T06:00:00Z', false, done) + }) + } + }) + + describe('when a constraint is specified', () => { + describe('via eventConstraint', () => { + pushOptions({ + eventConstraint: { + startTime: '04:00', + endTime: '08:00', + }, + }) + + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + duration: '02:00', + })) + }) + + defineTests() + }) + + describe('via the event object\'s constraint property', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + duration: '02:00', + constraint: { + startTime: '04:00', + endTime: '08:00', + }, + })) + }) + + defineTests() + }) + + function defineTests() { + it('allows a drop when inside the constraint', (done) => { + testExternalEventDrag({}, '2014-11-13T05:00:00Z', '2014-11-13T05:00:00Z', true, done) + }) + it('disallows a drop when partially outside of the constraint', (done) => { + testExternalEventDrag({}, '2014-11-13T07:00:00Z', '2014-11-13T07:00:00Z', false, done) + }) + } + }) + }) + }) + + // TODO: write more tests for DayGrid! + + describe('in month whole-days', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + describe('when event data is given', () => { + it('fires correctly', (done) => { + dragEl.attr('data-event', JSON.stringify({ + title: 'hey', + })) + testExternalEventDrag({}, '2014-11-13', '2014-11-13', true, done) + }) + + describe('when given a start time', () => { + describe('through the event object\'s time property', () => { + beforeEach(() => { + dragEl.attr('data-event', JSON.stringify({ + startTime: '05:00', + })) + }) + + it('accepts the given start time for the dropped day', (done) => { + testExternalEventDrag({}, '2014-11-13', '2014-11-13T05:00:00Z', true, () => { + // the whole-day start was already checked. we still need to check the exact time + let event = currentCalendar.getEvents()[0] + expect(event.start).toEqualDate('2014-11-13T05:00:00Z') + done() + }) + }) + }) + }) + }) + }) + + function testExternalElDrag(options, dragToDate, expectedDate, expectSuccess, callback) { // with NO event creation + options.droppable = true + options.drop = (arg) => { + expect(arg.date instanceof Date).toBe(true) + expect(arg.date).toEqualDate(expectedDate) + expect(typeof arg.jsEvent).toBe('object') + } + options.eventReceive = () => {} + spyOn(options, 'drop').and.callThrough() + spyOn(options, 'eventReceive').and.callThrough() + + testEventDrag(options, dragToDate, expectSuccess, () => { + if (expectSuccess) { + expect(options.drop).toHaveBeenCalled() + } else { + expect(options.drop).not.toHaveBeenCalled() + } + expect(options.eventReceive).not.toHaveBeenCalled() + callback() + }, 'drag') // .drag className + } + + function testExternalEventDrag(options, dragToDate, expectedDate, expectSuccess, callback) { + let expectedAllDay = dragToDate.indexOf('T') === -1 // for the drop callback only! + + options.droppable = true + options.drop = (arg) => { + expect(arg.date instanceof Date).toBe(true) + expect(arg.date).toEqualDate(dragToDate) + expect(arg.allDay).toBe(expectedAllDay) + expect(typeof arg.jsEvent).toBe('object') + } + options.eventReceive = (arg) => { + expect(arg.event.start).toEqualDate(expectedDate) + } + spyOn(options, 'drop').and.callThrough() + spyOn(options, 'eventReceive').and.callThrough() + + testEventDrag(options, dragToDate, expectSuccess, () => { + if (expectSuccess) { + expect(options.drop).toHaveBeenCalled() + expect(options.eventReceive).toHaveBeenCalled() + } else { + expect(options.drop).not.toHaveBeenCalled() + expect(options.eventReceive).not.toHaveBeenCalled() + } + callback() + }, 'drag') // .drag className + } +}) diff --git a/fullcalendar-main/tests/src/legacy/external-dnd.ts b/fullcalendar-main/tests/src/legacy/external-dnd.ts new file mode 100644 index 0000000..5668591 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/external-dnd.ts @@ -0,0 +1,405 @@ +import { Calendar } from '@fullcalendar/core' +import interactionPlugin, { ThirdPartyDraggable } from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import { ListenerCounter } from '../lib/ListenerCounter.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('external drag and drop with jquery UI', () => { + pushOptions({ + plugins: [interactionPlugin, timeGridPlugin, dayGridPlugin], + initialDate: '2014-08-23', + initialView: 'dayGridMonth', + droppable: true, + }) + + // TODO: fill out tests for droppable/drop, with RTL + + let thirdPartyDraggable + + beforeEach(() => { + $('body').append( + '<div id="sidebar" style="width:200px">' + + `<a class="${CalendarWrapper.EVENT_CLASSNAME} event1">event 1</a>` + + `<a class="${CalendarWrapper.EVENT_CLASSNAME} event2">event 2</a>` + + '</div>' + + '<div id="cal" style="width:600px;position:absolute;top:10px;left:220px" />', + ) + + thirdPartyDraggable = new ThirdPartyDraggable({ + itemSelector: `#sidebar .${CalendarWrapper.EVENT_CLASSNAME}`, + }) + }) + + afterEach(() => { + $('#sidebar').remove() + $('#cal').remove() + thirdPartyDraggable.destroy() + }) + + function initCalendarInContainer(options = {}) { + return initCalendar(options, $('#cal')[0]) + } + + describeValues({ + 'with draggable': () => $('#sidebar a').draggable(), + 'with sortable': () => $('#sidebar').sortable(), + }, (initDnd) => { + describe('in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('works after the view is changed', (done) => { // issue 2240 + let callCnt = 0 + let dayGridWrapper + let calendar = initCalendarInContainer({ + drop(arg) { + if (callCnt === 0) { + expect(arg.date).toEqualDate('2014-08-06') + + calendar.next() + calendar.prev() + + setTimeout(() => { // weird + $('#sidebar .event1').remove() + $('#sidebar .event2').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + }) + }, 0) + } else if (callCnt === 1) { + expect(arg.date).toEqualDate('2014-08-06') + setTimeout(done) // weird + } + + callCnt += 1 + }, + }) + dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + }) + }) + }) + + describe('dropAccept', () => { + it('works with a className that does match', (done) => { + let options = { + dropAccept: '.event1', + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + callback() { + expect(options.drop).toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('prevents a classNames that doesn\'t match', (done) => { + let options = { + dropAccept: '.event2', + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + callback() { + expect(options.drop).not.toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('works with a filter function that returns true', (done) => { + let options = { + dropAccept(el) { + expect(el instanceof HTMLElement).toBe(true) + return true + }, + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + callback() { + expect(options.drop).toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('prevents a drop with a filter function that returns false', (done) => { + let options = { + dropAccept(el) { + expect(el instanceof HTMLElement).toBe(true) + return false + }, + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + callback() { + expect(options.drop).not.toHaveBeenCalled() + done() + }, + }) + }) + }) + }) + }) + + describe('in timeGrid view', () => { + pushOptions({ + initialView: 'timeGridWeek', + dragScroll: false, + scrollTime: '00:00:00', + }) + + it('works after the view is changed', (done) => { + let callCnt = 0 + let timeGridWrapper + let calendar = initCalendarInContainer({ + drop(arg) { + if (callCnt === 0) { + expect(arg.date).toEqualDate('2014-08-20T01:00:00Z') + + currentCalendar.next() + currentCalendar.prev() + + setTimeout(() => { // weird + $('#sidebar .event1').remove() + $('#sidebar .event2').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + }) + }, 0) + } else if (callCnt === 1) { + expect(arg.date).toEqualDate('2014-08-20T01:00:00Z') + setTimeout(done) // weird + } + + callCnt += 1 + }, + }) + timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + }) + }) + }) + + it('works with timezone as "local"', (done) => { // for issue 2225 + let calendar = initCalendarInContainer({ + timeZone: 'local', + drop(arg) { + expect(arg.date).toEqualLocalDate('2014-08-20T01:00:00') + done() + }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + }) + }) + }) + + it('works with timezone as "UTC"', (done) => { // for issue 2225 + let calendar = initCalendarInContainer({ + timeZone: 'UTC', + drop(arg) { + expect(arg.date).toEqualDate('2014-08-20T01:00:00Z') + done() + }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + }) + }) + }) + + describe('dropAccept', () => { + it('works with a className that does match', (done) => { + let options = { + dropAccept: '.event1', + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + callback() { + expect(options.drop).toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('prevents a classNames that doesn\'t match', (done) => { + let options = { + dropAccept: '.event2', + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + callback() { + expect(options.drop).not.toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('works with a filter function that returns true', (done) => { + let options = { + dropAccept(el) { + expect(el instanceof HTMLElement).toBe(true) + return true + }, + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + callback() { + expect(options.drop).toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('prevents a drop with a filter function that returns false', (done) => { + let options = { + dropAccept(el) { + expect(el instanceof HTMLElement).toBe(true) + return false + }, + drop() { }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + initDnd() + setTimeout(() => { // weird + $('#sidebar .event1').simulate('drag', { + end: timeGridWrapper.getPoint('2014-08-20T01:00:00'), + callback() { + expect(options.drop).not.toHaveBeenCalled() + done() + }, + }) + }) + }) + }) + }) + + // Issue 2433 + it('should not have drag handlers cleared when other calendar navigates', () => { + let calendar0 = initCalendarInContainer() + initDnd() + + let el0 = calendar0.el + let $el1 = $('<div id="calendar2">').insertAfter(el0) + let calendar1 = new Calendar($el1[0], getCurrentOptions()) + calendar1.render() + + let docListenerCounter = new ListenerCounter(document) + docListenerCounter.startWatching() + + calendar0.next() + expect(docListenerCounter.stopWatching()).toBe(0) + + calendar1.destroy() + $el1.remove() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/2926 + it('gives a mouseup event to the drop handler', (done) => { + let options = { + drop(info) { + expect(info.jsEvent.type).toBe('mouseup') + }, + } + spyOn(options, 'drop').and.callThrough() + + let calendar = initCalendarInContainer(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + setTimeout(() => { // weird + $('#sidebar .event1').draggable().simulate('drag', { + end: dayGridWrapper.getDayEl('2014-08-06'), + callback() { + expect(options.drop).toHaveBeenCalled() + done() + }, + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/firstDay.ts b/fullcalendar-main/tests/src/legacy/firstDay.ts new file mode 100644 index 0000000..ea83a24 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/firstDay.ts @@ -0,0 +1,101 @@ +import enGbLocale from '@fullcalendar/core/locales/en-gb' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('firstDay', () => { + describe('when using default settings', () => { + it('should make Sunday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 0) + }) + }) + + describe('when setting firstDay to 0', () => { + pushOptions({ + firstDay: 0, + }) + + it('should make Sunday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 0) + }) + }) + + describe('when setting firstDay to 1', () => { + pushOptions({ + firstDay: 1, + }) + + it('should make Monday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 1) + }) + }) + + describe('when setting weekNumberCalculation to ISO', () => { + pushOptions({ + weekNumberCalculation: 'ISO', + }) + + it('should make Monday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 1) + }) + }) + + describeOptions('direction', { + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, () => { + pushOptions({ + firstDay: 2, + }) + + it('should make Tuesday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 2) + }) + }) + + describe('when setting firstDay to 2 and weekNumberCalculation to ISO', () => { + pushOptions({ + firstDay: 2, + weekNumberCalculation: 'ISO', + }) + + it('should make Tuesday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 2) + }) + }) + + describe('when setting firstDay to 3', () => { + pushOptions({ + firstDay: 3, + }) + + it('should make Wednesday the first day of the week', () => { + let calendar = initCalendar() + expectDowStartAt(calendar, 3) + }) + }) + + it('should have a different default value based on the locale', () => { + let calendar = initCalendar({ + locale: enGbLocale, + }) + // firstDay will be 1 (Monday) in Great Britain + expectDowStartAt(calendar, 1) + }) + + const DOW_CLASSNAMES = CalendarWrapper.DOW_CLASSNAMES + + function expectDowStartAt(calendar, dowNum) { + let headerWrapper = new DayGridViewWrapper(calendar).header + let cellEls = headerWrapper.getCellEls() + + for (let i = 0; i < 7; i += 1) { + expect(cellEls[i]).toHaveClass(DOW_CLASSNAMES[(i + dowNum) % 7]) + } + } +}) diff --git a/fullcalendar-main/tests/src/legacy/fixedWeekCount.ts b/fullcalendar-main/tests/src/legacy/fixedWeekCount.ts new file mode 100644 index 0000000..df29b51 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/fixedWeekCount.ts @@ -0,0 +1,47 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('fixedWeekCount', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-07-01', // has 5 weeks + }) + + describe('when true', () => { + pushOptions({ + fixedWeekCount: true, + }) + + it('renders a 5-week month with 6 rows', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getRowEls().length).toBe(6) + }) + }) + + describe('when false', () => { + pushOptions({ + fixedWeekCount: false, + }) + + it('renders a 5-week month with 5 rows', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getRowEls().length).toBe(5) + }) + }); + + [true, false].forEach((bool) => { + describe('regardless of value (' + bool + ')', () => { + pushOptions({ + fixedWeekCount: bool, + initialDate: '2014-08-01', // has 6 weeks + }) + + it('should render a 6-week month consistently', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getRowEls().length).toBe(6) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/footer-navigation.ts b/fullcalendar-main/tests/src/legacy/footer-navigation.ts new file mode 100644 index 0000000..5309a12 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/footer-navigation.ts @@ -0,0 +1,85 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('footerToolbar navigation', () => { // TODO: rename file + pushOptions({ + now: '2010-02-01', + headerToolbar: false, + footerToolbar: { + left: 'next,prev,prevYear,nextYear today', + center: '', + right: 'title', + }, + }) + + describe('and click next', () => { + it('should change view to next month', (done) => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).footerToolbar + + $(toolbarWrapper.getButtonEl('next')).simulate('click') + setTimeout(() => { + let newDate = currentCalendar.getDate() + expect(newDate).toEqualDate('2010-03-01') + done() + }) + }) + }) + + describe('and click prev', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).footerToolbar + + $(toolbarWrapper.getButtonEl('prev')).simulate('click') + setTimeout(() => { + let newDate = currentCalendar.getDate() + expect(newDate).toEqualDate('2010-01-01') + done() + }) + }) + }) + + describe('and click prevYear', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).footerToolbar + + $(toolbarWrapper.getButtonEl('prevYear')).simulate('click') + setTimeout(() => { + let newDate = currentCalendar.getDate() + expect(newDate).toEqualDate('2009-02-01') + done() + }) + }) + }) + + describe('and click nextYear', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).footerToolbar + + $(toolbarWrapper.getButtonEl('nextYear')).simulate('click') + setTimeout(() => { + let newDate = currentCalendar.getDate() + expect(newDate).toEqualDate('2011-02-01') + done() + }) + }) + }) + + describe('and click today', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar({ + initialDate: '2010-03-15', // something other than the `now` date + }) + let toolbarWrapper = new CalendarWrapper(calendar).footerToolbar + + $(toolbarWrapper.getButtonEl('today')).simulate('click') + setTimeout(() => { + let newDate = currentCalendar.getDate() // will be ambig zone + expect(newDate).toEqualDate('2010-02-01') + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/footer-rendering.ts b/fullcalendar-main/tests/src/legacy/footer-rendering.ts new file mode 100644 index 0000000..a8880f6 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/footer-rendering.ts @@ -0,0 +1,46 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('footerToolbar rendering', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-06-04', + initialView: 'timeGridWeek', + }) + + describe('when supplying footerToolbar options', () => { + it('should append a footerToolbar element to the DOM', () => { + let calendar = initCalendar({ + footerToolbar: { + left: 'next,prev', + center: 'prevYear today nextYear timeGridDay,timeGridWeek', + right: 'title', + }, + }) + let calendarWrapper = new CalendarWrapper(calendar) + expect(calendarWrapper.footerToolbar).toBeTruthy() + }) + }) + + describe('when setting footerToolbar to false', () => { + it('should not have footerToolbar table', () => { + let calendar = initCalendar({ + footerToolbar: false, + }) + let calendarWrapper = new CalendarWrapper(calendar) + expect(calendarWrapper.footerToolbar).toBeFalsy() + }) + }) + + it('allow for dynamically changing', () => { + let calendar = initCalendar({ + footerToolbar: { + left: 'next,prev', + center: 'prevYear today nextYear timeGridDay,timeGridWeek', + right: 'title', + }, + }) + let calendarWrapper = new CalendarWrapper(calendar) + expect(calendarWrapper.footerToolbar).toBeTruthy() + currentCalendar.setOption('footerToolbar', false) + expect(calendarWrapper.footerToolbar).toBeFalsy() + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/forceEventDuration.ts b/fullcalendar-main/tests/src/legacy/forceEventDuration.ts new file mode 100644 index 0000000..9eeea10 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/forceEventDuration.ts @@ -0,0 +1,58 @@ +describe('forceEventDuration', () => { + pushOptions({ + initialDate: '2014-05-01', + initialView: 'dayGridMonth', + }) + + describe('when turned off', () => { + pushOptions({ + forceEventDuration: false, + }) + it('allows a null end date for all-day and timed events', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-10', + }, + { + id: '2', + start: '2014-05-10T14:00:00', + }, + ], + }) + let events = currentCalendar.getEvents() + expect(events[0].end).toBeNull() + expect(events[1].end).toBeNull() + }) + }) + + describe('when turned on', () => { + pushOptions({ + forceEventDuration: true, + }) + it('allows a null end date for all-day and timed events', () => { + initCalendar({ + events: [ + { + id: '1', + start: '2014-05-10', + }, + { + id: '2', + start: '2014-05-10T14:00:00', + }, + ], + }) + let events = currentCalendar.getEvents() + expect(events[0].id).toEqual('1') + expect(events[0].end instanceof Date).toEqual(true) + expect(events[1].id).toEqual('2') + expect(events[1].end instanceof Date).toEqual(true) + }) + }) + + // NOTE: the actual verification of the correct calculation of the end + // (using defaultTimedEventDuration and defaultAllDayEventDuration) + // is done in those test files. +}) diff --git a/fullcalendar-main/tests/src/legacy/getEventSourceById.ts b/fullcalendar-main/tests/src/legacy/getEventSourceById.ts new file mode 100644 index 0000000..dd9a172 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/getEventSourceById.ts @@ -0,0 +1,38 @@ +describe('getEventSource', () => { + pushOptions({ + now: '2015-08-07', + initialView: 'timeGridWeek', + eventSources: [ + { + events: [ + { id: '1', start: '2015-08-07T02:00:00', end: '2015-08-07T03:00:00', title: 'event A' }, + ], + id: 'source1', + }, + { + events: [ + { id: '2', start: '2015-08-07T03:00:00', end: '2015-08-07T04:00:00', title: 'event B' }, + ], + id: 'source2', + }, + { + events: [ + { id: '3', start: '2015-08-07T04:00:00', end: '2015-08-07T05:00:00', title: 'event C' }, + ], + id: 'source3', + }, + ], + }) + + it('retreives the queried event source', (done) => { + initCalendar() + + let eventSource1 = currentCalendar.getEventSourceById('source1') + let eventSource2 = currentCalendar.getEventSourceById('source2') + + expect(eventSource1.id).toBe('source1') + expect(eventSource2.id).toBe('source2') + + done() + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/getEventSources.ts b/fullcalendar-main/tests/src/legacy/getEventSources.ts new file mode 100644 index 0000000..0303d52 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/getEventSources.ts @@ -0,0 +1,35 @@ +describe('getEventSources', () => { + pushOptions({ + now: '2015-08-07', + initialView: 'timeGridWeek', + eventSources: [ + { + events: [ + { id: '1', start: '2015-08-07T02:00:00', end: '2015-08-07T03:00:00', title: 'event A' }, + ], + }, + { + events: [ + { id: '2', start: '2015-08-07T03:00:00', end: '2015-08-07T04:00:00', title: 'event B' }, + ], + }, + { + events: [ + { id: '3', start: '2015-08-07T04:00:00', end: '2015-08-07T05:00:00', title: 'event C' }, + ], + }, + ], + }) + + it('does not mutate when removeEventSource is called', (done) => { + initCalendar() + let eventSources = currentCalendar.getEventSources() + expect(eventSources.length).toBe(3) + + // prove that eventSources is a copy, and wasn't mutated + eventSources[0].remove() + expect(eventSources.length).toBe(3) + + done() + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/header-navigation.ts b/fullcalendar-main/tests/src/legacy/header-navigation.ts new file mode 100644 index 0000000..d006cbb --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/header-navigation.ts @@ -0,0 +1,86 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('header navigation', () => { + pushOptions({ + headerToolbar: { + left: 'next,prev,prevYear,nextYear today', + center: '', + right: 'title', + }, + }) + + describe('and click next', () => { + it('should change view to next month', (done) => { + let calendar = initCalendar() + calendar.gotoDate('2010-02-01') + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + $(toolbarWrapper.getButtonEl('next')).simulate('click') + setTimeout(() => { + let newDate = calendar.getDate() + expect(newDate).toEqualDate('2010-03-01') + done() + }) + }) + }) + + describe('and click prev', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + calendar.gotoDate('2010-02-01') + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + $(toolbarWrapper.getButtonEl('prev')).simulate('click') + setTimeout(() => { + let newDate = calendar.getDate() + expect(newDate).toEqualDate('2010-01-01') + done() + }) + }) + }) + + describe('and click prevYear', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + calendar.gotoDate('2010-02-01') + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + $(toolbarWrapper.getButtonEl('prevYear')).simulate('click') + setTimeout(() => { + let newDate = calendar.getDate() + expect(newDate).toEqualDate('2009-02-01') + done() + }) + }) + }) + + describe('and click nextYear', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + calendar.gotoDate('2010-02-01') + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + $(toolbarWrapper.getButtonEl('nextYear')).simulate('click') + setTimeout(() => { + let newDate = calendar.getDate() + expect(newDate).toEqualDate('2011-02-01') + done() + }) + }) + }) + + describe('and click today', () => { + it('should change view to prev month', (done) => { + let calendar = initCalendar() + calendar.gotoDate('2010-02-01') + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + $(toolbarWrapper.getButtonEl('today')).simulate('click') + setTimeout(() => { + let newDate = calendar.getDate() // will be ambig zone + expect(newDate).toEqualNow() + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/header-rendering.ts b/fullcalendar-main/tests/src/legacy/header-rendering.ts new file mode 100644 index 0000000..39e54cb --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/header-rendering.ts @@ -0,0 +1,151 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('headerToolbar rendering', () => { // TODO: rename file + it('renders the default headerToolbar option', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getSectionContent(0)).toEqual( + [{ type: 'title' }], + ) + + expect(toolbarWrapper.getSectionContent(1)).toEqual([]) + + expect(toolbarWrapper.getSectionContent(2)).toEqual([ + { type: 'button', name: 'today' }, + { + type: 'button-group', + children: [ + { type: 'button', name: 'prev' }, + { type: 'button', name: 'next' }, + ], + }, + ]) + }) + + it('renders a given headerToolbar option', () => { + let calendar = initCalendar({ + headerToolbar: { + left: 'next,prev', + center: 'prevYear today nextYear timeGridDay,timeGridWeek', + right: 'title', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getSectionContent(0)).toEqual([ + { + type: 'button-group', + children: [ + { type: 'button', name: 'next' }, + { type: 'button', name: 'prev' }, + ], + }, + ]) + + expect(toolbarWrapper.getSectionContent(1)).toEqual([ + { type: 'button', name: 'prevYear' }, + { type: 'button', name: 'today' }, + { type: 'button', name: 'nextYear' }, + { + type: 'button-group', + children: [ + { type: 'button', name: 'timeGridDay' }, + { type: 'button', name: 'timeGridWeek' }, + ], + }, + ]) + + expect(toolbarWrapper.getSectionContent(2)).toEqual([ + { type: 'title' }, + ]) + }) + + describe('when setting headerToolbar to false', () => { + pushOptions({ + headerToolbar: false, + }) + + it('should not have headerToolbar', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper).toBeFalsy() + }) + }) + + it('allow for dynamically changing', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper).toBeTruthy() + + calendar.setOption('headerToolbar', false) + toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper).toBeFalsy() + }) + + describeOptions('direction', { + 'when direction is LTR': 'ltr', + 'when direction is RTL': 'rtl', + }, () => { + it('renders left and right literally', () => { + let calendar = initCalendar({ + headerToolbar: { + left: 'prev', + center: 'today', + right: 'next', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getSectionContent(0)).toEqual([ + { type: 'button', name: 'prev' }, + ]) + + expect(toolbarWrapper.getSectionContent(1)).toEqual([ + { type: 'button', name: 'today' }, + ]) + + expect(toolbarWrapper.getSectionContent(2)).toEqual([ + { type: 'button', name: 'next' }, + ]) + }) + }) + + describe('when calendar is within a form', () => { + it('should not submit the form when clicking the button', (done) => { + let unloadCalled = false + let el = $('<div id="calendar"/>') + .wrap('<form action="https://google.com/"></form>') + .appendTo('body') + + function beforeUnloadHandler() { + console.log('when calendar is within a form, it submits!!!') // eslint-disable-line no-console + unloadCalled = true + cleanup() + return 'click stay on this page' + } + $(window).on('beforeunload', beforeUnloadHandler) + + function cleanup() { + el.remove() + $(window).off('beforeunload', beforeUnloadHandler) + } + + let calendar = initCalendar({ + headerToolbar: { + left: 'prev,next', + right: 'title', + }, + }, el) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + $(toolbarWrapper.getButtonEl('next')).simulate('click') + setTimeout(() => { // wait to see if handler was called + expect(unloadCalled).toBe(false) + cleanup() + done() + }, 100) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/height-and-contentHeight.ts b/fullcalendar-main/tests/src/legacy/height-and-contentHeight.ts new file mode 100644 index 0000000..d186625 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/height-and-contentHeight.ts @@ -0,0 +1,329 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import '../lib/dom-misc' + +['height', 'contentHeight'].forEach((heightProp) => { + describe(heightProp, () => { + let $calendarEl + let heightEl // HTMLElement + let asAMethod + let heightPropDescriptions: { description: string, height: string | number, heightWrapper?: boolean }[] = [ + { description: 'as a number', height: 600 }, + ] + + if (heightProp === 'height') { + heightPropDescriptions.push({ description: 'as "100%"', height: '100%', heightWrapper: true }) + } + + pushOptions({ + initialDate: '2014-08-01', + }) + + beforeEach(() => { + $calendarEl = $('<div />').appendTo('body').width(900) + }) + + afterEach(() => { + $calendarEl.remove() + }) + + // relies on asAMethod (boolean) + // otherOptions: other calendar options to dynamically set (assumes asAMethod) + function init(heightVal) { + let calendar + + if (asAMethod) { + calendar = initCalendar({}, $calendarEl[0]) + let calendarWrapper = new CalendarWrapper(calendar) + let dateEl = calendarWrapper.getFirstDateEl() + + calendar.setOption(heightProp, heightVal) + expect(calendarWrapper.getFirstDateEl()).toBe(dateEl) + } else { + calendar = initCalendar({ [heightProp]: heightVal }, $calendarEl[0]) + } + + if (heightProp === 'height') { + heightEl = calendar.el + } else { + heightEl = new CalendarWrapper(calendar).getViewEl() + } + + return calendar + } + + function expectHeight(heightVal) { + let diff = Math.abs(heightEl.offsetHeight - heightVal) + expect(diff).toBeLessThan(2) // off-by-one or exactly the same. for zoom, and firefox + } + + $.each({ + 'as an init option': false, + 'as a method': true, + }, (desc, bool) => { + describe(desc, () => { + beforeEach(() => { + asAMethod = bool + }) + + describe('for ' + heightProp, () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + heightPropDescriptions.forEach((testInfo) => { + describe(testInfo.description, () => { + if (testInfo.heightWrapper) { + beforeEach(() => { + $calendarEl.wrap('<div id="calendar-container" style="height: 600px;" />') + }) + afterEach(() => { + $('#calendar-container').remove() + }) + } + + describe('when there are no events', () => { + it('should be the specified height, with no scrollbars', () => { + let calendar = init(testInfo.height) + let viewWrapper = new DayGridViewWrapper(calendar) + let diff = Math.abs(heightEl.offsetHeight - 600) + + expect(diff).toBeLessThan(2) + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + + describe('when there is one tall row of events', () => { + pushOptions({ + events: repeatClone({ title: 'event', start: '2014-08-04' }, 9), + }) + + it('should take away height from other rows, but not do scrollbars', () => { + let calendar = init(testInfo.height) + let viewWrapper = new DayGridViewWrapper(calendar) + let $rows = $(viewWrapper.dayGrid.getRowEls()) + let $tallRow = $rows.eq(1) + let $shortRows = $rows.not($tallRow) // 0, 2, 3, 4, 5 + let shortHeight = $shortRows.eq(0).outerHeight() + + expectHeight(600) + + $shortRows.each((i, node) => { + let rowHeight = $(node).outerHeight() + let diff = Math.abs(rowHeight - shortHeight) + expect(diff).toBeLessThan(10) // all roughly the same + }) + + expect($tallRow.outerHeight()).toBeGreaterThan(shortHeight * 2) // much taller + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + + describe('when there are many tall rows of events', () => { + pushOptions({ + events: [].concat( + repeatClone({ title: 'event0', start: '2014-07-28' }, 9), + repeatClone({ title: 'event1', start: '2014-08-04' }, 9), + repeatClone({ title: 'event2', start: '2014-08-11' }, 9), + repeatClone({ title: 'event3', start: '2014-08-18' }, 9), + repeatClone({ title: 'event4', start: '2014-08-25' }, 9), + repeatClone({ title: 'event5', start: '2014-09-01' }, 9), + ), + }) + + it('height is correct and scrollbars show up', () => { + let calendar = init(testInfo.height) + let viewWrapper = new DayGridViewWrapper(calendar) + + expectHeight(600) + expect(viewWrapper.getScrollerEl()).toHaveScrollbars() + }) + }) + }) + }) + + describe('as "auto", when there are many tall rows of events', () => { + pushOptions({ + events: [].concat( + repeatClone({ title: 'event0', start: '2014-07-28' }, 9), + repeatClone({ title: 'event1', start: '2014-08-04' }, 9), + repeatClone({ title: 'event2', start: '2014-08-11' }, 9), + repeatClone({ title: 'event3', start: '2014-08-18' }, 9), + repeatClone({ title: 'event4', start: '2014-08-25' }, 9), + repeatClone({ title: 'event5', start: '2014-09-01' }, 9), + ), + }) + + it('height is really tall and there are no scrollbars', () => { + let calendar = init('auto') + let viewWrapper = new DayGridViewWrapper(calendar) + + expect(heightEl.offsetHeight).toBeGreaterThan(1000) // pretty tall + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + }); + + ['dayGridWeek', 'dayGridDay'].forEach((viewName) => { + describe('in ' + viewName + ' view', () => { + pushOptions({ + initialView: viewName, + }) + + heightPropDescriptions.forEach((testInfo) => { + describe(testInfo.description, () => { + if (testInfo.heightWrapper) { + beforeEach(() => { + $calendarEl.wrap('<div id="calendar-container" style="height: 600px;" />') + }) + afterEach(() => { + $('#calendar-container').remove() + }) + } + + describe('when there are no events', () => { + it('should be the specified height, with no scrollbars', () => { + let calendar = init(testInfo.height) + let viewWrapper = new DayGridViewWrapper(calendar) + + expectHeight(600) + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + + describe('when there are many events', () => { + pushOptions({ + events: repeatClone({ title: 'event', start: '2014-08-01' }, 100), + }) + + it('should have the correct height, with scrollbars', () => { + let calendar = init(testInfo.height) + let viewWrapper = new DayGridViewWrapper(calendar) + + expectHeight(600) + expect(viewWrapper.getScrollerEl()).toHaveScrollbars() + }) + }) + }) + }) + + describe('as "auto", when there are many events', () => { + pushOptions({ + events: repeatClone({ title: 'event', start: '2014-08-01' }, 100), + }) + it('should be really tall with no scrollbars', () => { + let calendar = init('auto') + let viewWrapper = new DayGridViewWrapper(calendar) + + expect(heightEl.offsetHeight).toBeGreaterThan(1000) // pretty tall + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + }) + }); + + ['timeGridWeek', 'timeGridDay'].forEach((viewName) => { + describe('in ' + viewName + ' view', () => { + pushOptions({ + initialView: viewName, + }) + + describeOptions({ + 'with no all-day section': { allDaySlot: false }, + 'with no all-day events': { }, + 'with some all-day events': { events: repeatClone({ title: 'event', start: '2014-08-01' }, 6) }, + }, () => { + heightPropDescriptions.forEach((testInfo) => { + describe(testInfo.description, () => { + if (testInfo.heightWrapper) { + beforeEach(() => { + $calendarEl.wrap('<div id="calendar-container" style="height: 600px;" />') + }) + afterEach(() => { + $('#calendar-container').remove() + }) + } + + describe('with many slots', () => { + pushOptions({ + slotMinTime: '00:00:00', + slotMaxTime: '24:00:00', + }) + it('should be the correct height, with scrollbars', () => { + let calendar = init(testInfo.height) + let viewWrapper = new TimeGridViewWrapper(calendar) + + expectHeight(600) + expect(viewWrapper.getScrollerEl()).toHaveScrollbars() + }) + }) + }) + }) + + describe('as "auto", with only a few slots', () => { + pushOptions({ + slotMinTime: '06:00:00', + slotMaxTime: '10:00:00', + }) + it('should be really short with no scrollbars nor horizontal rule', () => { + let calendar = init('auto') + let viewWrapper = new TimeGridViewWrapper(calendar) + + expect(heightEl.offsetHeight).toBeLessThan(500) // pretty short + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + + describe('as a "auto", with many slots', () => { + pushOptions({ + slotMinTime: '00:00:00', + slotMaxTime: '24:00:00', + }) + + it('should be really tall with no scrollbars nor horizontal rule', () => { + let calendar = init('auto') + let viewWrapper = new TimeGridViewWrapper(calendar) + + expect(heightEl.offsetHeight).toBeGreaterThan(900) // pretty tall + expect(viewWrapper.getScrollerEl()).not.toHaveScrollbars() + }) + }) + }) + }) + }) + }) + }) + }) + }) +}) + +it('no height oscillation happens', () => { + let $container = $( + '<div style="width:301px;height:300px;overflow-y:auto">' + + '<div style="margin:0"></div>' + + '</div>', + ).appendTo('body') + + // will freeze browser if bug exists :) + let calendar = initCalendar({ + headerToolbar: false, + initialView: 'dayGridMonth', + aspectRatio: 1, + }, $container.find('div')[0]) + + calendar.destroy() + $container.remove() +}) + +function repeatClone(srcObj, times) { + let a = [] + let i + + for (i = 0; i < times; i += 1) { + a.push($.extend({}, srcObj)) + } + + return a +} diff --git a/fullcalendar-main/tests/src/legacy/hiddenDays.ts b/fullcalendar-main/tests/src/legacy/hiddenDays.ts new file mode 100644 index 0000000..3aacea0 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/hiddenDays.ts @@ -0,0 +1,94 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('hiddenDays', () => { + const DOW_CLASSNAMES = CalendarWrapper.DOW_CLASSNAMES + + describe('when using default', () => { + it('should show 7 days of the week', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let daysCount = headerWrapper.getCellEls().length + expect(daysCount).toEqual(7) + }) + }) + + describe('when setting an empty hiddenDays', () => { + pushOptions({ + hiddenDays: [], + }) + + it('should return 7 days of the week', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let daysCount = headerWrapper.getCellEls().length + expect(daysCount).toEqual(7) + }) + }) + + describe('when setting hiddenDays with 1', () => { + pushOptions({ + hiddenDays: [1], + }) + + it('should return 6 days', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let daysCount = headerWrapper.getCellEls().length + expect(daysCount).toEqual(6) + }) + + it('should return sun,tue,wed,thu,fri,sat days', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let dowEls = headerWrapper.getCellEls() + expect(dowEls[0]).toHaveClass(DOW_CLASSNAMES[0]) + expect(dowEls[1]).toHaveClass(DOW_CLASSNAMES[2]) + expect(dowEls[2]).toHaveClass(DOW_CLASSNAMES[3]) + expect(dowEls[3]).toHaveClass(DOW_CLASSNAMES[4]) + expect(dowEls[4]).toHaveClass(DOW_CLASSNAMES[5]) + expect(dowEls[5]).toHaveClass(DOW_CLASSNAMES[6]) + }) + + it('should expect 7th day to be undefined', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let dowEls = headerWrapper.getCellEls() + expect(dowEls[6]).toBeUndefined() + }) + }) + + describe('when setting hiddenDays with 3,5', () => { + pushOptions({ + hiddenDays: [3, 5], + }) + + it('should return 6 days', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let daysCount = headerWrapper.getCellEls().length + expect(daysCount).toEqual(5) + }) + + it('should return s,m,t,t,s ', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + let dowEls = headerWrapper.getCellEls() + expect(dowEls[0]).toHaveClass(DOW_CLASSNAMES[0]) + expect(dowEls[1]).toHaveClass(DOW_CLASSNAMES[1]) + expect(dowEls[2]).toHaveClass(DOW_CLASSNAMES[2]) + expect(dowEls[3]).toHaveClass(DOW_CLASSNAMES[4]) + expect(dowEls[4]).toHaveClass(DOW_CLASSNAMES[6]) + }) + }) + + describe('when setting all hiddenDays', () => { + it('should expect to throw an exception', () => { + expect(() => { + initCalendar({ + hiddenDays: [0, 1, 2, 3, 4, 5, 6], + }) + }).toThrow(new Error('invalid hiddenDays')) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/locale.ts b/fullcalendar-main/tests/src/legacy/locale.ts new file mode 100644 index 0000000..0021b34 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/locale.ts @@ -0,0 +1,49 @@ +import esLocale from '@fullcalendar/core/locales/es' +import frLocale from '@fullcalendar/core/locales/fr' +import arLocale from '@fullcalendar/core/locales/ar' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('locale', () => { + pushOptions({ + locales: [esLocale, frLocale, arLocale], + }) + + it('works when certain locale has no FC settings defined', () => { + let calendar = initCalendar({ + locale: 'en-asdf', + initialView: 'timeGridWeek', + initialDate: '2014-12-25', + events: [ + { title: 'Christmas', start: '2014-12-25T10:00:00' }, + ], + }) + let headerWrapper = new TimeGridViewWrapper(calendar).header + + expect(headerWrapper.getCellText(0)).toMatch(/^Sun\.? 12[-/ ]21$/) + + let calendarWrapper = new CalendarWrapper(calendar) + let eventEl = calendarWrapper.getFirstEventEl() + let eventInfo = calendarWrapper.getEventElInfo(eventEl) + + expect(eventInfo.timeText).toBe('10:00') + }) + + it('allows dynamic setting', () => { + let calendar = initCalendar({ + locale: 'es', + initialDate: '2016-07-10', + initialView: 'dayGridMonth', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getTitleText()).toBe('julio de 2016') + expect(calendar.getOption('direction')).toBe('ltr') + + currentCalendar.setOption('locale', 'fr') + expect(toolbarWrapper.getTitleText()).toBe('juillet 2016') + + currentCalendar.setOption('locale', 'ar') // NOTE: we had problems testing for RTL title text + expect(calendar.getOption('direction')).toBe('rtl') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/maxTime.ts b/fullcalendar-main/tests/src/legacy/maxTime.ts new file mode 100644 index 0000000..0f82cfa --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/maxTime.ts @@ -0,0 +1,98 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotMaxTime', () => { // TODO: rename file + describe('when using the default settings', () => { + describeOptions('initialView', { + 'in week': 'timeGridWeek', + 'in day': 'timeGridDay', + }, () => { + it('should start at 12am', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let lastMajor = timeGridWrapper.getLastMajorAxisInfo() + expect(lastMajor.text).toEqual('11pm') + }) + }) + }) + + describe('when using a whole number', () => { + let hourNumbers = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] + + describe('in week', () => { + hourNumbers.forEach((hourNumber) => { + it('should end at ' + hourNumber, () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + slotMaxTime: { hours: hourNumber }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let lastMajor = timeGridWrapper.getLastMajorAxisInfo() + let expected = numToStringConverter(hourNumber - 1) + expect(lastMajor.text).toEqual(expected) + }) + }) + }) + + describe('in day', () => { + hourNumbers.forEach((hourNumber) => { + it('should end at ' + hourNumber, () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + slotMaxTime: hourNumber + ':00', // in addition, test string duration input + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let lastMajor = timeGridWrapper.getLastMajorAxisInfo() + let expected = numToStringConverter(hourNumber - 1) + expect(lastMajor.text).toEqual(expected) + }) + }) + }) + }) + + describe('when using default slotInterval and \'uneven\' slotMaxTime', () => { + let hourNumbers = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] + + describe('in week', () => { + hourNumbers.forEach((hourNumber) => { + it('should end at ' + hourNumber + ':20', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + slotMaxTime: { hours: hourNumber, minutes: 20 }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let lastMajor = timeGridWrapper.getLastMajorAxisInfo() + // since exclusive end is :20, last slot will be on the current hour's 00:00 + let expected = numToStringConverter(hourNumber) + expect(lastMajor.text).toEqual(expected) + }) + }) + }) + + describe('in day', () => { + hourNumbers.forEach((hourNumber) => { + it('should end at ' + hourNumber + ':20', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + slotMaxTime: { hours: hourNumber, minutes: 20 }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let lastMajor = timeGridWrapper.getLastMajorAxisInfo() + // since exclusive end is :20, last slot will be on the current hour's 00:00 + let expected = numToStringConverter(hourNumber) + expect(lastMajor.text).toEqual(expected) + }) + }) + }) + }) + + function numToStringConverter(timeIn) { + let time = (timeIn % 12) || 12 + let amPm = 'am' + if ((timeIn % 24) > 11) { + amPm = 'pm' + } + return time + amPm + } +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link-click.ts b/fullcalendar-main/tests/src/legacy/more-link-click.ts new file mode 100644 index 0000000..786e9ae --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link-click.ts @@ -0,0 +1,169 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('moreLinkClick', () => { + pushOptions({ + initialDate: '2014-08-01', // important that it is the first week, so works w/ month + week views + initialView: 'dayGridMonth', + dayMaxEventRows: 3, + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + + describe('when set to "popover"', () => { + pushOptions({ + moreLinkClick: 'popover', + }) + + it('renders a popover upon click', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).toBeVisible() + done() + }) + }) + + // more popover tests are done in *-popover.js + }) + + describe('when set to "week"', () => { + pushOptions({ + moreLinkClick: 'week', + }) + + it('should go to dayGridWeek if it is one of the available views', (done) => { + let calendar = initCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay', + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('dayGridWeek') + done() + }) + }) + + it('should go to week if it is one of the available views', (done) => { + let calendar = initCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay', + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('timeGridWeek') + done() + }) + }) + }) + + describe('when set to "day"', () => { + pushOptions({ + moreLinkClick: 'day', + }) + + it('should go to dayGridDay if it is one of the available views', (done) => { + let calendar = initCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay', + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('dayGridDay') + done() + }) + }) + + it('should go to day if it is one of the available views', (done) => { + let calendar = initCalendar({ + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay', + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('timeGridDay') + done() + }) + }) + }) + + it('works with an explicit view name', (done) => { + let calendar = initCalendar({ + moreLinkClick: 'timeGridWeek', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay', + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('timeGridWeek') + done() + }) + }) + + it('works with custom function and all the arguments are correct', (done) => { + let calendar = initCalendar({ + moreLinkClick(arg) { + expect(typeof arg).toBe('object') + expect(arg.date).toEqualDate('2014-07-29') + expect(arg.hiddenSegs.length).toBe(2) + expect(arg.allSegs.length).toBe(4) + expect(typeof arg.jsEvent).toBe('object') + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(done) + }) + + it('works with custom function, and can return a view name', (done) => { + let calendar = initCalendar({ + moreLinkClick() { + return 'timeGridDay' + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let view = currentCalendar.view + expect(view.type).toBe('timeGridDay') + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link-popover-destroy.ts b/fullcalendar-main/tests/src/legacy/more-link-popover-destroy.ts new file mode 100644 index 0000000..1598fe9 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link-popover-destroy.ts @@ -0,0 +1,86 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('more-link popover', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-08-01', + dayMaxEventRows: 3, + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30', className: 'event1' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31', className: 'event2' }, + { title: 'event3', start: '2014-07-29', className: 'event3' }, + { title: 'event4', start: '2014-07-29', className: 'event4' }, + ], + handleWindowResize: false, // because showing the popover causes scrollbars and fires resize + }) + + it('closes when user clicks the X and trigger eventWillUnmount for every render', (done) => { + let eventsRendered = {} + let renderCount = 0 + let activated = false + + let calendar = initCalendar({ + eventDidMount(arg) { + if (activated) { + eventsRendered[arg.event.title] = true + renderCount += 1 + } + }, + eventWillUnmount(arg) { + delete eventsRendered[arg.event.title] + renderCount -= 1 + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + // Activate flags and pop event limit popover + activated = true + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).toBeVisible() + + dayGridWrapper.closeMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).not.toBeVisible() + expect(Object.keys(eventsRendered).length).toEqual(0) + expect(renderCount).toEqual(0) + done() + }) + }) + }) + + it('closes when user clicks outside of the popover and trigger eventWillUnmount for every render', (done) => { + let eventsRendered = {} + let renderCount = 0 + let activated = false + + let calendar = initCalendar({ + eventDidMount(arg) { + if (activated) { + eventsRendered[arg.event.title] = true + renderCount += 1 + } + }, + eventWillUnmount(arg) { + delete eventsRendered[arg.event.title] + renderCount -= 1 + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + // Activate flags and pop event limit popover + activated = true + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).toBeVisible() + + $('body').simulate('mousedown').simulate('click') + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).not.toBeVisible() + expect(Object.keys(eventsRendered).length).toEqual(0) + expect(renderCount).toEqual(0) + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link-popover.ts b/fullcalendar-main/tests/src/legacy/more-link-popover.ts new file mode 100644 index 0000000..95fcf0d --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link-popover.ts @@ -0,0 +1,568 @@ +import { EventInput } from '@fullcalendar/core' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridWrapper } from '../lib/wrappers/DayGridWrapper.js' + +describe('more-link popover', () => { + let testEvents: EventInput[] = [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30', className: 'event1' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31', className: 'event2' }, + { title: 'event3', start: '2014-07-29', className: 'event3' }, + { title: 'event4', start: '2014-07-29', className: 'event4' }, + ] + + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-08-01', + dayMaxEventRows: 3, + events: testEvents, + dragScroll: false, // don't do autoscrolling while dragging. close quarters in PhantomJS + handleWindowResize: false, // because showing the popover causes scrollbars and fires resize + }) + + describeOptions('initialView', { + 'when in month view': 'dayGridMonth', + 'when in dayGridWeek view': 'dayGridWeek', + 'when in week view': 'timeGridWeek', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('aligns horizontally with left edge of cell if LTR', (done) => { + let calendar = initCalendar({ + direction: 'ltr', + }) + setTimeout(() => { + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let cellLeft = dayGridWrapper.getDayEl('2014-07-29').getBoundingClientRect().left + let popoverLeft = dayGridWrapper.getMorePopoverEl().getBoundingClientRect().left + let diff = Math.abs(cellLeft - popoverLeft) + expect(diff).toBeLessThan(2) + done() + }) + }) + }) + + it('aligns horizontally with left edge of cell if RTL', (done) => { + let calendar = initCalendar({ + direction: 'rtl', + }) + setTimeout(() => { + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let cellRight = dayGridWrapper.getDayEl('2014-07-29').getBoundingClientRect().right + let popoverRight = dayGridWrapper.getMorePopoverEl().getBoundingClientRect().right + let diff = Math.abs(cellRight - popoverRight) + expect(diff).toBeLessThan(2) + done() + }) + }) + }) + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('aligns with top of cell', (done) => { + let calendar = initCalendar() + setTimeout(() => { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let cellTop = dayGridWrapper.getDayEl('2014-07-29').getBoundingClientRect().top + let popoverTop = dayGridWrapper.getMorePopoverEl().getBoundingClientRect().top + let diff = Math.abs(cellTop - popoverTop) + expect(diff).toBeLessThan(2) + done() + }) + }) + }) + + it('works with background events', (done) => { + let calendar = initCalendar({ + events: testEvents.concat([ + { + start: '2014-07-29', + display: 'background', + }, + ]), + }) + setTimeout(() => { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEventCnt()).toBeGreaterThan(1) + expect(dayGridWrapper.getMorePopoverBgEventCnt()).toBe(0) + done() + }) + }) + }) + + it('works with events that have invalid end times', (done) => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29', end: '2014-07-29' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-28' }, + { title: 'event3', start: '2014-07-29T00:00:00', end: '2014-07-29T00:00:00' }, + { title: 'event4', start: '2014-07-29T00:00:00', end: '2014-07-28T23:00:00' }, + ], + }) + setTimeout(() => { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEventCnt()).toBe(4) + done() + }) + }) + }) + + // issue 2385 + it('orders events correctly regardless of ID', (done) => { + let calendar = initCalendar({ + initialDate: '2012-03-22', + dayMaxEventRows: 3, + events: [ + { + id: '39957', + title: 'event01', + start: '2012-03-22T11:00:00', + end: '2012-03-22T11:30:00', + allDay: false, + }, + { + id: '40607', + title: 'event02', + start: '2012-03-22T16:15:00', + end: '2012-03-22T16:30:00', + allDay: false, + }, + { + id: '40760', + title: 'event03', + start: '2012-03-22T16:00:00', + end: '2012-03-22T16:15:00', + allDay: false, + }, + { + id: '41284', + title: 'event04', + start: '2012-03-22T19:00:00', + end: '2012-03-22T19:15:00', + allDay: false, + }, + { + id: '41645', + title: 'event05', + start: '2012-03-22T11:30:00', + end: '2012-03-22T12:00:00', + allDay: false, + }, + { + id: '41679', + title: 'event07', + start: '2012-03-22T12:00:00', + end: '2012-03-22T12:15:00', + allDay: false, + }, + { + id: '42246', + title: 'event08', + start: '2012-03-22T16:45:00', + end: '2012-03-22T17:00:00', + allDay: false, + }, + ], + }) + setTimeout(() => { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let titles = dayGridWrapper.getMorePopoverEventTitles() + expect(titles).toEqual([ + 'event01', 'event05', 'event07', 'event03', 'event02', 'event08', 'event04', + ]) + done() + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/3856 + it('displays multi-day events only once', (done) => { + let calendar = initCalendar({ + initialDate: '2017-10-04', + events: [ + { + title: 'Long event', + className: 'long-event', + start: '2017-10-03', + end: '2017-10-20', + }, + { + title: 'Meeting', + className: 'meeting-event', + start: '2017-10-04T10:00:00', + end: '2017-10-04T12:00:00', + }, + { + title: 'Lunch 1', + className: 'lunch1-event', + start: '2017-10-04T12:00:00', + }, + { + title: 'Lunch 2', + className: 'lunch2-event', + start: '2017-10-04T14:00:00', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let popoverEl = dayGridWrapper.getMorePopoverEl() + let eventEls = dayGridWrapper.getMorePopoverEventEls() + + expect(eventEls.length).toBe(4) + + let $longEventEl = $('.long-event', popoverEl) + let $meetingEventEl = $('.meeting-event', popoverEl) + let $lunch1EventEl = $('.lunch1-event', popoverEl) + let $lunch2EventEl = $('.lunch2-event', popoverEl) + + expect($longEventEl).not.toHaveClass(CalendarWrapper.EVENT_IS_START_CLASSNAME) + expect($longEventEl).not.toHaveClass(CalendarWrapper.EVENT_IS_END_CLASSNAME); + + [$meetingEventEl, $lunch1EventEl, $lunch2EventEl].forEach(($el) => { + expect($el).toHaveClass(CalendarWrapper.EVENT_IS_START_CLASSNAME) + expect($el).toHaveClass(CalendarWrapper.EVENT_IS_END_CLASSNAME) + }) + + done() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4331 + it('displays events that were collapsed in previous days', (done) => { + let calendar = initCalendar({ + initialDate: '2018-10-01', + events: [ + { + title: 'e1', + start: '2018-10-18', + }, + { + title: 'e2', + start: '2018-10-18', + }, + { + title: 'e3', + start: '2018-10-18T11:00:00', + }, + { + title: 'e4', + start: '2018-10-18T12:00:00', + end: '2018-10-19T12:00:00', + }, + { + title: 'e5', + start: '2018-10-19', + className: 'event-e5', + }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + dayGridWrapper.openMorePopover(1) // click the second +more link + setTimeout(done) + }) + }) + + describeOptions('initialView', { + 'when in dayGridWeek view': 'dayGridWeek', + 'when in week view': 'timeGridWeek', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('aligns with top of header', (done) => { + let calendar = initCalendar() + let viewWrapper = new ViewWrapper(calendar) + let dayGridWrapper = viewWrapper.dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let popoverTop = dayGridWrapper.getMorePopoverEl().getBoundingClientRect().top + let headTop = viewWrapper.header.el.getBoundingClientRect().top + let diff = Math.abs(popoverTop - headTop) + expect(diff).toBeLessThan(2) + done() + }) + }) + }) + + // TODO: somehow test how the popover does to the edge of any scroll container + + it('closes when user clicks the X', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).toBeVisible() + + dayGridWrapper.closeMorePopover() + setTimeout(() => { + expect(dayGridWrapper.getMorePopoverEl()).not.toBeVisible() + done() + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4584 + it('doesn\'t fire a dateClick', (done) => { + let dateClickCalled = false + + spyOnCalendarCallback('dateClick', () => { + dateClickCalled = true + }) + + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + $.simulateMouseClick(dayGridWrapper.getMorePopoverHeaderEl()) + setTimeout(() => { // because click would take some time to register + expect(dateClickCalled).toBe(false) + done() + }, 500) + }) + }) + + it('doesn\'t close when user clicks somewhere inside of the popover', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let popoverEl = dayGridWrapper.getMorePopoverEl() + let popoverHeaderEl = dayGridWrapper.getMorePopoverHeaderEl() + + expect(popoverEl).toBeVisible() + expect(popoverHeaderEl).toBeInDOM() + + $(popoverHeaderEl).simulate('mousedown').simulate('click') + setTimeout(() => { + expect(popoverEl).toBeVisible() + done() + }) + }) + }) + + it('closes when user clicks outside of the popover', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + let popoverEl = dayGridWrapper.getMorePopoverEl() + expect(popoverEl).toBeVisible() + + $('body').simulate('mousedown').simulate('click') + setTimeout(() => { + expect(popoverEl).not.toBeVisible() + done() + }) + }) + }) + + describe('when dragging events out', () => { + pushOptions({ + editable: true, + }) + + describe('when dragging an all-day event to a different day', () => { + it('should have the new day and remain all-day', (done) => { + let calendar = initCalendar({ + eventDrop(arg) { + expect(arg.event.start).toEqualDate('2014-07-28') + expect(arg.event.allDay).toBe(true) + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { // simulate was getting confused about which thing was being clicked :( + $('.event4', dayGridWrapper.getMorePopoverEl()).simulate('drag', { + end: dayGridWrapper.getDayEl('2014-07-28'), + }) + }, 0) + }) + }) + + describe('when dragging a timed event to a whole day', () => { + it('should move to new day but maintain its time', (done) => { + let calendar = initCalendar({ + events: testEvents.concat([ + { + title: 'event5', + start: '2014-07-29T13:00:00', + className: 'event5', + }, + ]), + eventDrop(arg) { + expect(arg.event.start).toEqualDate('2014-07-28T13:00:00Z') + expect(arg.event.allDay).toBe(false) + done() + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { // simulate was getting confused about which thing was being clicked :( + $('.event5', dayGridWrapper.getMorePopoverEl()).simulate('drag', { + end: dayGridWrapper.getDayEl('2014-07-28T13:00:00'), + }) + }, 0) + }) + }) + + describe('when dragging a whole day event to a timed slot', () => { + it('should assume the new time, with a cleared end', (done) => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + scrollTime: '00:00:00', + eventDrop(arg) { + expect(arg.event.start).toEqualDate('2014-07-30T03:00:00Z') + expect(arg.event.allDay).toBe(false) + done() + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let dayGridWrapper = viewWrapper.dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { // simulate was getting confused about which thing was being clicked :( + $('.event4', dayGridWrapper.getMorePopoverEl()).simulate('drag', { + localPoint: { left: '0%', top: '50%' }, // leftmost is guaranteed to be over the 30th + end: viewWrapper.timeGrid.getPoint('2014-07-30T03:00:00'), + }) + }, 0) + }) + }) + + describe('when a single-day event isn\'t dragged out all the way', () => { + it('shouldn\'t do anything', (done) => { + let dayGridWrapper + let calendar = initCalendar({ + eventDragStop() { + setTimeout(() => { // try to wait until drag is over. eventMutation won't fire BTW + expect(dayGridWrapper.getMorePopoverEl()).toBeInDOM() + done() + }, 0) + }, + }) + let viewWrapper = new DayGridViewWrapper(calendar) + dayGridWrapper = viewWrapper.dayGrid + dayGridWrapper.openMorePopover() + + setTimeout(() => { // simulate was getting confused about which thing was being clicked :( + $('.event1', dayGridWrapper.getMorePopoverEl()).simulate('drag', { + localPoint: { left: '0%', top: '50%' }, // leftmost is guaranteed to be over the 30th + dx: 20, + }) + }, 0) + }) + }) + }) + + it('calls event render handlers', (done) => { + let options = { + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30', className: 'event1' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31', className: 'event2' }, + { title: 'event3', start: '2014-07-29', className: 'event3' }, + { title: 'event4', start: '2014-07-29', className: 'event4' }, + ], + eventDidMount() {}, + eventContent() {}, + eventWillUnmount() {}, + } + + spyOn(options, 'eventDidMount') + spyOn(options, 'eventContent') + spyOn(options, 'eventWillUnmount') + + function resetCounts() { + options.eventDidMount.calls.reset() + options.eventContent.calls.reset() + options.eventWillUnmount.calls.reset() + } + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(options.eventDidMount.calls.count()).toBe(4) + expect(options.eventContent.calls.count()).toBe(4) + expect(options.eventWillUnmount.calls.count()).toBe(0) + + resetCounts() + dayGridWrapper.openMorePopover() + setTimeout(() => { + expect(options.eventDidMount.calls.count()).toBe(4) + expect(options.eventContent.calls.count()).toBe(4) + expect(options.eventWillUnmount.calls.count()).toBe(0) + + resetCounts() + dayGridWrapper.closeMorePopover() + setTimeout(() => { + expect(options.eventDidMount.calls.count()).toBe(0) + expect(options.eventContent.calls.count()).toBe(0) + expect(options.eventWillUnmount.calls.count()).toBe(4) + + done() + }) + }) + }) + + it('displays latest events after refetch', (done) => { + let fetchCnt = 0 + let newTitle = 'cool' + let calendar = initCalendar({ + events(info, callback) { + fetchCnt += 1 + if (fetchCnt === 1) { + callback(testEvents) + } else { + callback(testEvents.slice(0, -1).concat([ + { + ...testEvents[testEvents.length - 1], + title: newTitle, + }, + ])) + } + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.openMorePopover() + setTimeout(() => { + calendar.refetchEvents() + let eventEls = dayGridWrapper.getMorePopoverEventEls() + let eventInfo = DayGridWrapper.getEventElInfo(eventEls[2]) + expect(eventInfo.title).toBe(newTitle) + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link-text.ts b/fullcalendar-main/tests/src/legacy/more-link-text.ts new file mode 100644 index 0000000..62a7b4d --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link-text.ts @@ -0,0 +1,52 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('moreLinkText', () => { + pushOptions({ + initialDate: '2014-08-01', // important that it is the first week, so works w/ month + week views + initialView: 'dayGridMonth', + dayMaxEventRows: 3, + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + + it('allows a string', () => { + let calendar = initCalendar({ + moreLinkText: 'extra', + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEl()).toHaveText('+2 extra') + }) + + it('allows a function', () => { + let calendar = initCalendar({ + moreLinkText(n) { + expect(typeof n).toBe('number') + return 'there are ' + n + ' more events!' + }, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEl()).toHaveText('there are 2 more events!') + }) + + it('has a default value that is affected by the custom locale', () => { + let calendar = initCalendar({ + locale: frLocale, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEl()).toHaveText('+2 en plus') + }) + + it('is not affected by a custom locale when the value is explicitly specified', () => { + let calendar = initCalendar({ + locale: frLocale, + moreLinkText: 'extra', + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEl()).toHaveText('+2 extra') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link-timegrid.ts b/fullcalendar-main/tests/src/legacy/more-link-timegrid.ts new file mode 100644 index 0000000..7b77fbf --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link-timegrid.ts @@ -0,0 +1,101 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { TimeGridWrapper } from '../lib/wrappers/TimeGridWrapper.js' + +describe('eventMaxStack', () => { + pushOptions({ + initialView: 'timeGridDay', + initialDate: '2021-05-07', + scrollTime: 0, + eventMaxStack: 2, + }) + + it('puts hidden events in a popover', (done) => { + let calendar = initCalendar({ + events: [ + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, // hidden + ], + }) + let timeGrid = new TimeGridViewWrapper(calendar).timeGrid + let moreLinkEls = timeGrid.getMoreEls() + expect(moreLinkEls.length).toBe(1) + + timeGrid.openMorePopover() + setTimeout(() => { + let moreEventEls = timeGrid.getMorePopoverEventEls() + expect(moreEventEls.length).toBe(1) + done() + }) + }) + + it('can drag events out of popover', (done) => { + let calendar = initCalendar({ + editable: true, + events: [ + { id: '1', start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { id: '2', start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { id: '3', start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, // hidden + ], + }) + let timeGrid = new TimeGridViewWrapper(calendar).timeGrid + timeGrid.openMorePopover() + setTimeout(() => { + let moreEventEls = timeGrid.getMorePopoverEventEls() + let newStart = '2021-05-07T02:00:00' + $(moreEventEls).simulate('drag', { + end: timeGrid.getPoint(newStart), + onRelease() { + let event = calendar.getEventById('3') + expect(event.start).toEqualDate(newStart) + done() + }, + }) + }) + }) + + it('causes separate adjacent more links', () => { + let calendar = initCalendar({ + events: [ + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, + { start: '2021-05-07T00:00:00', end: '2021-05-07T01:00:00' }, // hidden + { start: '2021-05-07T01:00:00', end: '2021-05-07T02:00:00' }, + { start: '2021-05-07T01:00:00', end: '2021-05-07T02:00:00' }, + { start: '2021-05-07T01:00:00', end: '2021-05-07T02:00:00' }, // hidden + ], + }) + let timeGrid = new TimeGridViewWrapper(calendar).timeGrid + let moreLinkEls = timeGrid.getMoreEls() + expect(moreLinkEls.length).toBe(2) + }) + + it('puts overlapping hidden events in same popover, respecting eventOrder', (done) => { + let calendar = initCalendar({ + eventOrder: 'title', + events: [ + { title: '1', start: '2021-05-07T00:00:00', end: '2021-05-07T02:00:00' }, + { title: '2', start: '2021-05-07T00:00:00', end: '2021-05-07T02:00:00' }, + { title: '3', start: '2021-05-07T01:00:00', end: '2021-05-07T03:00:00' }, // hidden + { title: '4', start: '2021-05-07T00:30:00', end: '2021-05-07T02:30:00' }, // hidden + ], + }) + let timeGrid = new TimeGridViewWrapper(calendar).timeGrid + let moreLinkEls = timeGrid.getMoreEls() + expect(moreLinkEls.length).toBe(1) + + const canvasCoords = timeGrid.el.getBoundingClientRect() + const moreLinkCoords = moreLinkEls[0].getBoundingClientRect() + const moreLinkTop = moreLinkCoords.top - canvasCoords.top + // TODO: more precise coord matching + expect(moreLinkTop).toBeGreaterThan(10) + + timeGrid.openMorePopover() + setTimeout(() => { + let moreEventEls = timeGrid.getMorePopoverEventEls() + expect(moreEventEls.length).toBe(2) + expect(TimeGridWrapper.getEventElInfo(moreEventEls[0]).title).toBe('3') + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/more-link.ts b/fullcalendar-main/tests/src/legacy/more-link.ts new file mode 100644 index 0000000..86d8502 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/more-link.ts @@ -0,0 +1,247 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { filterVisibleEls } from '../lib/dom-misc.js' + +describe('dayMaxEventRows', () => { + pushOptions({ + initialDate: '2014-08-01', // important that it is the first week, so works w/ month + week views + dayMaxEventRows: 3, + }) + + describe('as a number', () => { + describeOptions('initialView', { + 'when in month view': 'dayGridMonth', + 'when in dayGridWeek view': 'dayGridWeek', + 'when in week view': 'timeGridWeek', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('doesn\'t display a more link when limit is more than the # of events', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEls().length).toBe(0) + }) + + it('doesn\'t display a more link when limit equal to the # of events', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEls().length).toBe(0) + }) + + it('displays a more link when limit is less than the # of events', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let moreEls = dayGridWrapper.getMoreEls() + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+2 more') + }) + + it('displays one more per day, when a multi-day event is above', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let moreEls = dayGridWrapper.getMoreEls() + let cells = dayGridWrapper.getDayElsInRow(0) + expect(moreEls.length).toBe(2) + expect(moreEls[0]).toHaveText('+2 more') + expect(moreEls[0]).toBeBoundedBy(cells[2]) + expect(moreEls[1]).toHaveText('+2 more') + expect(moreEls[1]).toBeBoundedBy(cells[3]) + }) + + it('will render a pertially hidden single-day event', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event2', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event3', start: '2014-07-29', end: '2014-07-31' }, + { title: 'event4', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + let moreEls = dayGridWrapper.getMoreEls() + let cells = dayGridWrapper.getAllDayEls() + expect(visibleEventEls.length).toBe(3) + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+2 more') + expect(moreEls[0]).toBeBoundedBy(cells[2]) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/6187 + it('will render a partially multi-day hidden event', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event2', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event3', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event4', start: '2014-07-29', end: '2014-07-31' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let eventEls = dayGridWrapper.getEventEls() + let visibleEventEls = filterVisibleEls(eventEls) + let moreEls = dayGridWrapper.getMoreEls() + let cells = dayGridWrapper.getDayElsInRow(0) + expect(visibleEventEls.length).toBe(4) + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+2 more') + expect(moreEls[0]).toBeBoundedBy(cells[2]) + }) + + it('will render a link in place of a hidden single day event, if covered by a multi-day', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event2', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event3', start: '2014-07-28' }, + { title: 'event4', start: '2014-07-28' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let cells = dayGridWrapper.getDayElsInRow(0) + let moreEls = dayGridWrapper.getMoreEls() + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+2 more') + expect(moreEls[0]).toBeBoundedBy(cells[1]) + }) + + it('will render a link in place of a hidden single day event, if covered by a multi-day ' + + 'and in its second column', + () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event2', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event3', start: '2014-07-29' }, + { title: 'event4', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new ViewWrapper(calendar).dayGrid + let cells = dayGridWrapper.getDayElsInRow(0) + let moreEls = dayGridWrapper.getMoreEls() + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+2 more') + expect(moreEls[0]).toBeBoundedBy(cells[2]) + }) + }) + }) + + describe('when auto', () => { + pushOptions({ + dayMaxEvents: true, + }) + + describe('in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event2', start: '2014-07-28', end: '2014-07-30' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + + it('renders the heights of all the rows the same, regardless of # of events', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let rowEls = dayGridWrapper.getRowEls() + expect(rowEls.length).toBeGreaterThan(0) + + let rowHeights = rowEls.map((rowEl) => rowEl.getBoundingClientRect().height) + let totalHeight = rowHeights.reduce((prev, current) => prev + current, 0) + let aveHeight = totalHeight / rowHeights.length + + rowHeights.forEach((rowHeight) => { + let diff = Math.abs(rowHeight - aveHeight) + expect(diff).toBeLessThan(2) + }) + }) + + it('renders a more link when there are obviously too many events', () => { + let $el = $('<div id="calendar">').appendTo('body').width(800) + let calendar = initCalendar({}, $el) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + let moreEls = dayGridWrapper.getMoreEls() + expect(moreEls.length).toBe(1) + }) + }) + + describeOptions('initialView', { + 'when in month view': 'dayGridMonth', + 'when in dayGridWeek view': 'dayGridWeek', + }, () => { + it('doesn\'t render a more link where there should obviously not be a limit', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-28', end: '2014-07-30' }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getMoreEls().length).toBe(0) + }) + }) + + describe('in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('behaves as if limit is 5', () => { + let calendar = initCalendar({ + events: [ + { title: 'event1', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + { title: 'event2', start: '2014-07-29' }, + ], + }) + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + let eventEls = filterVisibleEls(dayGridWrapper.getEventEls()) + let moreEls = dayGridWrapper.getMoreEls() + + expect(eventEls.length).toBe(4) + expect(moreEls.length).toBe(1) + expect(moreEls[0]).toHaveText('+3 more') + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/navLinks.ts b/fullcalendar-main/tests/src/legacy/navLinks.ts new file mode 100644 index 0000000..a1a2829 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/navLinks.ts @@ -0,0 +1,208 @@ +import { addDays } from '@fullcalendar/core/internal' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { ListViewWrapper } from '../lib/wrappers/ListViewWrapper.js' + +describe('navLinks', () => { + pushOptions({ + now: '2016-08-20', + navLinks: true, + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek', // affects which view is jumped to by default + }, + }) + + describeTimeZones((tz) => { + describe('in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('moves to day', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.clickNavLink('2016-08-09') + expectDayView(calendar, 'timeGridDay', tz.parseDate('2016-08-09')) + expect(dateClickSpy).not.toHaveBeenCalled() + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4619 + it('moves to day when no toolbars', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar({ + headerToolbar: null, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.clickNavLink('2016-08-09') + expectDayView(calendar, 'dayGridDay', tz.parseDate('2016-08-09')) // is hash-key order-dependent I think :( + expect(dateClickSpy).not.toHaveBeenCalled() + }) + + // https://github.com/fullcalendar/fullcalendar/issues/3869 + it('moves to two different days', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + dayGridWrapper.clickNavLink('2016-08-09') + expectDayView(calendar, 'timeGridDay', tz.parseDate('2016-08-09')) + expect(dateClickSpy).not.toHaveBeenCalled() + + calendar.changeView('dayGridMonth') + let dayGridWrapper2 = new DayGridViewWrapper(calendar).dayGrid + dayGridWrapper2.clickNavLink('2016-08-10') + expectDayView(calendar, 'timeGridDay', tz.parseDate('2016-08-10')) + }) + + it('moves to day specifically', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar({ + navLinkDayClick: 'day', + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.clickNavLink('2016-08-09') + expectDayView(calendar, 'timeGridDay', tz.parseDate('2016-08-09')) + expect(dateClickSpy).not.toHaveBeenCalled() + }) + + it('moves to dayGridDay specifically', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar({ + navLinkDayClick: 'dayGridDay', + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.clickNavLink('2016-08-09') + expectDayView(calendar, 'dayGridDay', tz.parseDate('2016-08-09')) + expect(dateClickSpy).not.toHaveBeenCalled() + }) + + it('executes a custom handler', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let navLinkDayClickSpy = spyOnCalendarCallback('navLinkDayClick', (date, ev) => { + expect(date).toEqualDate(tz.parseDate('2016-08-09')) + expect(typeof ev).toBe('object') + }) + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.clickNavLink('2016-08-09') + expect(dateClickSpy).not.toHaveBeenCalled() + expect(navLinkDayClickSpy).toHaveBeenCalled() + }) + + describe('with weekNumbers', () => { + pushOptions({ + weekNumbers: true, + }) + + it('moves to week', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $.simulateMouseClick(dayGridWrapper.getWeekNavLinkEls()[1]) + expectWeekView(calendar, 'timeGridWeek', tz.parseDate('2016-08-07')) + expect(dateClickSpy).not.toHaveBeenCalled() + }) + }) + + it('does not have clickable day header', () => { + let calendar = initCalendar() + let headerWrapper = new DayGridViewWrapper(calendar).header + + expect(headerWrapper.getNavLinkEls().length).toBe(0) + }) + }) + }) + + describe('in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('moves to day view', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + let headerWrapper = new TimeGridViewWrapper(calendar).header + + headerWrapper.clickNavLink('2016-08-15') + expectDayView(calendar, 'timeGridDay', '2016-08-15') + expect(dateClickSpy).not.toHaveBeenCalled() + }) + }) + + describe('in listWeek', () => { + pushOptions({ + initialView: 'listWeek', + events: [ + { + title: 'event 1', + start: '2016-08-20', + }, + ], + }) + + it('moves to day view', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar() + let listWrapper = new ListViewWrapper(calendar) + + listWrapper.clickNavLink('2016-08-20') + expectDayView(calendar, 'timeGridDay', '2016-08-20') + expect(dateClickSpy).not.toHaveBeenCalled() + }) + }) + + describe('in day view', () => { + pushOptions({ + initialView: 'timeGridDay', + }) + + it('moves to week view', () => { + let dateClickSpy = spyOnCalendarCallback('dateClick') + let calendar = initCalendar({ + weekNumbers: true, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + + $.simulateMouseClick(viewWrapper.getHeaderWeekNumberLink()) + expectWeekView(calendar, 'timeGridWeek', '2016-08-14') + expect(dateClickSpy).not.toHaveBeenCalled() + }) + + it('does not have a clickable day header', () => { + let calendar = initCalendar() + let headerWrapper = new TimeGridViewWrapper(calendar).header + + expect(headerWrapper.getNavLinkEls().length).toBe(0) + }) + }) + + function expectDayView(calendar, viewName, dayDate) { + let calendarWrapper = new CalendarWrapper(calendar) + let start = calendar.view.activeStart + let end = calendar.view.activeEnd + + expect(calendarWrapper.getViewName()).toBe(viewName) + expect(start).toEqualDate(dayDate) + expect(addDays(end, -1)).toEqualDate(dayDate) + } + + function expectWeekView(calendar, viewName, firstDayDate) { + let calendarWrapper = new CalendarWrapper(calendar) + let start = calendar.view.activeStart + let end = calendar.view.activeEnd + + expect(calendarWrapper.getViewName()).toBe(viewName) + expect(start).toEqualDate(firstDayDate) + expect(addDays(end, -7)).toEqualDate(firstDayDate) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/nextDayThreshold.ts b/fullcalendar-main/tests/src/legacy/nextDayThreshold.ts new file mode 100644 index 0000000..5afaee1 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/nextDayThreshold.ts @@ -0,0 +1,85 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('nextDayThreshold', () => { + // when a view object exposes its nextDayThreshold value (after some refactoring)... + // TODO: detect the default of 9am + // TODO: detect 2 or more different types of Duration-ish parsing + + it('renders an event before the threshold', () => { + let calendar = initCalendar({ + nextDayThreshold: '10:00:00', + initialDate: '2014-06', + initialView: 'dayGridMonth', + events: [ + { + title: 'event1', + start: '2014-06-08T22:00:00', + end: '2014-06-10T09:00:00', + }, + ], + }) + expect(renderedDayCount(calendar)).toBe(2) + }) + + it('renders an event equal to the threshold', () => { + let calendar = initCalendar({ + nextDayThreshold: '10:00:00', + initialDate: '2014-06', + initialView: 'dayGridMonth', + events: [ + { + title: 'event1', + start: '2014-06-08T22:00:00', + end: '2014-06-10T10:00:00', + }, + ], + }) + expect(renderedDayCount(calendar)).toBe(3) + }) + + it('renders an event after the threshold', () => { + let calendar = initCalendar({ + nextDayThreshold: '10:00:00', + initialDate: '2014-06', + initialView: 'dayGridMonth', + events: [ + { + title: 'event1', + start: '2014-06-08T22:00:00', + end: '2014-06-10T11:00:00', + }, + ], + }) + expect(renderedDayCount(calendar)).toBe(3) + }) + + it('won\'t render an event that ends before the first day\'s threshold', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-10-01', + nextDayThreshold: '09:00:00', + events: [{ + start: '2017-09-30T08:00:00', + end: '2017-10-01T08:00:00', + }], + }) + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getEventEls().length).toBe(0) + }) + + function renderedDayCount(calendar) { // assumes only one event on the calendar + let headerWrapper = new DayGridViewWrapper(calendar).header + let dayEl = headerWrapper.getCellEl(0) + let cellWidth = $(dayEl).outerWidth() // works with dayGrid and timeGrid + let totalWidth = 0 + + let eventEls = new CalendarWrapper(calendar).getEventEls() + $(eventEls).each((i, eventEl) => { + totalWidth += $(eventEl).outerWidth() + }) + + return Math.round(totalWidth / cellWidth) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/now.ts b/fullcalendar-main/tests/src/legacy/now.ts new file mode 100644 index 0000000..78c8fe2 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/now.ts @@ -0,0 +1,61 @@ +import { parseUtcDate } from '../lib/date-parsing.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('now', () => { + pushOptions({ + initialDate: '2014-05-01', + }) + + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('changes the highlighted day when customized', () => { + let calendar = initCalendar({ + now: '2014-05-06', + }) + expectRenderedTodayDate(calendar, '2014-05-06') + }) + }) + + describe('when week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('changes the highlighted day when customized', () => { + let calendar = initCalendar({ + now: '2014-04-29T12:00:00', + }) + expectRenderedTodayDate(calendar, '2014-04-29') + }) + }) + + it('accepts a function that returns a Date', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now() { + return parseUtcDate('2014-05-01') + }, + }) + expectRenderedTodayDate(calendar, '2014-05-01') + }) + + it('accepts a function that returns a date string', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + now() { + return '2014-05-01' + }, + }) + expectRenderedTodayDate(calendar, '2014-05-01') + }) + + function expectRenderedTodayDate(calendar, expectedDate) { + let calendarWrapper = new CalendarWrapper(calendar) + let todayCell = calendarWrapper.getTodayEls()[0] + let todayDate = todayCell.getAttribute('data-date') + expect(todayDate).toEqual(expectedDate) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/nowIndicator.ts b/fullcalendar-main/tests/src/legacy/nowIndicator.ts new file mode 100644 index 0000000..522dc0c --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/nowIndicator.ts @@ -0,0 +1,81 @@ +import { getBoundingRect } from '../lib/dom-geom.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('now indicator', () => { + pushOptions({ + now: '2015-12-26T06:00:00', + scrollTime: '00:00', + initialView: 'timeGridWeek', + }) + + it('doesn\'t render by default', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.hasNowIndicator()).toBe(false) + }) + + describe('when activated', () => { + pushOptions({ + nowIndicator: true, + }) + + describeOptions('direction', { + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, () => { + it('doesn\'t render when out of view', () => { + let calendar = initCalendar({ + initialDate: '2015-12-27', // sun of next week + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.hasNowIndicator()).toBe(false) + }) + + it('renders on correct time', () => { + let calendar = initCalendar() + isNowIndicatorRenderedAt(calendar, '2015-12-26T06:00:00Z') + }) + + it('renders on correct time2', () => { + let calendar = initCalendar({ + now: '2015-12-20T02:30:00', + }) + isNowIndicatorRenderedAt(calendar, '2015-12-20T02:30:00Z') + }) + }) + }) + + function isNowIndicatorRenderedAt(calendar, date) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let line = timeGridWrapper.getLine(date) + let lineEl = timeGridWrapper.getNowIndicatorLineEl() + let arrowEl = timeGridWrapper.getNowIndicatorArrowEl() + + expect(lineEl).toBeTruthy() + expect(arrowEl).toBeTruthy() + + let lineElRect = getBoundingRect(lineEl) + let arrowElRect = getBoundingRect(arrowEl) + + expect(Math.abs( + (lineElRect.top + lineElRect.bottom) / 2 - + line.top, + )).toBeLessThan(2) + expect(Math.abs( + (arrowElRect.top + arrowElRect.bottom) / 2 - + line.top, + )).toBeLessThan(2) + + let timeGridRect = getBoundingRect(timeGridWrapper.el) + + if (calendar.getOption('direction') === 'rtl') { + expect(Math.abs( + arrowElRect.right - timeGridRect.right, + )).toBeLessThan(2) + } else { + expect(Math.abs( + arrowElRect.left - timeGridRect.left, + )).toBeLessThan(2) + } + } +}) diff --git a/fullcalendar-main/tests/src/legacy/overlap.ts b/fullcalendar-main/tests/src/legacy/overlap.ts new file mode 100644 index 0000000..74f20d1 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/overlap.ts @@ -0,0 +1,804 @@ +import { testEventDrag, testEventResize, testSelection } from '../lib/dnd-resize-utils.js' + +describe('event overlap', () => { + let options + + beforeEach(() => { + options = { + initialDate: '2014-11-04', + initialView: 'timeGridWeek', + scrollTime: '00:00', + } + }) + + describe('when other event overlap is false', () => { + describe('when dragged adjacently before the other event', () => { + describe('when subject event\'s end is explicit', () => { + it('allows dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T03:00:00', true, done, 'event-a') + }) + }) + describe('when subject event\'s end is implied', () => { + it('allows dragging', (done) => { + options.defaultTimedEventDuration = '01:30' + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T03:30:00', true, done, 'event-a') + }) + }) + }) + + describe('when dragged adjacently after the other event', () => { + it('allows dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T09:00:00', true, done, 'event-a') + }) + }) + + describe('when dragged intersecting the other event\'s start', () => { + describe('when no timezone', () => { + describe('when subject event\'s end is explicit', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T04:00:00', false, done, 'event-a') + }) + }) + describe('when subject event\'s end is implied', () => { + it('does not allow dragging', (done) => { + options.defaultTimedEventDuration = '03:00' + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T03:00:00', false, done, 'event-a') + }) + }) + }) + describe('when UTC timezone', () => { + it('does not allow dragging', (done) => { + options.timeZone = 'UTC' + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00+00:00', + end: '2014-11-04T03:00:00+00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00+00:00', + end: '2014-11-04T09:00:00+00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T04:00:00+00:00', false, done, 'event-a') + }) + }) + }) + + describe('when dragged intersecting the other event\'s end', () => { + describe('when in week view with timed events', () => { + describe('when no timezone', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T08:00:00', false, done, 'event-a') + }) + }) + describe('when UTC timezone', () => { + it('does not allow dragging', (done) => { + options.timeZone = 'UTC' + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00+00:00', + end: '2014-11-04T03:00:00+00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00+00:00', + end: '2014-11-04T09:00:00+00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T08:00:00+00:00', false, done, 'event-a') + }) + }) + }) + describe('when in month view', () => { + beforeEach(() => { + options.initialView = 'dayGridMonth' + }) + describe('with all-day subject and all-day other', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04', + end: '2014-11-05', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-07', + end: '2014-11-09', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-08', false, done, 'event-a') + }) + }) + describe('with all-day subject and timed other', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04', + end: '2014-11-05', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-07T05:00:00', + end: '2014-11-09T12:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-08', false, done, 'event-a') + }) + }) + describe('with timed subject and all-day other', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-07T05:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04', false, done, 'event-b') + }) + }) + }) + }) + + describe('when dragged to be encompassed by the other event', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T06:00:00', false, done, 'event-a') + }) + describe('when both events have the same group ID', () => { + it('allows the drag', (done) => { + options.events = [ + { + groupId: 'myid', + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + groupId: 'myid', + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventDrag(options, '2014-11-04T06:00:00', true, done, 'event-a') + }) + }) + }) + + describe('when resized to be adjacently before the other event', () => { + it('allows resizing', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventResize(options, '2014-11-04T05:00:00', true, done, 'event-a') + }) + }) + + describe('when resized to intersect the other event\'s start', () => { + it('does not allow resizing', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: false, + }, + ] + testEventResize(options, '2014-11-04T06:00:00', false, done, 'event-a') + }) + }) + }) + + describe('when both events\' overlap is true AND they intersect', () => { + it('allows dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + overlap: true, + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + overlap: true, + }, + ] + testEventDrag(options, '2014-11-04T04:00:00', true, done, 'event-a') + }) + }) + + describe('when other eventSource overlap is false', () => { + describe('when dragged over the other event', () => { + it('does not allow dragging', (done) => { + options.eventSources = [ + { + events: [{ + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }], + }, + { + overlap: false, + events: [{ + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }], + }, + ] + testEventDrag(options, '2014-11-04T06:00:00', false, done, 'event-a') + }) + }) + }) + + describe('when subject event is false', () => { + describe('when dragged adjacently after the other event', () => { + it('allows dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + overlap: false, + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + testEventDrag(options, '2014-11-04T09:00:00', true, done, 'event-a') + }) + }) + describe('when dragged intersecting the other event\'s end', () => { + it('does not allow dragging', (done) => { + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + overlap: false, + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + testEventDrag(options, '2014-11-04T04:00:00', false, done, 'event-a') + }) + }) + }) + + describe('when subject eventSource is false', () => { + describe('when dragged after the other event', () => { + it('allows dragging', (done) => { + options.eventSources = [ + { + overlap: false, + events: [{ + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }], + }, + { + events: [{ + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }], + }, + ] + testEventDrag(options, '2014-11-04T09:00:00', true, done, 'event-a') + }) + }) + describe('when dragged over the other event', () => { + it('does not allow dragging', (done) => { + options.eventSources = [ + { + overlap: false, + events: [{ + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }], + }, + { + events: [{ + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }], + }, + ] + testEventDrag(options, '2014-11-04T06:00:00', false, done, 'event-a') + }) + }) + }) + + describe('when eventOverlap is false', () => { + describe('when dragged adjacently after another event', () => { + it('allows dragging', (done) => { + options.eventOverlap = false + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + testEventDrag(options, '2014-11-04T09:00:00', true, done, 'event-a') + }) + }) + describe('when dragged intersecting another event', () => { + it('does not allow dragging', (done) => { + options.eventOverlap = false + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + testEventDrag(options, '2014-11-04T06:00:00', false, done, 'event-a') + }) + }) + }) + + describe('when eventOverlap is a function', () => { + describe('when no intersecting events upon drag', () => { + it('does not get called, allows dragging', (done) => { + options.eventOverlap = () => {} + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T03:00:00', + end: '2014-11-04T07:00:00', + }, + ] + spyOn(options, 'eventOverlap').and.callThrough() + testEventDrag(options, '2014-11-04T06:00:00', true, () => { + expect(options.eventOverlap).not.toHaveBeenCalled() + done() + }, 'event-b') + }) + }) + describe('when an intersection and returning true', () => { + it('allows dragging AND gets called', (done) => { + options.eventOverlap = (stillEvent, movingEvent) => { + // checks arguments here + expect(stillEvent.title).toBe('Event B') + expect(movingEvent.title).toBe('Event A') + return true + } + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + spyOn(options, 'eventOverlap').and.callThrough() + testEventDrag(options, '2014-11-04T06:00:00', true, () => { + expect(options.eventOverlap).toHaveBeenCalled() + done() + }, 'event-a') + }) + }) + describe('when an intersection and returning false', () => { + it('disallows dragging AND gets called', (done) => { + options.eventOverlap = () => false + options.events = [ + { + title: 'Event A', + className: 'event-a', + start: '2014-11-04T01:00:00', + end: '2014-11-04T03:00:00', + }, + { + title: 'Event B', + className: 'event-b', + start: '2014-11-04T05:00:00', + end: '2014-11-04T09:00:00', + }, + ] + spyOn(options, 'eventOverlap').and.callThrough() + testEventDrag(options, '2014-11-04T06:00:00', false, () => { + expect(options.eventOverlap).toHaveBeenCalled() + done() + }, 'event-a') + }) + }) + }) +}) + +describe('selectOverlap', () => { + let options + + beforeEach(() => { + options = { + initialDate: '2014-11-12', + initialView: 'timeGridWeek', + scrollTime: '00:00', + } + }) + + describe('as false', () => { + beforeEach(() => { + options.selectOverlap = false + }) + describe('when dragged adjacently before an event', () => { + it('allows selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + testSelection(options, '2014-11-12T01:00:00Z', '2014-11-12T04:00:00Z', true, done) + }) + }) + describe('when dragged adjacently after an event', () => { + it('allows selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + testSelection(options, '2014-11-12T06:00:00Z', '2014-11-12T12:00:00Z', true, done) + }) + }) + describe('when dragged intersecting an event\'s start', () => { + describe('when UTC timezone', () => { + it('does not allow selection', (done) => { + options.timeZone = 'UTC' + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00+00:00', + end: '2014-11-12T06:00:00+00:00', + }] + testSelection(options, '2014-11-12T01:00:00Z', '2014-11-12T05:00:00Z', false, done) + }) + }) + describe('when local timezone', () => { + it('does not allow selection', (done) => { + options.timeZone = 'local' + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + testSelection(options, '2014-11-12T01:00:00', '2014-11-12T05:00:00', false, done) + }) + }) + }) + describe('when dragged intersecting an event\'s end', () => { + describe('when in week view with timed events', () => { + describe('when no timezone', () => { + it('does not allow selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T08:00:00Z', false, done) + }) + }) + describe('when UTC timezone', () => { + it('does not allow selection', (done) => { + options.timeZone = 'UTC' + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00+00:00', + end: '2014-11-12T06:00:00+00:00', + }] + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T08:00:00Z', false, done) + }) + }) + describe('when local timezone', () => { + it('does not allow selection', (done) => { + options.timeZone = 'local' + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + testSelection(options, '2014-11-12T05:00:00', '2014-11-12T08:00:00', false, done) + }) + }) + }) + describe('when in month view', () => { + beforeEach(() => { + options.initialView = 'dayGridMonth' + }) + describe('with all-day event', () => { + it('does not allow selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12', + end: '2014-11-14', + }] + testSelection(options, '2014-11-12', '2014-11-13', false, done) + }) + }) + describe('with timed event', () => { + it('does not allow selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12T05:00:00', + end: '2014-11-14T20:00:00', + }] + testSelection(options, '2014-11-12', '2014-11-13', false, done) + }) + }) + }) + }) + describe('when dragged to be encompassed by an event', () => { + it('does not allow selection', (done) => { + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T10:00:00', + }] + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T08:00:00Z', false, done) + }) + }) + }) + + describe('as a function', () => { + describe('when no intersecting events when selecting', () => { + it('does not get called, allows selection', (done) => { + options.selectOverlap = () => {} + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + spyOn(options, 'selectOverlap').and.callThrough() + testSelection(options, '2014-11-12T08:00:00Z', '2014-11-12T10:00:00Z', true, () => { + expect(options.selectOverlap).not.toHaveBeenCalled() + done() + }) + }) + }) + describe('when an intersection and returning true', () => { + it('allows selection', (done) => { + options.selectOverlap = (arg0, arg1) => { + // checks arguments here + expect(arg0.title).toBe('Event A') + expect(arg1).toBeFalsy() + return true + } + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + spyOn(options, 'selectOverlap').and.callThrough() + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T07:00:00Z', true, () => { + expect(options.selectOverlap).toHaveBeenCalled() + done() + }) + }) + }) + describe('when an intersection and returning false', () => { + it('does not allow selection', (done) => { + options.selectOverlap = () => false + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + }] + spyOn(options, 'selectOverlap').and.callThrough() + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T07:00:00Z', false, () => { + expect(options.selectOverlap).toHaveBeenCalled() + done() + }) + }) + }) + }) + + describe('as true and an event object\'s overlap is false', () => { + it('is not affected AND allows the selection', (done) => { + options.selectOverlap = true + options.events = [{ + title: 'Event A', + start: '2014-11-12T04:00:00', + end: '2014-11-12T06:00:00', + overlap: false, + }] + testSelection(options, '2014-11-12T05:00:00Z', '2014-11-12T07:00:00', true, done) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/refetchEvents.ts b/fullcalendar-main/tests/src/legacy/refetchEvents.ts new file mode 100644 index 0000000..a3546e9 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/refetchEvents.ts @@ -0,0 +1,134 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('refetchEvents', () => { + // there IS a similar test in automated-better, but does month view + describe('when timeGrid events are rerendered', () => { + it('keeps scroll after refetchEvents', (done) => { + let calendar = initCalendar({ + now: '2015-08-07', + scrollTime: '00:00', + height: 400, // makes this test more consistent across viewports + initialView: 'timeGridDay', + events(arg, callback) { + setTimeout(() => { + callback([ + { id: '1', resourceId: 'b', start: '2015-08-07T02:00:00', end: '2015-08-07T07:00:00', title: 'event 1' }, + { id: '2', resourceId: 'c', start: '2015-08-07T05:00:00', end: '2015-08-07T22:00:00', title: 'event 2' }, + { id: '3', resourceId: 'd', start: '2015-08-06', end: '2015-08-08', title: 'event 3' }, + { id: '4', resourceId: 'e', start: '2015-08-07T03:00:00', end: '2015-08-07T08:00:00', title: 'event 4' }, + { id: '5', resourceId: 'f', start: '2015-08-07T00:30:00', end: '2015-08-07T02:30:00', title: 'event 5' }, + ]) + }, 100) + }, + }) + + setTimeout(() => { + let viewWrapper = new TimeGridViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + scrollEl.scrollTop = 100 + setTimeout(() => { + currentCalendar.refetchEvents() + + setTimeout(() => { + expect(scrollEl.scrollTop).toBe(100) + done() + }, 100) + }, 100) + }, 101) // after the fetch + }) + }) + + describe('when there are multiple event sources', () => { + let fetchCount // affects events created in createEventGenerator + let eventSources + + pushOptions({ + now: '2015-08-07', + initialView: 'timeGridWeek', + }) + + beforeEach(() => { + fetchCount = 0 + eventSources = [ + { + events: createEventGenerator(), + color: 'green', + id: 'source1', + }, + { + events: createEventGenerator(), + color: 'blue', + id: 'source2', + }, + { + events: createEventGenerator(), + color: 'red', + id: 'source3', + }, + ] + }) + + describe('and all events are fetched synchronously', () => { + it('all events are immediately updated', (done) => { + initCalendar({ eventSources }) + fetchCount += 1 + currentCalendar.refetchEvents() + expect($('.fetch0').length).toEqual(0) + expect($('.fetch1').length).toEqual(3) + done() + }) + }) + + describe('and one event source is asynchronous', () => { + it('original events remain on the calendar until all events have been refetched', (done) => { + // set a 100ms timeout on this event source + eventSources[0].events = (arg, callback) => { + let events = [ + { id: '1', + start: '2015-08-07T02:00:00', + end: '2015-08-07T03:00:00', + title: 'event A', + className: 'fetch' + fetchCount }, + ] + setTimeout(() => { + callback(events) + }, 100) + } + + initCalendar({ + eventSources, + }) + + setTimeout(() => { + fetchCount += 1 + currentCalendar.refetchEvents() + expect($('.fetch0').length).toEqual(3) // original events still on the calendar + expect($('.fetch1').length).toEqual(0) // new events not yet refetched + + setTimeout(() => { + expect($('.fetch0').length).toEqual(0) + expect($('.fetch1').length).toEqual(3) + done() + }, 101) + }, 101) + }) + }) + + // relies on fetchCount + function createEventGenerator() { + return (arg, callback) => { + let events = [ + { + id: 1, + start: '2015-08-07T02:00:00', + end: '2015-08-07T03:00:00', + title: 'event A', + className: 'fetch' + fetchCount, + }, + ] + callback(events) + } + } + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/removeEventSources.ts b/fullcalendar-main/tests/src/legacy/removeEventSources.ts new file mode 100644 index 0000000..bd08582 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/removeEventSources.ts @@ -0,0 +1,54 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('removeEventSources', () => { + pushOptions({ + initialDate: '2014-08-01', + initialView: 'timeGridDay', + eventSources: [ + buildEventSource(1), + buildEventSource(2), + buildEventSource(3), + ], + }) + + describe('when called with no arguments', () => { + it('removes all sources', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getEventEls().length).toBe(3) + + calendar.removeAllEventSources() + + expect(calendarWrapper.getEventEls().length).toBe(0) + }) + }) + + describe('when called with specific IDs', () => { + it('removes only events with matching sources', () => { + let calendar = initCalendar() + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getEventEls().length).toBe(3) + + calendar.getEventSourceById('1').remove() + calendar.getEventSourceById('3').remove() + + expect(calendarWrapper.getEventEls().length).toBe(1) + expect($('.event2').length).toBe(1) + }) + }) + + function buildEventSource(id) { + return { + id, + events(arg, callback) { + callback([{ + title: 'event' + id, + className: 'event' + id, + start: '2014-08-01T02:00:00', + }]) + }, + } + } +}) diff --git a/fullcalendar-main/tests/src/legacy/removeEvents.ts b/fullcalendar-main/tests/src/legacy/removeEvents.ts new file mode 100644 index 0000000..c66de16 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/removeEvents.ts @@ -0,0 +1,157 @@ +import { EventInput } from '@fullcalendar/core' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('removeEvents', () => { + pushOptions({ + initialDate: '2014-06-24', + initialView: 'dayGridMonth', + }) + + function buildEventsWithoutIds(): EventInput[] { + return [ + { title: 'event zero', start: '2014-06-24', className: 'event-zero' }, + { title: 'event one', start: '2014-06-24', className: 'event-non-zero event-one' }, + { title: 'event two', start: '2014-06-24', className: 'event-non-zero event-two' }, + ] + } + + function buildEventsWithIds() { + let events = buildEventsWithoutIds() + let i + + for (i = 0; i < events.length; i += 1) { + events[i].id = i + } + + return events + } + + $.each({ + 'when events without IDs': buildEventsWithoutIds, + 'when events with IDs': buildEventsWithIds, + }, (desc, eventGenerator) => { + describe(desc, () => { + it('can remove all events if no args specified', (done) => { + go( + eventGenerator(), + () => { + currentCalendar.removeAllEvents() + }, + () => { + expect(currentCalendar.getEvents().length).toEqual(0) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(0) + }, + done, + ) + }) + + it('can remove events individually', (done) => { + go( + eventGenerator(), + () => { + currentCalendar.getEvents().forEach((event) => { + if ($.inArray('event-one', event.classNames) !== -1) { + event.remove() + } + }) + }, + () => { + expect(currentCalendar.getEvents().length).toEqual(2) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(2) + expect($('.event-zero').length).toEqual(1) + expect($('.event-two').length).toEqual(1) + }, + done, + ) + }) + }) + }) + + it('can remove events with a numeric ID', (done) => { + go( + buildEventsWithIds(), + () => { + currentCalendar.getEventById(1 as any).remove() + }, + () => { + expect(currentCalendar.getEvents().length).toEqual(2) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(2) + expect($('.event-zero').length).toEqual(1) + expect($('.event-two').length).toEqual(1) + }, + done, + ) + }) + + it('can remove events with a string ID', (done) => { + go( + buildEventsWithIds(), + () => { + currentCalendar.getEventById('1').remove() + }, + () => { + expect(currentCalendar.getEvents().length).toEqual(2) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(2) + expect($('.event-zero').length).toEqual(1) + expect($('.event-two').length).toEqual(1) + }, + done, + ) + }) + + it('can remove an event with ID 0', (done) => { // for issue 2082 + go( + buildEventsWithIds(), + () => { + currentCalendar.getEventById(0 as any).remove() + }, + () => { + expect(currentCalendar.getEvents().length).toEqual(2) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(2) + expect($('.event-zero').length).toEqual(0) + expect($('.event-non-zero').length).toEqual(2) + }, + done, + ) + }) + + // Verifies the actions in removeFunc executed correctly by calling checkFunc. + function go(events, removeFunc, checkFunc, doneFunc) { + initCalendar({ + events, + }) + + checkAllEvents() // make sure all events initially rendered correctly + removeFunc() // remove the events + setTimeout(() => { // because the event rerender will be queued because we're a level deep + checkFunc() // check correctness + + // move the calendar back out of view, then back in + currentCalendar.next() + currentCalendar.prev() + + // array event sources should maintain the same state + // whereas "dynamic" event sources should refetch and reset the state + if ($.isArray(events)) { + checkFunc() // for issue 2187 + } else { + checkAllEvents() + } + + doneFunc() + }, 0) + } + + // Checks to make sure all events have been rendered and that the calendar + // has internal info on all the events. + function checkAllEvents() { + expect(currentCalendar.getEvents().length).toEqual(3) + let calendarWrapper = new CalendarWrapper(currentCalendar) + expect(calendarWrapper.getEventEls().length).toEqual(3) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/rerenderDelay.ts b/fullcalendar-main/tests/src/legacy/rerenderDelay.ts new file mode 100644 index 0000000..0fe834d --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/rerenderDelay.ts @@ -0,0 +1,48 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('rerenderDelay', () => { + it('batches together many event renders', (done) => { + let eventSource1 = [ + { title: 'event1', start: '2016-12-04T01:00:00', className: 'event1' }, + { title: 'event2', start: '2016-12-04T02:00:00', className: 'event2' }, + ] + let eventSource2 = [ + { title: 'event3', start: '2016-12-04T03:00:00', className: 'event3' }, + { title: 'event4', start: '2016-12-04T04:00:00', className: 'event4' }, + ] + let extraEvent1 = { title: 'event5', start: '2016-12-04T05:00:00', className: 'event5', id: '5' } + let extraEvent2 = { title: 'event6', start: '2016-12-04T06:00:00', className: 'event6', id: '6' } + + let calendar = initCalendar({ + initialDate: '2016-12-04', + initialView: 'timeGridDay', + events: eventSource1, + rerenderDelay: 0, // will still debounce despite being zero + }) + let calendarWrapper = new CalendarWrapper(calendar) + + expect(calendarWrapper.getEventEls().length).toBe(2) + + currentCalendar.addEventSource(eventSource2) + expect(calendarWrapper.getEventEls().length).toBe(2) + + currentCalendar.addEvent(extraEvent1) + expect(calendarWrapper.getEventEls().length).toBe(2) + + let refined2 = currentCalendar.addEvent(extraEvent2) + expect(calendarWrapper.getEventEls().length).toBe(2) + + refined2.remove() + expect(calendarWrapper.getEventEls().length).toBe(2) + + setTimeout(() => { // after rendered + expect($('.event1').length).toBe(1) + expect($('.event2').length).toBe(1) + expect($('.event3').length).toBe(1) + expect($('.event4').length).toBe(1) + expect($('.event5').length).toBe(1) + expect($('.event6').length).toBe(0) // got removed + done() + }, 1) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/scroll-state.ts b/fullcalendar-main/tests/src/legacy/scroll-state.ts new file mode 100644 index 0000000..ba9cd33 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/scroll-state.ts @@ -0,0 +1,69 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('scroll state', () => { + let calendarEl + + beforeEach(() => { + calendarEl = $('<div id="calendar">').width(800).appendTo('body') + }) + afterEach(() => { + calendarEl.remove() + calendarEl = null + }) + + pushOptions({ + initialDate: '2015-02-20', + contentHeight: 200, + scrollTime: '00:00', // for timeGrid + }) + + describeOptions('initialView', { + 'when in month view': 'dayGridMonth', + 'when in week view': 'timeGridWeek', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('should be maintained when resizing window', (done) => { + let scrollEl + let scroll0 + let calendar = initCalendar({ + windowResize() { + setTimeout(() => { // wait until all other tasks are finished + expect(scrollEl.scrollTop).toBe(scroll0) + done() + }, 0) + }, + }, calendarEl) + + scrollEl = new ViewWrapper(calendar).getScrollerEl() + + setTimeout(() => { // wait until after browser's scroll state is applied + scrollEl.scrollTop = 9999 // all the way + scroll0 = scrollEl.scrollTop + $(window).simulate('resize') + }, 0) + }) + + it('should be maintained when after rerendering events', () => { + let calendar = initCalendar({ + events: [{ + start: '2015-02-20', + }], + }, calendarEl) + + let scrollEl = new ViewWrapper(calendar).getScrollerEl() + let eventEl0 = new CalendarWrapper(calendar).getEventEls() + expect(eventEl0.length).toBe(1) + + scrollEl.scrollTop = 9999 // all the way + let scroll0 = scrollEl.scrollTop + currentCalendar.render() + + let eventEl1 = new CalendarWrapper(calendar).getEventEls() + expect(eventEl1.length).toBe(1) + expect(scrollEl.scrollTop).toBe(scroll0) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/scrollTime.ts b/fullcalendar-main/tests/src/legacy/scrollTime.ts new file mode 100644 index 0000000..7c401bd --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/scrollTime.ts @@ -0,0 +1,55 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('scrollTime', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('accepts a string Duration', () => { + let calendar = initCalendar({ + scrollTime: '02:00:00', + height: 400, // short enough to make scrolling happen + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let timeGridWrapper = viewWrapper.timeGrid + let slotTop = viewWrapper.timeGrid.getTimeTop('02:00:00') - $(timeGridWrapper.el).offset().top + let scrollTop = viewWrapper.getScrollerEl().scrollTop + let diff = Math.abs(slotTop - scrollTop) + + expect(slotTop).toBeGreaterThan(0) + expect(scrollTop).toBeGreaterThan(0) + expect(diff).toBeLessThan(3) + }) + + it('accepts a Duration object', () => { + let calendar = initCalendar({ + scrollTime: { hours: 2 }, + height: 400, // short enough to make scrolling happen + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let timeGridWrapper = viewWrapper.timeGrid + let slotTop = timeGridWrapper.getTimeTop('02:00:00') - $(timeGridWrapper.el).offset().top + let scrollTop = viewWrapper.getScrollerEl().scrollTop + let diff = Math.abs(slotTop - scrollTop) + + expect(slotTop).toBeGreaterThan(0) + expect(scrollTop).toBeGreaterThan(0) + expect(diff).toBeLessThan(3) + }) + + it('doesn\'t get applied on navigation when scrollTimeReset is false', () => { + let calendar = initCalendar({ + scrollTime: '02:00:00', + scrollTimeReset: false, + height: 400, // short enough to make scrolling happen + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + scrollEl.scrollTop = 99999 + let scrollTop = scrollEl.scrollTop + + calendar.next() + expect(scrollEl.scrollTop).toBe(scrollTop) // stays the same + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/select-callback.ts b/fullcalendar-main/tests/src/legacy/select-callback.ts new file mode 100644 index 0000000..929760f --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/select-callback.ts @@ -0,0 +1,352 @@ +import { Calendar } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' +import interactionPlugin from '@fullcalendar/interaction' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +// UNFORTUNATELY, these tests are affected by the window height b/c of autoscrolling + +describe('select callback', () => { + pushOptions({ + initialDate: '2014-05-25', + selectable: true, + longPressDelay: 100, + }) + + describeOptions('direction', { + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + it('gets fired correctly when the user selects cells', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-04-28') + expect(arg.startStr).toEqual('2014-04-28') + expect(arg.end).toEqualDate('2014-05-07') + expect(arg.endStr).toEqual('2014-05-07') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.selectDates('2014-04-28', '2014-05-06').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + it('gets fired correctly when the user selects cells via touch', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-04-28') + expect(arg.startStr).toEqual('2014-04-28') + expect(arg.end).toEqualDate('2014-05-07') + expect(arg.endStr).toEqual('2014-05-07') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.selectDatesTouch( + '2014-04-28', + '2014-05-06', + true, // debug. HACK + ).then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + it('gets fired correctly when the user selects just one cell', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-04-28') + expect(arg.startStr).toEqual('2014-04-28') + expect(arg.end).toEqualDate('2014-04-29') + expect(arg.endStr).toEqual('2014-04-29') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + dayGridWrapper.selectDates('2014-04-28', '2014-04-28').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('when selecting all-day slots', () => { + it('gets fired correctly when the user selects cells', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-05-28') + expect(arg.startStr).toEqual('2014-05-28') + expect(arg.end).toEqualDate('2014-05-30') + expect(arg.endStr).toEqual('2014-05-30') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + + dayGridWrapper.selectDates('2014-05-28', '2014-05-29').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + it('gets fired correctly when the user selects a single cell', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-05-28') + expect(arg.startStr).toEqual('2014-05-28') + expect(arg.end).toEqualDate('2014-05-29') + expect(arg.endStr).toEqual('2014-05-29') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + + dayGridWrapper.selectDates('2014-05-28', '2014-05-28').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + }) + + describe('when selecting timed slots', () => { + it('gets fired correctly when the user selects slots', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.start).toEqualDate('2014-05-28T09:00:00Z') + expect(arg.startStr).toEqual('2014-05-28T09:00:00Z') + expect(arg.end).toEqualDate('2014-05-28T10:30:00Z') + expect(arg.endStr).toEqual('2014-05-28T10:30:00Z') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + timeGridWrapper.selectDates('2014-05-28T09:00:00', '2014-05-28T10:30:00').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4505 + it('gets fired correctly when the user selects slots NEAR THE END', (done) => { + let options = { + scrollTime: '24:00', + select(arg) { + expect(arg.start).toEqualDate('2014-05-28T16:00:00Z') + expect(arg.end).toEqualDate('2014-05-29T00:00:00Z') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + timeGridWrapper.selectDates('2014-05-28T16:00:00', '2014-05-29T00:00:00').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + it('gets fired correctly when the user selects slots via touch', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.start).toEqualDate('2014-05-28T09:00:00Z') + expect(arg.startStr).toEqual('2014-05-28T09:00:00Z') + expect(arg.end).toEqualDate('2014-05-28T10:30:00Z') + expect(arg.endStr).toEqual('2014-05-28T10:30:00Z') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + setTimeout(() => { // HACK: sometimes touch dragging wouldn't grab onto anything + timeGridWrapper.selectDatesTouch( + '2014-05-28T09:00:00', + '2014-05-28T10:30:00', + true, // debug. HACK + ).then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }, 100) + }) + + it('gets fired correctly when the user selects slots in a different day', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.start).toEqualDate('2014-05-28T09:00:00Z') + expect(arg.startStr).toEqual('2014-05-28T09:00:00Z') + expect(arg.end).toEqualDate('2014-05-29T10:30:00Z') + expect(arg.endStr).toEqual('2014-05-29T10:30:00Z') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + timeGridWrapper.selectDates('2014-05-28T09:00:00', '2014-05-29T10:30:00').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + + it('gets fired correctly when the user selects a single slot', (done) => { + let options = { + select(arg) { + expect(arg.start instanceof Date).toEqual(true) + expect(arg.end instanceof Date).toEqual(true) + expect(typeof arg.jsEvent).toEqual('object') // TODO: more discrimination + expect(typeof arg.view).toEqual('object') // " + expect(arg.allDay).toEqual(false) + expect(arg.start).toEqualDate('2014-05-28T09:00:00Z') + expect(arg.startStr).toEqual('2014-05-28T09:00:00Z') + expect(arg.end).toEqualDate('2014-05-28T09:30:00Z') + expect(arg.endStr).toEqual('2014-05-28T09:30:00Z') + }, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + timeGridWrapper.selectDates('2014-05-28T09:00:00', '2014-05-28T09:30:00').then(() => { + expect(options.select).toHaveBeenCalled() + done() + }) + }) + }) + }) + }) + + describe('when selectMinDistance', () => { + pushOptions({ + selectMinDistance: 10, + }) + + it('will fire when dragged beyond distance', (done) => { + let options = { + select() {}, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getDayEl('2014-04-28')).simulate('drag', { + dx: 12, + dy: 0, + callback() { + expect(options.select).toHaveBeenCalled() + done() + }, + }) + }) + + it('will not fire when not dragged beyond distance', (done) => { + let options = { + select() {}, + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + $(dayGridWrapper.getDayEl('2014-04-28')).simulate('drag', { + dx: 8, + dy: 0, + callback() { + expect(options.select).not.toHaveBeenCalled() + done() + }, + }) + }) + }) + + it('will fire on a calendar that hasn\'t been rendered yet', (done) => { + let calendar = new Calendar( + document.createElement('div'), + { + plugins: [interactionPlugin, dayGridPlugin], + now: '2018-12-25', + select(info) { + expect(info.startStr).toBe('2018-12-20') + expect(info.endStr).toBe('2018-12-23') + done() + }, + }, + ) + + calendar.select('2018-12-20', '2018-12-23') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/select-method.ts b/fullcalendar-main/tests/src/legacy/select-method.ts new file mode 100644 index 0000000..a8584a0 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/select-method.ts @@ -0,0 +1,211 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('select method', () => { + pushOptions({ + initialDate: '2014-05-25', + selectable: true, + }) + + /* + THINGS TO IMPLEMENT IN SRC (in addition to notes further down): + - better date normalization (for both render and reporting to select callback) + - if second date is the same or before the first + - if given a mixture of timed/all-day + - for dayGrid/month views, when given timed dates, should really be all-day + */ + + describeOptions('direction', { + 'when LTR': 'ltr', + 'when RTL': 'rtl', + }, () => { + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + describe('when called with all-day date strings', () => { + describe('when in bounds', () => { + it('renders a selection', () => { + let calendar = initCalendar() + calendar.select('2014-05-07', '2014-05-09') + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getHighlightEls()).toBeVisible() + }) + + it('renders a selection when called with one argument', () => { + let calendar = initCalendar() + calendar.select('2014-05-07') + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getHighlightEls()).toBeVisible() + }) + + it('fires a selection event', () => { + let selectSpy = spyOnCalendarCallback('select', (arg) => { + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-05-07') + expect(arg.end).toEqualDate('2014-05-09') + }) + let calendar = initCalendar() + calendar.select('2014-05-07', '2014-05-09') + expect(selectSpy).toHaveBeenCalled() + }) + }) + + describe('when out of bounds', () => { + it('doesn\'t render a selection', () => { + let calendar = initCalendar() + calendar.select('2015-05-07', '2015-05-09') + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getHighlightEls()).not.toBeVisible() + }) + + /* + TODO: implement this behavior + it('doesn\'t fire a selection event', function() { + options.select = function(arg) { + expect(arg.start).toEqualDate('2014-05-07'); + expect(arg.end).toEqualDate('2014-05-09'); + }; + spyOn(options, 'select').and.callThrough(); + let calendar = initCalendar(options); + calendar.select('2015-05-07', '2015-05-09'); + expect(options.select).not.toHaveBeenCalled(); + }); + */ + }) + }) + + describe('when called with timed date strings', () => { + it('renders a selection', () => { + let calendar = initCalendar() + calendar.select('2014-05-07T06:00:00', '2014-05-09T07:00:00') + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getHighlightEls()).toBeVisible() + }) + + it('fires a selection event', () => { + let selectSpy = spyOnCalendarCallback('select', (arg) => { + expect(arg.allDay).toEqual(false) + expect(arg.start).toEqualDate('2014-05-07T06:00:00Z') + expect(arg.end).toEqualDate('2014-05-09T06:00:00Z') + }) + let calendar = initCalendar() + calendar.select('2014-05-07T06:00:00', '2014-05-09T06:00:00') + expect(selectSpy).toHaveBeenCalled() + }) + }) + }) + + describe('when in week view', () => { // May 25 - 31 + pushOptions({ + initialView: 'timeGridWeek', + scrollTime: '01:00:00', // so that most events will be below the divider + height: 400, // short enought to make scrolling happen + }) + + describe('when called with timed date strings', () => { + describe('when in bounds', () => { + it('renders a selection when called with one argument', () => { + let calendar = initCalendar() + calendar.select('2014-05-26T06:00:00') + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.getHighlightEls()).toBeVisible() + }) + + it('renders a selection over the slot area', () => { + let calendar = initCalendar() + calendar.select('2014-05-26T06:00:00', '2014-05-26T08:00:00') + let viewWrapper = new TimeGridViewWrapper(calendar) + let highlightEls = viewWrapper.timeGrid.getHighlightEls() + expect(highlightEls).toBeVisible() + let slotAreaTop = $(viewWrapper.getScrollerEl()).offset().top + let overlayTop = $(highlightEls[0]).offset().top + expect(overlayTop).toBeGreaterThan(slotAreaTop) + }) + }) + + describe('when out of bounds', () => { + it('doesn\'t render a selection', () => { + let calendar = initCalendar() + calendar.select('2015-05-26T06:00:00', '2015-05-26T07:00:00') + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.getHighlightEls()).not.toBeVisible() + }) + + /* + TODO: implement this behavior + it('doesn\'t fire a selection event', function() { + options.select = function(arg) { + expect(arg.start).toEqualDate('2015-05-07T06:00:00Z'); + expect(arg.end).toEqualDate('2015-05-09T07:00:00Z'); + }; + spyOn(options, 'select').and.callThrough(); + let calendar = initCalendar(options); + calendar.select('2015-05-07T06:00:00', '2015-05-09T07:00:00'); + expect(options.select).not.toHaveBeenCalled(); + }); + */ + }) + }) + + describe('when called with all-day date strings', () => { // forget about in/out bounds for this :) + describe('when allDaySlot is on', () => { + pushOptions({ + allDaySlot: true, + }) + + it('renders a selection over the day area', () => { + let calendar = initCalendar() + calendar.select('2014-05-26', '2014-05-28') + let viewWrapper = new TimeGridViewWrapper(calendar) + let highlightEls = viewWrapper.dayGrid.getHighlightEls() + expect(highlightEls).toBeVisible() + let slotAreaTop = $(viewWrapper.getScrollerEl()).offset().top + let overlayTop = $(highlightEls[0]).offset().top + expect(overlayTop).toBeLessThan(slotAreaTop) + }) + + it('fires a selection event', () => { + let selectSpy = spyOnCalendarCallback('select', (arg) => { + expect(arg.allDay).toEqual(true) + expect(arg.start).toEqualDate('2014-05-26') + expect(arg.end).toEqualDate('2014-05-28') + }) + let calendar = initCalendar() + calendar.select('2014-05-26', '2014-05-28') + expect(selectSpy).toHaveBeenCalled() + }) + }) + + describe('when allDaySlot is off', () => { + pushOptions({ + allDaySlot: false, + }) + + it('doesn\'t render the all-day selection over time area', () => { + let calendar = initCalendar() + calendar.select('2014-05-26', '2014-05-28') + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.getHighlightEls()).not.toBeVisible() + }) + + /* + TODO: implement + it('doesn\'t fire a selection event', function() { + options.select = function(arg) { + expect(arg.allDay).toEqual(true); + expect(arg.start).toEqualDate('2014-05-26'); + expect(arg.end).toEqualDate('2014-05-28'); + }; + spyOn(options, 'select').and.callThrough(); + let calendar = initCalendar(options); + calendar.select('2014-05-26', '2014-05-28'); + expect(options.select).not.toHaveBeenCalled(); + }); + */ + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/selectAllow.ts b/fullcalendar-main/tests/src/legacy/selectAllow.ts new file mode 100644 index 0000000..0226985 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/selectAllow.ts @@ -0,0 +1,54 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { waitDateSelect } from '../lib/wrappers/interaction-util.js' + +describe('selectAllow', () => { + pushOptions({ + now: '2016-09-04', + initialView: 'timeGridWeek', + scrollTime: '00:00', + selectable: true, + }) + + it('disallows selecting when returning false', (done) => { // and given correct params + let options = { + selectAllow(selectInfo) { + expect(typeof selectInfo).toBe('object') + expect(selectInfo.start instanceof Date).toBe(true) + expect(selectInfo.end instanceof Date).toBe(true) + return false + }, + } + spyOn(options, 'selectAllow').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let selecting = timeGridWrapper.selectDates('2016-09-04T01:00:00Z', '2016-09-04T05:00:00Z') + + waitDateSelect(calendar, selecting).then((selectInfo) => { + expect(selectInfo).toBeFalsy() + expect(options.selectAllow).toHaveBeenCalled() + done() + }) + }) + + it('allows selecting when returning true', (done) => { + let options = { + selectAllow(selectInfo) { + return true + }, + } + spyOn(options, 'selectAllow').and.callThrough() + + let calendar = initCalendar(options) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let selecting = timeGridWrapper.selectDates('2016-09-04T01:00:00Z', '2016-09-04T05:00:00Z') + + waitDateSelect(calendar, selecting).then((selectInfo) => { + expect(typeof selectInfo).toBe('object') + expect(selectInfo.start).toEqualDate('2016-09-04T01:00:00Z') + expect(selectInfo.end).toEqualDate('2016-09-04T05:00:00Z') + expect(options.selectAllow).toHaveBeenCalled() + done() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/selectMirror.ts b/fullcalendar-main/tests/src/legacy/selectMirror.ts new file mode 100644 index 0000000..9788a39 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/selectMirror.ts @@ -0,0 +1,30 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('selectMirror', () => { + pushOptions({ + initialDate: '2014-08-03', + initialView: 'timeGridWeek', + scrollTime: '00:00:00', + selectMirror: true, + }) + + it('goes through eventDidMount', () => { + let options = { + eventDidMount(arg) { + expect(arg.isMirror).toBe(true) + }, + } + + spyOn(options, 'eventDidMount').and.callThrough() + + let calendar = initCalendar(options) + + calendar.select('2014-08-04T01:00:00Z', '2014-08-04T04:00:00Z') + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let mirrorEls = timeGridWrapper.getMirrorEls() + + expect(mirrorEls.length).toBe(1) + expect(options.eventDidMount).toHaveBeenCalled() + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/slotDuration.ts b/fullcalendar-main/tests/src/legacy/slotDuration.ts new file mode 100644 index 0000000..24bbb4e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/slotDuration.ts @@ -0,0 +1,87 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotDuration', () => { + const minutesInADay = 1440 + + describe('when using the default settings', () => { + describe('in week', () => { + it('should have slots 1440/30 slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + expect(slotCount).toEqual(Math.ceil(minutesInADay / 30)) + }) + }) + + describe('in day', () => { + it('should have slots 1440/30 slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + expect(slotCount).toEqual(Math.ceil(minutesInADay / 30)) + }) + }) + }) + + describe('when slotMinutes is set to 30', () => { + describe('in week', () => { + it('should have slots 1440/30 slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + expect(slotCount).toEqual(Math.ceil(minutesInADay / 30)) + }) + }) + + describe('in day', () => { + it('should have slots 1440/30 slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + expect(slotCount).toEqual(Math.ceil(minutesInADay / 30)) + }) + }) + }) + + describe('when slotMinutes is set to a series of times', () => { + const slotMinutesList = [10, 12, 15, 17, 20, 30, 35, 45, 60, 62, 120, 300] + + describe('in week', () => { + slotMinutesList.forEach((slotMinutes) => { + it('should have slots 1440/x slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + slotDuration: { minutes: slotMinutes }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + let expected = Math.ceil(minutesInADay / slotMinutes) + expect(slotCount).toEqual(expected) + }) + }) + }) + + describe('in day', () => { + slotMinutesList.forEach((slotMinutes) => { + it('should have slots 1440/x slots', () => { + let calendar = initCalendar({ + initialView: 'timeGridDay', + slotDuration: { minutes: slotMinutes }, + }) + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let slotCount = timeGridWrapper.getSlotEls().length + let expected = Math.ceil(minutesInADay / slotMinutes) + expect(slotCount).toEqual(expected) + }) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/slotLabelFormat.ts b/fullcalendar-main/tests/src/legacy/slotLabelFormat.ts new file mode 100644 index 0000000..e655529 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/slotLabelFormat.ts @@ -0,0 +1,35 @@ +import enGbLocale from '@fullcalendar/core/locales/en-gb' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotLabelFormat', () => { + pushOptions({ + initialDate: '2014-06-04', + initialView: 'timeGridWeek', + }) + + it('renders correctly when default', () => { + let calendar = initCalendar() + expectAxisText(calendar, '12am') + }) + + it('renders correctly when default and the locale is customized', () => { + let calendar = initCalendar({ + locale: enGbLocale, + }) + expectAxisText(calendar, '00') + }) + + it('renders correctly when customized', () => { + let calendar = initCalendar({ + slotLabelFormat: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }, + locale: 'en-GB', // for 00:00 instead of 24:00 + }) + expectAxisText(calendar, '00:00:00') + }) + + function expectAxisText(calendar, expectedText) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + let axisTexts = timeGridWrapper.getAxisTexts() + expect(axisTexts[0]).toBe(expectedText) + } +}) diff --git a/fullcalendar-main/tests/src/legacy/themeSystem.ts b/fullcalendar-main/tests/src/legacy/themeSystem.ts new file mode 100644 index 0000000..60fec8e --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/themeSystem.ts @@ -0,0 +1,67 @@ +import bootstrapPlugin from '@fullcalendar/bootstrap' +import timeGridPlugin from '@fullcalendar/timegrid' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('themeSystem', () => { + pushOptions({ + plugins: [bootstrapPlugin, timeGridPlugin], + initialView: 'timeGridWeek', + headerToolbar: { + left: 'title', + center: '', + right: 'next', + }, + }) + + it('can be changed dynamically', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('next') + + expect(calendar.el).toHaveClass(CalendarWrapper.ROOT_CLASSNAME) + expect(calendar.el).toHaveClass(CalendarWrapper.UNTHEMED_CLASSNAME) + expect(calendar.el).not.toHaveClass(CalendarWrapper.BOOTSTRAP_CLASSNAME) + expect(buttonInfo.iconName).toBeTruthy() + expect($('.table-bordered').length).toBe(0) + + let viewWrapper = new TimeGridViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + scrollEl.scrollTop = 99999 // scroll all the way down + + // change option! + calendar.setOption('themeSystem', 'bootstrap') + + buttonInfo = toolbarWrapper.getButtonInfo('next', 'fa') + expect(calendar.el).toHaveClass(CalendarWrapper.ROOT_CLASSNAME) + expect(calendar.el).toHaveClass(CalendarWrapper.BOOTSTRAP_CLASSNAME) + expect(calendar.el).not.toHaveClass(CalendarWrapper.UNTHEMED_CLASSNAME) + expect(buttonInfo.iconName).toBeTruthy() + expect($('.table-bordered').length).toBeGreaterThan(0) + + // make sure scrolled down at least just a little bit + // since we don't have the bootstrap stylesheet loaded, this will be janky + expect(scrollEl.scrollTop).toBeGreaterThan(10) + }) + + // this tests the options setter with a single hash argument. + // TODO: not best place for this. + it('can be change with other options', () => { + let calendar = initCalendar() + + expect(calendar.el).toHaveClass(CalendarWrapper.ROOT_CLASSNAME) + expect(calendar.el).toHaveClass(CalendarWrapper.UNTHEMED_CLASSNAME) + expect(calendar.el).not.toHaveClass(CalendarWrapper.BOOTSTRAP_CLASSNAME) + + // change option! + calendar.batchRendering(() => { + calendar.setOption('themeSystem', 'bootstrap') + calendar.setOption('businessHours', true) + }) + + expect(calendar.el).toHaveClass(CalendarWrapper.ROOT_CLASSNAME) + expect(calendar.el).toHaveClass(CalendarWrapper.BOOTSTRAP_CLASSNAME) + expect(calendar.el).not.toHaveClass(CalendarWrapper.UNTHEMED_CLASSNAME) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/timeZone.ts b/fullcalendar-main/tests/src/legacy/timeZone.ts new file mode 100644 index 0000000..a3c791d --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/timeZone.ts @@ -0,0 +1,102 @@ +describe('timeZone', () => { + // NOTE: Only deals with the processing of *received* events. + // Verification of a correct AJAX *request* is done in events-json-feed.js + + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2014-05-01', + events: [ + { + id: '1', + title: 'all day event', + start: '2014-05-02', + }, + { + id: '2', + title: 'timed event', + start: '2014-05-10T12:00:00', + }, + { + id: '3', + title: 'timed and zoned event', + start: '2014-05-10T14:00:00+11:00', + }, + ], + }) + + it('receives events correctly when local timezone', () => { + initCalendar({ + timeZone: 'local', + }) + expectLocalTimezone() + }) + + function expectLocalTimezone() { + let allDayEvent = currentCalendar.getEventById('1') + let timedEvent = currentCalendar.getEventById('2') + let zonedEvent = currentCalendar.getEventById('3') + expect(allDayEvent.allDay).toEqual(true) + expect(allDayEvent.start).toEqualLocalDate('2014-05-02T00:00:00') + expect(timedEvent.allDay).toEqual(false) + expect(timedEvent.start).toEqualLocalDate('2014-05-10T12:00:00') + expect(zonedEvent.allDay).toEqual(false) + expect(zonedEvent.start).toEqualDate('2014-05-10T14:00:00+11:00') + } + + it('receives events correctly when UTC timezone', () => { + initCalendar({ + timeZone: 'UTC', + }) + expectUtcTimezone() + }) + + function expectUtcTimezone() { + let allDayEvent = currentCalendar.getEventById('1') + let timedEvent = currentCalendar.getEventById('2') + let zonedEvent = currentCalendar.getEventById('3') + expect(allDayEvent.allDay).toEqual(true) + expect(allDayEvent.start).toEqualDate('2014-05-02') + expect(timedEvent.allDay).toEqual(false) + expect(timedEvent.start).toEqualDate('2014-05-10T12:00:00Z') + expect(zonedEvent.allDay).toEqual(false) + expect(zonedEvent.start).toEqualDate('2014-05-10T14:00:00+11:00') + } + + it('receives events correctly when custom timezone', () => { + initCalendar({ + timeZone: 'America/Chicago', + }) + expectCustomTimezone() + }) + + function expectCustomTimezone() { + let allDayEvent = currentCalendar.getEventById('1') + let timedEvent = currentCalendar.getEventById('2') + let zonedEvent = currentCalendar.getEventById('3') + expect(allDayEvent.allDay).toEqual(true) + expect(allDayEvent.start).toEqualDate('2014-05-02') + expect(timedEvent.allDay).toEqual(false) + expect(timedEvent.start).toEqualDate('2014-05-10T12:00:00Z') + expect(zonedEvent.allDay).toEqual(false) + expect(zonedEvent.start).toEqualDate('2014-05-10T14:00:00Z') // coerced to UTC + } + + it('can be set dynamically', () => { + initCalendar({ + timeZone: 'local', + }) + + expectLocalTimezone() + + currentCalendar.setOption('timeZone', 'UTC') + let allDayEvent = currentCalendar.getEventById('1') + let timedEvent = currentCalendar.getEventById('2') + let zonedEvent = currentCalendar.getEventById('3') + expect(allDayEvent.allDay).toEqual(true) + expect(allDayEvent.start).toEqualDate('2014-05-02') + expect(timedEvent.allDay).toEqual(false) + expect(timedEvent.start).toEqualLocalDate('2014-05-10T12:00:00') // was parsed as LOCAL originally + expect(zonedEvent.allDay).toEqual(false) + expect(zonedEvent.start).toEqualDate('2014-05-10T14:00:00+11:00') + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/timegrid-view.ts b/fullcalendar-main/tests/src/legacy/timegrid-view.ts new file mode 100644 index 0000000..f43bc25 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/timegrid-view.ts @@ -0,0 +1,23 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('timeGrid view rendering', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('should have have days ordered sun to sat', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + let axisEl = viewWrapper.getHeaderAxisEl() + let thEls = viewWrapper.header.getCellEls() + + expect(axisEl).toBeTruthy() + + let dowClassNames = CalendarWrapper.DOW_CLASSNAMES + + for (let i = 0; i < dowClassNames.length; i += 1) { + expect(thEls[i]).toHaveClass(dowClassNames[i]) + } + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/titleFormat.ts b/fullcalendar-main/tests/src/legacy/titleFormat.ts new file mode 100644 index 0000000..c7d7c8c --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/titleFormat.ts @@ -0,0 +1,164 @@ +import frLocale from '@fullcalendar/core/locales/fr' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('titleFormat', () => { + describe('when default', () => { + pushOptions({ + initialDate: '2014-06-12', + titleRangeSeparator: ' - ', + }) + + const VIEWS_WITH_FORMATS = [ + { view: 'dayGridMonth', expected: 'June 2014' }, + { view: 'dayGridWeek', expected: /Jun 8 - 14,? 2014/ }, + { view: 'timeGridWeek', expected: /Jun 8 - 14,? 2014/ }, + { view: 'dayGridDay', expected: /June 12,? 2014/ }, + { view: 'timeGridDay', expected: /June 12,? 2014/ }, + ] + + it('should have default values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + for (let viewWithFormat of VIEWS_WITH_FORMATS) { + calendar.changeView(viewWithFormat.view) + expect(toolbarWrapper.getTitleText()).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when set on a per-view basis', () => { + pushOptions({ + initialDate: '2014-06-12', + titleRangeSeparator: ' - ', + views: { + month: { titleFormat: { year: 'numeric', month: 'long' } }, + dayGridWeek: { titleFormat: { day: 'numeric', month: 'short', year: 'numeric' } }, + week: { titleFormat: { day: 'numeric', month: 'long', year: 'numeric' } }, + dayGridDay: { titleFormat: { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' } }, + }, + }) + + const VIEWS_WITH_FORMATS = [ + { view: 'dayGridMonth', expected: 'June 2014' }, + { view: 'dayGridWeek', expected: 'Jun 8 - 14, 2014' }, + { view: 'timeGridWeek', expected: 'June 8 - 14, 2014' }, + { view: 'dayGridDay', expected: 'Thursday, June 12, 2014' }, + ] + + it('should have the correct values', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + for (let viewWithFormat of VIEWS_WITH_FORMATS) { + calendar.changeView(viewWithFormat.view) + expect(toolbarWrapper.getTitleText()).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('when default and locale is French', () => { + pushOptions({ + initialDate: '2014-06-12', + titleRangeSeparator: ' - ', + locale: frLocale, + }) + + const VIEWS_WITH_FORMATS = [ + { view: 'dayGridMonth', expected: 'juin 2014' }, + { view: 'dayGridWeek', expected: '9 - 15 juin 2014' }, + { view: 'timeGridWeek', expected: '9 - 15 juin 2014' }, + { view: 'dayGridDay', expected: '12 juin 2014' }, + { view: 'timeGridDay', expected: '12 juin 2014' }, + ] + + it('should have the translated dates', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + for (let viewWithFormat of VIEWS_WITH_FORMATS) { + calendar.changeView(viewWithFormat.view) + expect(toolbarWrapper.getTitleText()).toMatch(viewWithFormat.expected) + } + }) + }) + + describe('using custom views', () => { + it('multi-year default only displays year', () => { + let calendar = initCalendar({ + views: { + multiYear: { + type: 'dayGrid', + duration: { years: 2 }, + }, + }, + initialView: 'multiYear', + initialDate: '2014-12-25', + titleRangeSeparator: ' - ', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('2014 - 2015') + }) + + it('multi-month default only displays month/year', () => { + let calendar = initCalendar({ + views: { + multiMonth: { + type: 'dayGrid', + duration: { months: 2 }, + }, + }, + initialView: 'multiMonth', + initialDate: '2014-12-25', + titleRangeSeparator: ' - ', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('December 2014 - January 2015') + }) + + it('multi-week default displays short full date', () => { + let calendar = initCalendar({ + views: { + multiWeek: { + type: 'dayGrid', + duration: { weeks: 2 }, + }, + }, + initialView: 'multiWeek', + initialDate: '2014-12-25', + titleRangeSeparator: ' - ', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toMatch(/Dec 21,? 2014 - Jan 3,? 2015/) + }) + + it('multi-day default displays short full date', () => { + let calendar = initCalendar({ + views: { + multiDay: { + type: 'dayGrid', + duration: { days: 2 }, + }, + }, + initialView: 'multiDay', + initialDate: '2014-12-25', + titleRangeSeparator: ' - ', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toMatch(/Dec 25 - 26,? 2014/) + }) + }) + + describe('when not all days are shown', () => { + it('doesn\'t include hidden days in the title', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-02-13', + weekends: false, + titleRangeSeparator: ' - ', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getTitleText()).toBe('Feb 13 - 17, 2017') // does not include Sunday + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/unselectAuto.ts b/fullcalendar-main/tests/src/legacy/unselectAuto.ts new file mode 100644 index 0000000..77128b0 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/unselectAuto.ts @@ -0,0 +1,95 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('unselectAuto', () => { + pushOptions({ + selectable: true, + initialDate: '2014-12-25', + initialView: 'dayGridMonth', + }) + + beforeEach(() => { + $('<div id="otherthing" />').appendTo('body') + }) + + afterEach(() => { + $('#otherthing').remove() + }) + + describe('when enabled', () => { + pushOptions({ + unselectAuto: true, + }) + + describe('when clicking away', () => { + it('unselects the current selection when clicking elsewhere in DOM', (done) => { + let isDone = false // hack against dragging continuing after destroy + let dayGridWrapper + let calendar = initCalendar({ + unselect(arg) { + if (!isDone) { + expect(dayGridWrapper.getHighlightEls().length).toBe(0) + expect('currentTarget' in arg.jsEvent).toBe(true) // a JS event + expect(typeof arg.view).toBe('object') + isDone = true + done() + } + }, + }) + dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + calendar.select('2014-12-01', '2014-12-03') + expect(dayGridWrapper.getHighlightEls().length).toBeGreaterThan(0) + + $('#otherthing') + .simulate('mousedown') + .simulate('mouseup') + .simulate('click') + }) + }) + + describe('when clicking another date', () => { + it('unselects the current selection when clicking elsewhere in DOM', (done) => { + let isDone = false // hack against dragging continuing after destroy + let dayGridWrapper + let calendar = initCalendar({ + unselect(arg) { + if (!isDone) { + expect(dayGridWrapper.getHighlightEls().length).toBe(0) + expect('currentTarget' in arg.jsEvent).toBe(true) // a JS event + expect(typeof arg.view).toBe('object') + isDone = true + done() + } + }, + }) + dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + calendar.select('2014-12-01', '2014-12-03') + expect(dayGridWrapper.getHighlightEls().length).toBeGreaterThan(0) + $(dayGridWrapper.getDayEl('2014-12-04')).simulate('drag') + }) + }) + }) + + describe('when disabled', () => { + pushOptions({ + unselectAuto: false, + }) + + it('keeps current selection when clicking elsewhere in DOM', (done) => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + calendar.select('2014-12-01', '2014-12-03') + expect(dayGridWrapper.getHighlightEls().length).toBeGreaterThan(0) + + $('#otherthing') + .simulate('mousedown') + .simulate('mouseup') + .simulate('click') + + setTimeout(() => { + expect(dayGridWrapper.getHighlightEls().length).toBeGreaterThan(0) + done() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/views-specific-options.ts b/fullcalendar-main/tests/src/legacy/views-specific-options.ts new file mode 100644 index 0000000..6f6da99 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/views-specific-options.ts @@ -0,0 +1,152 @@ +import { createPlugin } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' + +describe('view-specific options', () => { + pushOptions({ + headerToolbar: { + left: 'prev,next', + center: 'title', + right: 'dayGridMonth,dayGridWeek,dayGridDay,timeGridWeek,timeGridDay', + }, + initialView: 'dayGridMonth', + titleFormat() { return 'default' }, + views: { }, + }) + + function testEachView(viewsAndVals) { + $.each(viewsAndVals, (view: string, val) => { + currentCalendar.changeView(view) + expect($('h2')).toHaveText(val) + }) + } + + it('can target a specific view (dayGridMonth)', () => { + initCalendar({ + views: { + dayGridMonth: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'special!!!', + dayGridWeek: 'default', + dayGridDay: 'default', + timeGridWeek: 'default', + timeGridDay: 'default', + }) + }) + + it('can target a specific view (timeGridWeek)', () => { + initCalendar({ + views: { + timeGridWeek: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'default', + dayGridWeek: 'default', + dayGridDay: 'default', + timeGridWeek: 'special!!!', + timeGridDay: 'default', + }) + }) + + it('can target dayGrid views', () => { + initCalendar({ + views: { + dayGrid: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'special!!!', + dayGridWeek: 'special!!!', + dayGridDay: 'special!!!', + timeGridWeek: 'default', + timeGridDay: 'default', + }) + }) + + it('can target timeGrid views', () => { + initCalendar({ + views: { + timeGrid: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'default', + dayGridWeek: 'default', + dayGridDay: 'default', + timeGridWeek: 'special!!!', + timeGridDay: 'special!!!', + }) + }) + + it('can target week views', () => { + initCalendar({ + views: { + week: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'default', + dayGridWeek: 'special!!!', + dayGridDay: 'default', + timeGridWeek: 'special!!!', + timeGridDay: 'default', + }) + }) + + it('can target day views', () => { + initCalendar({ + views: { + day: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + testEachView({ + dayGridMonth: 'default', + dayGridWeek: 'default', + dayGridDay: 'special!!!', + timeGridWeek: 'default', + timeGridDay: 'special!!!', + }) + }) + + it('views that explicitly extend others inherit options', () => { + initCalendar({ + plugins: [ + dayGridPlugin, + createPlugin({ + name: 'test-plugin', + views: { + superBasic: { + type: 'dayGrid', // explicitly extend + content: 'hello world', + }, + }, + }), + ], + views: { + dayGrid: { + titleFormat() { return 'special!!!' }, + }, + }, + }) + + testEachView({ + superBasic: 'special!!!', + dayGridMonth: 'special!!!', + dayGridDay: 'special!!!', + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/weekLabel.ts b/fullcalendar-main/tests/src/legacy/weekLabel.ts new file mode 100644 index 0000000..93c3fb5 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/weekLabel.ts @@ -0,0 +1,52 @@ +import esLocale from '@fullcalendar/core/locales/es' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('weekText', () => { // TODO: rename file + pushOptions({ + weekNumbers: true, + }); + + ['timeGridWeek'].forEach((viewName) => { + describe('when views is ' + viewName, () => { + pushOptions({ + initialView: viewName, + }) + + it('renders correctly by default', () => { + let calendar = initCalendar() + expectWeekNumberTitle(calendar, 'W') + }) + + it('renders correctly when unspecified and when locale is customized', () => { + let calendar = initCalendar({ + locale: esLocale, + }) + expectWeekNumberTitle(calendar, 'Sm') + }) + + it('renders correctly when customized and LTR', () => { + let calendar = initCalendar({ + direction: 'ltr', + weekText: 'YO', + }) + expectWeekNumberTitle(calendar, 'YO') + }) + + it('renders correctly when customized and RTL', () => { + let calendar = initCalendar({ + direction: 'rtl', + weekText: 'YO', + }) + expectWeekNumberTitle(calendar, 'YO') + }) + }) + + function expectWeekNumberTitle(calendar, title) { + let viewWrapper = new TimeGridViewWrapper(calendar) + let text = viewWrapper.getHeaderWeekText() + .replace(/\d/g, '').trim() // remove the number + + expect(text).toBe(title) + } + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/weekNumberCalculation.ts b/fullcalendar-main/tests/src/legacy/weekNumberCalculation.ts new file mode 100644 index 0000000..56b6d5f --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/weekNumberCalculation.ts @@ -0,0 +1,66 @@ +import arLocale from '@fullcalendar/core/locales/ar' +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('weekNumberCalculation', () => { + pushOptions({ + weekNumbers: true, + }) + + describeOptions('initialView', { + 'when in day-grid': 'dayGridDay', + 'when in time-grid': 'timeGridDay', + }, (viewName) => { + let getWeekNumberText = viewName.match(/^dayGrid/) + ? (calendar) => new DayGridViewWrapper(calendar).dayGrid.getWeekNumberText(0) + : (calendar) => new TimeGridViewWrapper(calendar).getHeaderWeekText() + + it('should display the American standard when using \'local\'', () => { + let calendar = initCalendar({ + initialDate: '2013-11-23', // a Saturday + weekNumberCalculation: 'local', + }) + expect(getWeekNumber(calendar)).toBe(47) + }) + + it('should display a locale-specific local week number', () => { + let calendar = initCalendar({ + initialDate: '2013-11-23', // a Saturday + locale: arLocale, + weekNumberCalculation: 'local', + }) + expect(getWeekNumberText(calendar)).toMatch(/٤٨|48/) + }) + + // another local test, but to make sure it is different from ISO + it('should display the American standard when using \'local\'', () => { + let calendar = initCalendar({ + initialDate: '2013-11-17', // a Sunday + weekNumberCalculation: 'local', + }) + expect(getWeekNumber(calendar)).toBe(47) + }) + + it('should display ISO standard when using \'ISO\'', () => { + let calendar = initCalendar({ + initialDate: '2013-11-17', // a Sunday + weekNumberCalculation: 'ISO', + }) + expect(getWeekNumber(calendar)).toBe(46) + }) + + it('should display the calculated number when a custom function', () => { + let calendar = initCalendar({ + weekNumberCalculation() { + return 4 + }, + }) + expect(getWeekNumber(calendar)).toBe(4) + }) + + function getWeekNumber(calendar) { + let text = getWeekNumberText(calendar) || '' + return parseInt(text.replace(/\D/g, ''), 10) + } + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/weekNumbers.ts b/fullcalendar-main/tests/src/legacy/weekNumbers.ts new file mode 100644 index 0000000..6edec94 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/weekNumbers.ts @@ -0,0 +1,108 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('weekNumbers', () => { + describe('when using month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + fixedWeekCount: true, // will make 6 rows + }) + + describe('with default weekNumbers', () => { // which is false! + it('should not display week numbers at all', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getWeekNumberEls().length).toEqual(0) + }) + }) + + describe('with weekNumbers to false', () => { + pushOptions({ + weekNumbers: false, + }) + + it('should not display week numbers at all', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getWeekNumberEls().length).toEqual(0) + }) + }) + + describe('with weekNumbers to true', () => { + pushOptions({ + weekNumbers: true, + }) + + it('should display week numbers in the day cells only', () => { + let calendar = initCalendar() + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getWeekNumberEls().length).toBeGreaterThan(0) + }) + }) + }) + + describe('when using an timeGrid view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('with default weekNumbers', () => { + it('should not display week numbers at all', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + expect(viewWrapper.getHeaderWeekNumberLink()).toBeFalsy() + }) + }) + + describe('with weekNumbers to false', () => { + pushOptions({ + weekNumbers: false, + }) + + it('should not display week numbers at all', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + expect(viewWrapper.getHeaderWeekNumberLink()).toBeFalsy() + }) + }) + + describe('with weekNumbers to true', () => { + pushOptions({ + weekNumbers: true, + }) + + it('should display week numbers in the top left corner only', () => { + let calendar = initCalendar() + let viewWrapper = new TimeGridViewWrapper(calendar) + expect(viewWrapper.getHeaderWeekNumberLink()).toBeTruthy() + }) + }) + }) + + describe('when using in dayGrid view', () => { + pushOptions({ + initialView: 'dayGridWeek', + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5708 + it('displays events evenly', () => { + let calendar = initCalendar({ + weekNumbers: true, + initialDate: '2020-08-07', + events: [ + { title: 'Event 1', start: '2020-08-02' }, + { title: 'Event 2', start: '2020-08-03' }, + ], + }) + let gridWrapper = new DayGridViewWrapper(calendar).dayGrid + let eventEls = gridWrapper.getEventEls() + + expect( + Math.abs( + eventEls[0].getBoundingClientRect().top - + eventEls[1].getBoundingClientRect().top, + ), + ).toBeLessThan(1) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/weekViewRender.ts b/fullcalendar-main/tests/src/legacy/weekViewRender.ts new file mode 100644 index 0000000..1903759 --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/weekViewRender.ts @@ -0,0 +1,21 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('weekViewRender', () => { + const nowStr = '2018-05-28' // is a Monday + + pushOptions({ + now: nowStr, + initialView: 'timeGridWeek', + }) + + describe('verify th class for today', () => { + it('should have today class only on "today"', () => { + let calendar = initCalendar() + let headerWrapper = new TimeGridViewWrapper(calendar).header + let cellInfo = headerWrapper.getCellInfo() + + expect(cellInfo[1].date).toEqualDate(nowStr) + expect(cellInfo[1].isToday).toBe(true) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/legacy/weekends.ts b/fullcalendar-main/tests/src/legacy/weekends.ts new file mode 100644 index 0000000..3f5154c --- /dev/null +++ b/fullcalendar-main/tests/src/legacy/weekends.ts @@ -0,0 +1,21 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('when weekends option is set', () => { + it('should show sat and sun if true', () => { + let calendar = initCalendar({ + weekends: true, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getDayEls(0).length).toBeGreaterThan(0) // 0=sunday + expect(dayGridWrapper.getDayEls(6).length).toBeGreaterThan(0) // 6=saturday + }) + + it('should not show sat and sun if false', () => { + let calendar = initCalendar({ + weekends: false, + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + expect(dayGridWrapper.getDayEls(0).length).toBe(0) // 0=sunday + expect(dayGridWrapper.getDayEls(6).length).toBe(0) // 6=saturday + }) +}) diff --git a/fullcalendar-main/tests/src/lib/DayGridEventRenderUtils.ts b/fullcalendar-main/tests/src/lib/DayGridEventRenderUtils.ts new file mode 100644 index 0000000..c8ea472 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/DayGridEventRenderUtils.ts @@ -0,0 +1,50 @@ +import { DayGridViewWrapper } from './wrappers/DayGridViewWrapper.js' +import { DayGridWrapper } from './wrappers/DayGridWrapper.js' + +/* +opts: + - el (optional) + - row (optional) + - firstCol + - lastCol + - isStart + - isEnd +*/ +export function directionallyTestSeg(opts) { + let dayGridWrapper = new DayGridViewWrapper(currentCalendar).dayGrid + let el = opts.el ? $(opts.el) : dayGridWrapper.getEventEls()[0] + + let row = opts.row || 0 + let rowTds = dayGridWrapper.getDayElsInRow(row) + + expect(rowTds.length).toBeGreaterThan(1) + + let leftCol = opts.firstCol + let rightCol = opts.lastCol + let col + let td + + for (col = leftCol; col <= rightCol; col += 1) { + td = rowTds[col] + expect(el).toIntersectWith(td) + } + + for (col = 0; col < rowTds.length; col += 1) { + if (col < leftCol || col > rightCol) { + td = rowTds[col] + expect(el).not.toIntersectWith(td) + } + } + + if (opts.isStart) { + expect(el).toHaveClass(DayGridWrapper.EVENT_IS_START_CLASSNAME) + } else { + expect(el).not.toHaveClass(DayGridWrapper.EVENT_IS_START_CLASSNAME) + } + + if (opts.isEnd) { + expect(el).toHaveClass(DayGridWrapper.EVENT_IS_END_CLASSNAME) + } else { + expect(el).not.toHaveClass(DayGridWrapper.EVENT_IS_END_CLASSNAME) + } +} diff --git a/fullcalendar-main/tests/src/lib/EventDragUtils.ts b/fullcalendar-main/tests/src/lib/EventDragUtils.ts new file mode 100644 index 0000000..5e0e745 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/EventDragUtils.ts @@ -0,0 +1,42 @@ +import { getRectCenter, intersectRects } from './geom.js' +import { CalendarWrapper } from './wrappers/CalendarWrapper.js' + +/* +TODO: Don't rely on legacy simulateDrag +Given the rectangles of the origin and destination +slot or day area. +*/ +export function drag(rect0, rect1, debug?, eventEl?) { + if (!eventEl) { + eventEl = new CalendarWrapper(currentCalendar).getFirstEventEl() + } + + let eventRect = eventEl.getBoundingClientRect() + let point0 = getRectCenter( + intersectRects(eventRect, rect0), + ) + let point1 = getRectCenter(rect1) + let deferred = $.Deferred() + + $(eventEl).simulate('drag', { + point: point0, + end: point1, + debug, + }) + + currentCalendar.on('eventDrop', (arg) => { + deferred.resolve(arg) + }) + + currentCalendar.on('_noEventDrop', () => { + deferred.resolve(false) + }) + + return deferred.promise() +} + +// makes the setTimeout's work. +// also makes the tests faster. +pushOptions({ + dragRevertDuration: 0, +}) diff --git a/fullcalendar-main/tests/src/lib/EventResizeUtils.ts b/fullcalendar-main/tests/src/lib/EventResizeUtils.ts new file mode 100644 index 0000000..77ffbbf --- /dev/null +++ b/fullcalendar-main/tests/src/lib/EventResizeUtils.ts @@ -0,0 +1,39 @@ +import { getRectCenter, subtractPoints, addPoints } from './geom.js' +import { CalendarWrapper } from './wrappers/CalendarWrapper.js' + +export function resize(point0, point1, fromStart?, debug?) { + let eventEl = new CalendarWrapper(currentCalendar).getFirstEventEl() + + let $resizerEl = $(eventEl).find( + '.' + (fromStart ? CalendarWrapper.EVENT_START_RESIZER_CLASSNAME : CalendarWrapper.EVENT_END_RESIZER_CLASSNAME), + ).css('display', 'block') // usually only displays on hover. force display + + let resizerRect = $resizerEl[0].getBoundingClientRect() + let resizerCenter = getRectCenter(resizerRect) + + let vector = subtractPoints( + resizerCenter, + point0, + ) + let endPoint = addPoints( + point1, + vector, + ) + let deferred = $.Deferred() + + $resizerEl.simulate('drag', { + point: resizerCenter, + end: endPoint, + debug, + }) + + currentCalendar.on('eventResize', (arg) => { + deferred.resolve(arg) + }) + + currentCalendar.on('_noEventResize', () => { + deferred.resolve(false) + }) + + return deferred.promise() +} diff --git a/fullcalendar-main/tests/src/lib/ListenerCounter.ts b/fullcalendar-main/tests/src/lib/ListenerCounter.ts new file mode 100644 index 0000000..cbfe9e0 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/ListenerCounter.ts @@ -0,0 +1,63 @@ +const IGNORED_EVENTS = { + load: true, // ignore when jQuery detaches the load event from the window +} + +export class ListenerCounter { + el: HTMLElement + delta = 0 + jQueryStartCount = 0 + + constructor(el) { + this.el = el + } + + startWatching() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let t = this + let el = t.el + let origAddEventListened = el.addEventListener + let origRemoveEventListener = el.removeEventListener + + el.addEventListener = (eventName, ...otherArgs) => { + if (!IGNORED_EVENTS[eventName]) { + t.delta += 1 + } + return origAddEventListened.call(el, eventName, ...otherArgs) + } + + el.removeEventListener = (eventName, ...otherArgs) => { + if (!IGNORED_EVENTS[eventName]) { + t.delta -= 1 + } + return origRemoveEventListener.call(el, eventName, ...otherArgs) + } + + this.jQueryStartCount = countJqueryListeners(el) + } + + stopWatching() { + delete this.el.addEventListener + delete this.el.removeEventListener + + return this.computeDelta() + } + + computeDelta() { + return this.delta + (countJqueryListeners(this.el) - this.jQueryStartCount) + } +} + +function countJqueryListeners(el) { + let hash = getJqueryHandlerHash(el) + let cnt = 0 + + $.each(hash, (name, handlers) => { + cnt += handlers.length + }) + + return cnt +} + +function getJqueryHandlerHash(el) { + return $._data($(el)[0], 'events') || {} +} diff --git a/fullcalendar-main/tests/src/lib/ViewDateUtils.ts b/fullcalendar-main/tests/src/lib/ViewDateUtils.ts new file mode 100644 index 0000000..2fb562b --- /dev/null +++ b/fullcalendar-main/tests/src/lib/ViewDateUtils.ts @@ -0,0 +1,13 @@ +export function expectRenderRange(start, end) { + let { dateProfile } = currentCalendar.getCurrentData() // not a great way to get this info + + expect(dateProfile.renderRange.start).toEqualDate(start) + expect(dateProfile.renderRange.end).toEqualDate(end) +} + +export function expectActiveRange(start, end) { + let currentView = currentCalendar.view + + expect(currentView.activeStart).toEqualDate(start) + expect(currentView.activeEnd).toEqualDate(end) +} diff --git a/fullcalendar-main/tests/src/lib/ViewRenderUtils.ts b/fullcalendar-main/tests/src/lib/ViewRenderUtils.ts new file mode 100644 index 0000000..00bd695 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/ViewRenderUtils.ts @@ -0,0 +1,43 @@ +import { addDays } from '@fullcalendar/core/internal' +import { formatIsoDay } from './datelib-utils.js' +import { CalendarWrapper } from './wrappers/CalendarWrapper.js' + +export function expectDayRange(start, end) { + if (typeof start === 'string') { + expect(start.indexOf('T')).toBe(-1) + start = new Date(start) + } + + if (typeof end === 'string') { + expect(end.indexOf('T')).toBe(-1) + end = new Date(end) + } + + let dayBefore = addDays(start, -1) + expectDay(dayBefore, false) + + let date = start + while (date < end) { // eslint-disable-line + expectDay(date, true) + date = addDays(date, 1) + } + + // `date` is now the first day after the range + expectDay(date, false) +} + +export function expectDay(date, bool) { + if (typeof date === 'string') { + expect(date.indexOf('T')).toBe(-1) + date = new Date(date) + } + + let calendarWrapper = new CalendarWrapper(currentCalendar) + let dayEl = calendarWrapper.getDateCellEl(formatIsoDay(date)) + + if (bool) { + expect(dayEl).toBeTruthy() + } else { + expect(dayEl).toBeFalsy() + } +} diff --git a/fullcalendar-main/tests/src/lib/date-matchers.ts b/fullcalendar-main/tests/src/lib/date-matchers.ts new file mode 100644 index 0000000..69a3796 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/date-matchers.ts @@ -0,0 +1,97 @@ +import { parseUtcDate, parseLocalDate } from './date-parsing.js' + +beforeEach(() => { + jasmine.addMatchers({ + + toEqualDate() { + return { + compare(actual, expected) { + let result + + if (typeof expected === 'string') { + expected = parseUtcDate(expected) + } + + if (!(actual instanceof Date)) { + result = { + pass: false, + message: 'Actual value ' + actual + ' needs to be an instance of a Date', + } + } else if (!(expected instanceof Date)) { + result = { + pass: false, + message: 'Expected value ' + expected + ' needs to be an instance of a Date', + } + } else if (actual.valueOf() !== expected.valueOf()) { + result = { + pass: false, + message: 'Date ' + actual.toUTCString() + ' does not equal ' + expected.toUTCString(), + } + } else { + result = { pass: true } + } + + return result + }, + } + }, + + toEqualLocalDate() { + return { + compare(actual, expected) { + let result + + if (typeof expected === 'string') { + expected = parseLocalDate(expected) + } + + if (!(actual instanceof Date)) { + result = { + pass: false, + message: 'Actual value ' + actual + ' needs to be an instance of a Date', + } + } else if (!(expected instanceof Date)) { + result = { + pass: false, + message: 'Expected value ' + expected + ' needs to be an instance of a Date', + } + } else if (actual.valueOf() !== expected.valueOf()) { + result = { + pass: false, + message: 'Date ' + actual.toString() + ' does not equal ' + expected.toString(), + } + } else { + result = { pass: true } + } + + return result + }, + } + }, + + toEqualNow() { + return { + compare(actual) { + let result + + if (!(actual instanceof Date)) { + result = { + pass: false, + message: 'Actual value ' + actual + ' needs to be an instance of a Date', + } + } else if (Math.abs(actual.valueOf() - new Date().valueOf()) > 1000) { + result = { + pass: false, + message: 'Date ' + actual.toUTCString() + ' is not close enough to now', + } + } else { + result = { pass: true } + } + + return result + }, + } + }, + + }) +}) diff --git a/fullcalendar-main/tests/src/lib/date-math.ts b/fullcalendar-main/tests/src/lib/date-math.ts new file mode 100644 index 0000000..782c07a --- /dev/null +++ b/fullcalendar-main/tests/src/lib/date-math.ts @@ -0,0 +1,27 @@ +export function startOfLocalDay(date) { + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + ) +} + +export function addLocalDays(date, n) { + let newDate = new Date(date.valueOf()) + newDate.setDate(newDate.getDate() + n) + return newDate +} + +export function startOfUtcDay(date) { + return new Date(Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + )) +} + +export function addUtcDays(date, n) { + let newDate = new Date(date.valueOf()) + newDate.setUTCDate(newDate.getUTCDate() + n) + return newDate +} diff --git a/fullcalendar-main/tests/src/lib/date-parsing.ts b/fullcalendar-main/tests/src/lib/date-parsing.ts new file mode 100644 index 0000000..f2f9aec --- /dev/null +++ b/fullcalendar-main/tests/src/lib/date-parsing.ts @@ -0,0 +1,44 @@ +/* +NOTE: can't use Date.parse or new Date(str) to parse strings without timezones: +https://stackoverflow.com/a/33909265/96342 +*/ + +/* +Given an ISO8601 string with no timezone part, parses as UTC +*/ +export function parseUtcDate(str) { + let parts = str.split(/\D/) + + if (parts.length > 6) { // has timezone info. will correctly parse + return new Date(str) + } + + return new Date(Date.UTC( + parseInt(parts[0], 10), + parseInt(parts[1], 10) - 1, + parts[2] ? parseInt(parts[2], 10) : 0, + parts[3] ? parseInt(parts[3], 10) : 0, + parts[4] ? parseInt(parts[4], 10) : 0, + parts[5] ? parseInt(parts[5], 10) : 0, + )) +} + +/* +Given an ISO8601 string with no timezone part, parses as local +*/ +export function parseLocalDate(str) { + let parts = str.split(/\D/) + + if (parts.length > 6) { // has timezone info + throw new Error('Don\'t pass timezone info to parseLocalDate. Use parseUtcDate instead.') + } else { + return new Date( + parseInt(parts[0], 10), + parseInt(parts[1], 10) - 1, + parts[2] ? parseInt(parts[2], 10) : 0, + parts[3] ? parseInt(parts[3], 10) : 0, + parts[4] ? parseInt(parts[4], 10) : 0, + parts[5] ? parseInt(parts[5], 10) : 0, + ) + } +} diff --git a/fullcalendar-main/tests/src/lib/datelib-utils.ts b/fullcalendar-main/tests/src/lib/datelib-utils.ts new file mode 100644 index 0000000..9a329b6 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/datelib-utils.ts @@ -0,0 +1,66 @@ +export function formatIsoTimeZoneOffset(date) { + let minutes = date.getTimezoneOffset() + let sign = minutes < 0 ? '+' : '-' // whaaa + let abs = Math.abs(minutes) + let hours = Math.floor(abs / 60) + let mins = Math.round(abs % 60) + + return sign + pad(hours) + ':' + pad(mins) +} + +export function formatPrettyTimeZoneOffset(date) { + let minutes = date.getTimezoneOffset() + let sign = minutes < 0 ? '+' : '-' // whaaa + let abs = Math.abs(minutes) + let hours = Math.floor(abs / 60) + let mins = Math.round(abs % 60) + + return 'GMT' + sign + hours + (mins ? ':' + pad(mins) : '') +} + +function pad(n) { // always pads for 2 digits + return n < 10 ? '0' + n : '' + n +} + +export function formatIsoDay(date) { + return date.toISOString().replace(/T.*/, '') +} + +export function formatIsoTime(date) { + return pad(date.getUTCHours()) + ':' + + pad(date.getUTCMinutes()) + ':' + + pad(date.getUTCSeconds()) +} + +export function formatIsoWithoutTz(date) { + return date.toISOString().replace(/(Z|[-+]\d\d:\d\d)$/, '').replace('.000', '') +} + +export function parseIsoAsUtc(s) { + if (s.length <= 10) { + s += 'T00:00:00Z' + } else if (s.indexOf('Z') === -1) { + s += 'Z' + } + + let d = new Date(s) + + if (isNaN(d.valueOf())) { + throw new Error(s + ' is not valid date input') + } + + return d +} + +export function ensureDate(input) { + if (input instanceof Date) { + return input + } + if (typeof input === 'string') { + return parseIsoAsUtc(input) + } + if (typeof input === 'number') { + return new Date(input) + } + throw new Error(input + ' is invalid date input') +} diff --git a/fullcalendar-main/tests/src/lib/dnd-resize-utils.ts b/fullcalendar-main/tests/src/lib/dnd-resize-utils.ts new file mode 100644 index 0000000..a2a2e9a --- /dev/null +++ b/fullcalendar-main/tests/src/lib/dnd-resize-utils.ts @@ -0,0 +1,255 @@ +import { parseMarker, addDays } from '@fullcalendar/core/internal' +import { formatIsoDay } from './datelib-utils.js' +import { TimeGridViewWrapper } from './wrappers/TimeGridViewWrapper.js' +import { DayGridViewWrapper } from './wrappers/DayGridViewWrapper.js' +import { CalendarWrapper } from './wrappers/CalendarWrapper.js' + +export function testEventDrag(options, dropDate, expectSuccess, callback, eventClassName?) { + options.editable = true + options.viewDidMount = () => { + setTimeout(() => { + let calendar = currentCalendar + let isDraggingExternal = false + let $dayEl + let $eventEl + let $dragEl + let slatIndex + let $slatEl + let dx + let dy + let allowed + let dropDateMeta + let dropDateHasTime + + if (typeof dropDate === 'string') { + dropDateMeta = parseMarker(dropDate) + dropDateHasTime = !dropDateMeta.isTimeUnspecified + dropDate = dropDateMeta.marker + } else { + dropDateHasTime = true + } + + let calendarWrapper = new CalendarWrapper(calendar) + $eventEl = eventClassName ? $(`.${eventClassName}:first`) : $(calendarWrapper.getFirstEventEl()) + expect($eventEl.length).toBe(1) + + if (dropDateHasTime) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + $dragEl = $eventEl.find('.' + CalendarWrapper.EVENT_TIME_CLASSNAME) + $dayEl = $(timeGridWrapper.getDayEls(dropDate)) + slatIndex = dropDate.getUTCHours() * 2 + (dropDate.getUTCMinutes() / 30) // assumes slotDuration:'30:00' + $slatEl = $(timeGridWrapper.getSlotElByIndex(slatIndex)) + expect($slatEl.length).toBe(1) + dy = $slatEl.offset().top - $eventEl.offset().top + } else { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $dragEl = $eventEl.find('.' + CalendarWrapper.EVENT_TITLE_CLASSNAME) + $dayEl = $(dayGridWrapper.getDayEl(dropDate)) + dy = $dayEl.offset().top - $eventEl.offset().top + } + + if (!$dragEl.length) { + isDraggingExternal = true + $dragEl = $eventEl // well, not really an "event" element anymore + } + + expect($dragEl.length).toBe(1) + expect($dayEl.length).toBe(1) + dx = $dayEl.offset().left - $eventEl.offset().left + + $dragEl.simulate('drag', { + dx, + dy, + onBeforeRelease() { + allowed = calendarWrapper.isAllowingDragging() + expect(allowed).toBe(expectSuccess) + }, + onRelease() { + let eventObj + let successfulDrop + + if (!isDraggingExternal) { // if dragging an event within the calendar, check dates + if (eventClassName) { + eventObj = calendar.getEvents().filter((o) => o.classNames.join(' ') === eventClassName)[0] + } else { + eventObj = calendar.getEvents()[0] + } + + if (dropDateHasTime) { // dropped on a slot + successfulDrop = eventObj.start.valueOf() === dropDate.valueOf() // compare exact times + } else { // dropped on a whole day + // only compare days + successfulDrop = formatIsoDay(eventObj.start) === formatIsoDay(dropDate) + } + + expect(successfulDrop).toBe(allowed) + expect(successfulDrop).toBe(expectSuccess) + } + + callback() + }, + }) + }, 0) + } + initCalendar(options) +} + +export function testEventResize(options, resizeDate, expectSuccess, callback, eventClassName?) { + options.editable = true + options.viewDidMount = () => { + setTimeout(() => { + let calendar = currentCalendar + let $lastDayEl + let lastSlatIndex + let $lastSlatEl + let $eventEl + let $dragEl + let dx + let dy + let allowed + let resizeDateMeta + let resizeDateHasTime + + if (typeof resizeDate === 'string') { + resizeDateMeta = parseMarker(resizeDate) + resizeDateHasTime = !resizeDateMeta.isTimeUnspecified + resizeDate = resizeDateMeta.marker + } else { + resizeDateHasTime = true + } + + let calendarWrapper = new CalendarWrapper(calendar) + $eventEl = eventClassName ? $(`.${eventClassName}:first`) : (() => { + let eventEls = calendarWrapper.getEventEls() + return $(eventEls[eventEls.length - 1]) // the last one + })() + + $dragEl = $eventEl.find('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME) + .css('display', 'block') // resizer usually only shows on hover. force-show it + + if (resizeDateHasTime) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + $lastDayEl = $(timeGridWrapper.getDayEls(resizeDate)) + lastSlatIndex = resizeDate.getUTCHours() * 2 + (resizeDate.getUTCMinutes() / 30) // assumes slotDuration:'30:00' + $lastSlatEl = $(timeGridWrapper.getSlotElByIndex(lastSlatIndex - 1)) + expect($lastSlatEl.length).toBe(1) + dy = $lastSlatEl.offset().top + $lastSlatEl.outerHeight() - ($eventEl.offset().top + $eventEl.outerHeight()) + } else { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $lastDayEl = $(dayGridWrapper.getDayEl(addDays(resizeDate, -1))) + dy = $lastDayEl.offset().top - $eventEl.offset().top + } + + expect($lastDayEl.length).toBe(1) + expect($eventEl.length).toBe(1) + expect($dragEl.length).toBe(1) + dx = $lastDayEl.offset().left + $lastDayEl.outerWidth() - 2 - ($eventEl.offset().left + $eventEl.outerWidth()) + + $dragEl.simulate('drag', { + dx, + dy, + onBeforeRelease() { + allowed = calendarWrapper.isAllowingDragging() + }, + onRelease() { + let eventObj + let successfulDrop + + if (eventClassName) { + eventObj = calendar.getEvents().filter((o) => o.classNames.join(' ') === eventClassName)[0] + } else { + eventObj = calendar.getEvents()[0] + } + + successfulDrop = eventObj.end && eventObj.end.valueOf() === resizeDate.valueOf() + + expect(allowed).toBe(successfulDrop) + expect(allowed).toBe(expectSuccess) + expect(successfulDrop).toBe(expectSuccess) + callback() + }, + }) + }, 0) + } + initCalendar(options) +} + +export function testSelection(options, start, end, expectSuccess, callback) { + let successfulSelection = false + let $firstDayEl + let $lastDayEl + let firstSlatIndex + let lastSlatIndex + let $firstSlatEl + let $lastSlatEl + let dx + let dy + let $dragEl + let allowed + let allDay = false + let meta + + if (typeof start === 'string') { + meta = parseMarker(start) + allDay = allDay || meta.isTimeUnspecified + start = meta.marker + } + if (typeof end === 'string') { + meta = parseMarker(end) + allDay = allDay || meta.isTimeUnspecified + end = meta.marker + } + + options.selectable = true + options.select = (arg) => { + successfulSelection = + arg.allDay === allDay && + arg.start.valueOf() === start.valueOf() && + arg.end.valueOf() === end.valueOf() + } + spyOn(options, 'select').and.callThrough() + + let calendar = initCalendar(options) + let calendarWrapper = new CalendarWrapper(calendar) + + if (!allDay) { + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + $firstDayEl = $(timeGridWrapper.getDayEls(start)) + $lastDayEl = $(timeGridWrapper.getDayEls(end)) + firstSlatIndex = start.getUTCHours() * 2 + (start.getUTCMinutes() / 30) // assumes slotDuration:'30:00' + lastSlatIndex = end.getUTCHours() * 2 + (end.getUTCMinutes() / 30) - 1 // assumes slotDuration:'30:00' + $firstSlatEl = $(timeGridWrapper.getSlotElByIndex(firstSlatIndex)) + $lastSlatEl = $(timeGridWrapper.getSlotElByIndex(lastSlatIndex)) + expect($firstSlatEl.length).toBe(1) + expect($lastSlatEl.length).toBe(1) + dy = $lastSlatEl.offset().top - $firstSlatEl.offset().top + $dragEl = $firstSlatEl + } else { + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $firstDayEl = $(dayGridWrapper.getDayEl(start)) + $lastDayEl = $(dayGridWrapper.getDayEl(new Date(end.valueOf() - 1))) // inclusive + dy = $lastDayEl.offset().top - $firstDayEl.offset().top + $dragEl = $firstDayEl + } + + expect($firstDayEl.length).toBe(1) + expect($lastDayEl.length).toBe(1) + dx = $lastDayEl.offset().left - $firstDayEl.offset().left + + $dragEl.simulate('drag', { + dx, + dy, + onBeforeRelease() { + allowed = calendarWrapper.isAllowingDragging() + }, + onRelease() { + if (expectSuccess) { + expect(options.select).toHaveBeenCalled() + } + expect(expectSuccess).toBe(allowed) + expect(expectSuccess).toBe(successfulSelection) + expect(allowed).toBe(successfulSelection) + callback() + }, + }) +} diff --git a/fullcalendar-main/tests/src/lib/dom-geom.ts b/fullcalendar-main/tests/src/lib/dom-geom.ts new file mode 100644 index 0000000..15092cd --- /dev/null +++ b/fullcalendar-main/tests/src/lib/dom-geom.ts @@ -0,0 +1,325 @@ +import { + isRect, isRectMostlyAbove, isRectMostlyLeft, isRectMostlyBounded, + isRectMostlyHBounded, isRectMostlyVBounded, rectsIntersect, rectContainersOther, +} from './geom.js' + +// fix bug with jQuery 3 returning 0 height for <td> elements in the IE's +['height', 'outerHeight'].forEach((methodName) => { + let orig = $.fn[methodName] + + $.fn[methodName] = function () { // eslint-disable-line func-names + if (!arguments.length && this.is('td')) { // eslint-disable-line prefer-rest-params + return this[0].getBoundingClientRect().height + } + return orig.apply(this, arguments) // eslint-disable-line prefer-rest-params + } +}) + +export function getBoundingRects(els) { + return $(els).map((i, node) => getBoundingRect(node)).get() +} + +export function getBoundingRect(el) { + el = $(el) + return $.extend({}, el[0].getBoundingClientRect(), { + node: el, // very useful for debugging + }) +} + +export function anyElsIntersect(els) { + let rects = els.map((el) => el.getBoundingClientRect()) + + for (let i = 0; i < rects.length; i += 1) { + for (let j = i + 1; j < rects.length; j += 1) { + if (rectsIntersect(rects[i], rects[j])) { + return [els[i], els[j]] + } + } + } + + return false +} + +export function anyElsObscured(els) { + let rects = els.map((el) => el.getBoundingClientRect()) + + for (let i = 0; i < rects.length; i += 1) { + for (let j = 0; j < rects.length; j += 1) { + if (i !== j && rectContainersOther(rects[i], rects[j])) { + return [els[i], els[j]] + } + } + } + + return false +} + +export function getLeadingBoundingRect(els, direction = 'ltr') { + els = $(els) + expect(els.length).toBeGreaterThan(0) + let best = null + els.each((i, node) => { + const rect = getBoundingRect(node) + if (!best) { + best = rect + } else if (direction === 'rtl') { + if (rect.right > best.right) { + best = rect + } + } else if (rect.left < best.left) { + best = rect + } + }) + return best +} + +export function getTrailingBoundingRect(els, direction = 'ltr') { + els = $(els) + expect(els.length).toBeGreaterThan(0) + let best = null + els.each((i, node) => { + const rect = getBoundingRect(node) + if (!best) { + best = rect + } else if (direction === 'rtl') { + if (rect.left < best.left) { + best = rect + } + } else if (rect.right > best.right) { + best = rect + } + }) + return best +} + +export function sortBoundingRects(els, direction = 'ltr') { + els = $(els) // TODO: un-jquery-ify + const rects = els.map((i, node) => getBoundingRect(node)).get() + rects.sort((a, b) => { + if (direction === 'rtl') { + return b.right - a.right + } + return a.left - b.left + }) + return rects +} + +// given an element, returns its bounding box. given a rect, returns the rect. +function massageRect(input) { + if (isRect(input)) { + return input + } + return getBoundingRect(input) +} + +// Jasmine Adapters +// -------------------------------------------------------------------------------------------------- + +beforeEach(() => { + jasmine.addMatchers({ + + toBeMostlyAbove() { + return { + compare(subject, other) { + const result = { pass: isRectMostlyAbove(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect is not mostly above the second' + } + return result + }, + } + }, + + toBeMostlyBelow() { + return { + compare(subject, other) { + const result = { pass: !isRectMostlyAbove(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect is not mostly below the second' + } + return result + }, + } + }, + + toBeMostlyLeftOf() { + return { + compare(subject, other) { + const result = { pass: isRectMostlyLeft(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect is not mostly left of the second' + } + return result + }, + } + }, + + toBeMostlyRightOf() { + return { + compare(subject, other) { + const result = { pass: !isRectMostlyLeft(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect is not mostly right of the second' + } + return result + }, + } + }, + + toBeMostlyBoundedBy() { + return { + compare(subject, other) { + const result = { pass: isRectMostlyBounded(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect is not mostly bounded by the second' + } + return result + }, + } + }, + + toBeMostlyHBoundedBy() { + return { + compare(subject, other) { + const result = { pass: isRectMostlyHBounded(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect does not mostly horizontally bound the second' + } + return result + }, + } + }, + + toBeMostlyVBoundedBy() { + return { + compare(subject, other) { + const result = { pass: isRectMostlyVBounded(massageRect(subject), massageRect(other)), message: '' } + if (!result.pass) { + result.message = 'first rect does not mostly vertically bound the second' + } + return result + }, + } + }, + + toBeBoundedBy() { + return { + compare(actual, expected) { + let outer = massageRect(expected) + let inner = massageRect(actual) + let result = { + message: '', + pass: outer && inner && + inner.left >= outer.left && + inner.right <= outer.right && + inner.top >= outer.top && + inner.bottom <= outer.bottom, + } + if (!result.pass) { + result.message = 'Element does not bound other element' + } + return result + }, + } + }, + + toBeLeftOf() { + return { + compare(actual, expected) { + let subjectBounds = massageRect(actual) + let otherBounds = massageRect(expected) + let result = { + message: '', + pass: subjectBounds && otherBounds && + Math.round(subjectBounds.right) <= Math.round(otherBounds.left) + 2, + // need to round because IE was giving weird fractions + } + if (!result.pass) { + result.message = 'Element is not to the left of the other element' + } + return result + }, + } + }, + + toBeRightOf() { + return { + compare(actual, expected) { + let subjectBounds = massageRect(actual) + let otherBounds = massageRect(expected) + let result = { + message: '', + pass: subjectBounds && otherBounds && + Math.round(subjectBounds.left) >= Math.round(otherBounds.right) - 2, + // need to round because IE was giving weird fractions + } + if (!result.pass) { + result.message = 'Element is not to the right of the other element' + } + return result + }, + } + }, + + toBeAbove() { + return { + compare(actual, expected) { + let subjectBounds = massageRect(actual) + let otherBounds = massageRect(expected) + let result = { + message: '', + pass: subjectBounds && otherBounds && + Math.round(subjectBounds.bottom) <= Math.round(otherBounds.top) + 2, + // need to round because IE was giving weird fractions + } + if (!result.pass) { + result.message = 'Element is not above the other element' + } + return result + }, + } + }, + + toBeBelow() { + return { + compare(actual, expected) { + let subjectBounds = massageRect(actual) + let otherBounds = massageRect(expected) + let result = { + message: '', + pass: subjectBounds && otherBounds && + Math.round(subjectBounds.top) >= Math.round(otherBounds.bottom) - 2, + // need to round because IE was giving weird fractions + } + if (!result.pass) { + result.message = 'Element is not below the other element' + } + return result + }, + } + }, + + toIntersectWith() { + return { + compare(actual, expected) { + let subjectBounds = massageRect(actual) + let otherBounds = massageRect(expected) + let result = { + message: '', + pass: subjectBounds && otherBounds && + subjectBounds.right - 1 > otherBounds.left && + subjectBounds.left + 1 < otherBounds.right && + subjectBounds.bottom - 1 > otherBounds.top && + subjectBounds.top + 1 < otherBounds.bottom, + // +/-1 because of zoom + } + if (!result.pass) { + result.message = 'Element does not intersect with other element' + } + return result + }, + } + }, + + }) +}) diff --git a/fullcalendar-main/tests/src/lib/dom-misc.ts b/fullcalendar-main/tests/src/lib/dom-misc.ts new file mode 100644 index 0000000..85a48f3 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/dom-misc.ts @@ -0,0 +1,61 @@ +export const RED_REGEX = /red|rgb\(255,\s*0,\s*0\)/ +export const GREEN_REGEX = /green|rgb\(0,\s*255,\s*0\)/ +export const BLUE_REGEX = /blue|rgb\(0,\s*0,\s*255\)/ + +export function getStockScrollbarWidths(direction) { + let el = $('<div><div style="position:relative"/></div>') + .css({ + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: direction || 'ltr', + }) + .appendTo('body') + + let elRect = el[0].getBoundingClientRect() + let innerEl = el.children() + let innerElRect = innerEl[0].getBoundingClientRect() + + let girths = { + left: innerElRect.left - elRect.left, + right: elRect.left + elRect.width - innerElRect.left, + top: innerElRect.top - elRect.top, + bottom: elRect.top + elRect.height - innerElRect.top, + } + + el.remove() + + return girths +} + +export function filterVisibleEls(els) { + return els.filter((el) => { + let $el = $(el) + return $el.is(':visible') && $el.css('visibility') !== 'hidden' + }) +} + +// TODO: make sure these matchers are loaded globally first + +beforeEach(() => { + jasmine.addMatchers({ + + toHaveScrollbars() { + return { + compare(actual) { + let elm = $(actual) + let result = { + pass: elm[0].scrollWidth - 1 > elm[0].clientWidth || // -1 !!! + elm[0].scrollHeight - 1 > elm[0].clientHeight, // -1 !!! + } + // !!! - IE was reporting a scrollWidth/scrollHeight 1 pixel taller than what it was :( + return result + }, + } + }, + + }) +}) diff --git a/fullcalendar-main/tests/src/lib/dst-dead-zone.ts b/fullcalendar-main/tests/src/lib/dst-dead-zone.ts new file mode 100644 index 0000000..b2041b4 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/dst-dead-zone.ts @@ -0,0 +1,71 @@ +/* +Some hours don't exist in local time when a daylight-savings shift happpens. +This time is called a "dead zone". +If possible, this function returns an array of two dates, +the date just before the dead zone (with a 999 millisecond time) +and the date just after the dead zone. + +I apologize for the unreadability of this code. It was written a long time ago: +https://github.com/arshaw/xdate/blob/master/test/old.js +*/ +export function getDSTDeadZone() { + let dstDates = getDSTDates() + + if (dstDates) { + let prior = new Date(dstDates[0].valueOf() - 1) + + if (Math.abs(dstDates[0].getHours() - prior.getHours()) > 1) { + return [prior, dstDates[0]] + } + + prior = new Date(dstDates[1].valueOf() - 1) + + if (Math.abs(dstDates[1].getHours() - prior.getHours()) > 1) { + return [prior, dstDates[1]] + } + } + + return null +} + +function getDSTDates() { + let MS_DAY = 86400000 + let res = [] + let d0 = new Date() + let overAYear = new Date(+d0) + + overAYear.setFullYear(overAYear.getFullYear() + 1) + overAYear = new Date(overAYear.valueOf() + MS_DAY) + + while (d0 < overAYear) { + let d1 = new Date(d0.valueOf() + MS_DAY) + if (d0.getTimezoneOffset() !== d1.getTimezoneOffset()) { + res.push(new Date(narrowDSTDate(+d0, +d1))) + if (res.length === 2) { + break + } + } + d0 = d1 + } + + return res.length === 2 ? res : null +} + +function narrowDSTDate(start, end) { + if (end <= start + 1) { + return end + } + + let mid = start + Math.floor((end - start) / 2) + let midTZO = new Date(mid).getTimezoneOffset() + let startTZO = new Date(start).getTimezoneOffset() + let endTZO = new Date(end).getTimezoneOffset() + + if (midTZO === startTZO) { + return narrowDSTDate(mid, end) + } if (midTZO === endTZO) { + return narrowDSTDate(start, mid) + } + + return null +} diff --git a/fullcalendar-main/tests/src/lib/geom.ts b/fullcalendar-main/tests/src/lib/geom.ts new file mode 100644 index 0000000..17db1b4 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/geom.ts @@ -0,0 +1,112 @@ +export function getRectCenter(rect) { + return buildPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ) +} + +export function intersectRects(rect0, rect1) { + return buildRectViaEdges( + Math.max(rect0.left, rect1.left), + Math.max(rect0.top, rect1.top), + Math.min(rect0.right, rect1.right), + Math.min(rect0.bottom, rect1.bottom), + ) +} + +export function rectsIntersect(rect0, rect1) { + return rect0.left < rect1.right && rect0.right > rect1.left && rect0.top < rect1.bottom && rect0.bottom > rect1.top +} + +export function rectContainersOther(rect0, rect1) { // rect0 contains rect1? + return rect1.left >= rect0.left && rect1.right <= rect0.right && rect1.top >= rect0.top && rect1.bottom <= rect0.bottom +} + +export function joinRects(rect1, rect2) { + return { + left: Math.min(rect1.left, rect2.left), + right: Math.max(rect1.right, rect2.right), + top: Math.min(rect1.top, rect2.top), + bottom: Math.max(rect1.bottom, rect2.bottom), + } +} + +function buildRectViaEdges(left, top, right, bottom) { + return { + left, + top, + width: right - left, + height: bottom - top, + right, + bottom, + } +} + +function buildPoint(left, top) { + return { + left, + top, + } +} + +export function subtractPoints(point1, point0) { + return buildPoint( + point1.left - point0.left, + point1.top - point0.top, + ) +} + +export function addPoints(point0, point1) { + return buildPoint( + point0.left + point1.left, + point0.top + point1.top, + ) +} + +// in most situations you can just use the rect directly, since the interface is a superset +export function getRectTopLeft(rect) { + return buildPoint(rect.left, rect.top) +} + +export function isRect(input) { + return typeof input === 'object' && 'left' in input && 'right' in input && 'top' in input && 'bottom' in input +} + +export function isRectMostlyAbove(subjectRect, otherRect) { + return (subjectRect.bottom - otherRect.top) < // overlap is less than + ((subjectRect.bottom - subjectRect.top) / 2) // half the height +} + +export function isRectMostlyLeft(subjectRect, otherRect) { + return (subjectRect.right - otherRect.left) < // overlap is less then + ((subjectRect.right - subjectRect.left) / 2) // half the width +} + +export function isRectMostlyBounded(subjectRect, boundRect) { + return isRectMostlyHBounded(subjectRect, boundRect) && + isRectMostlyVBounded(subjectRect, boundRect) +} + +export function isRectMostlyHBounded(subjectRect, boundRect) { + return (Math.min(subjectRect.right, boundRect.right) - + Math.max(subjectRect.left, boundRect.left)) > // overlap area is greater than + ((subjectRect.right - subjectRect.left) / 2) // half the width +} + +export function isRectMostlyVBounded(subjectRect, boundRect) { + return (Math.min(subjectRect.bottom, boundRect.bottom) - + Math.max(subjectRect.top, boundRect.top)) > // overlap area is greater than + ((subjectRect.bottom - subjectRect.top) / 2) // half the height +} + +export function isRectsSimilar(rect1, rect2) { + return isRectsHSimilar(rect1, rect2) && isRectsVSimilar(rect1, rect2) +} + +function isRectsHSimilar(rect1, rect2) { + return (Math.abs(rect1.left - rect2.left) <= 2) && (Math.abs(rect1.right - rect2.right) <= 3) // :( +} + +function isRectsVSimilar(rect1, rect2) { + return (Math.abs(rect1.top - rect2.top) <= 2) && (Math.abs(rect1.bottom - rect2.bottom) <= 3) // :( +} diff --git a/fullcalendar-main/tests/src/lib/global-plugins.ts b/fullcalendar-main/tests/src/lib/global-plugins.ts new file mode 100644 index 0000000..f998947 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/global-plugins.ts @@ -0,0 +1,18 @@ +import { PluginDef } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import listPlugin from '@fullcalendar/list' +import multiMonthPlugin from '@fullcalendar/multimonth' + +export const DEFAULT_PLUGINS: PluginDef[] = [ + interactionPlugin, + dayGridPlugin, + timeGridPlugin, + listPlugin, + multiMonthPlugin, +] + +pushOptions({ + plugins: DEFAULT_PLUGINS, +}) diff --git a/fullcalendar-main/tests/src/lib/global-utils.ts b/fullcalendar-main/tests/src/lib/global-utils.ts new file mode 100644 index 0000000..6214e15 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/global-utils.ts @@ -0,0 +1,320 @@ +import { Calendar, CalendarOptions } from '@fullcalendar/core' +import { createPlugin } from '@fullcalendar/core' +import { parseLocalDate, parseUtcDate } from './date-parsing.js' + +// Other Important Global Stuff +// --------------------------------------------------------------------------------------------------------------------- + +import './hacks.js' +import './simulate.js' +import './date-matchers.js' + +// Setup / Teardown +// --------------------------------------------------------------------------------------------------------------------- + +let optionsStack = null + +beforeEach(() => { + optionsStack = [] +}) + +afterEach(() => { + optionsStack = null + + if (window.currentCalendar) { + window.currentCalendar.destroy() + window.currentCalendar = null + } + + $('#calendar').remove() +}) + +// Calendar Options and Initialization +// --------------------------------------------------------------------------------------------------------------------- + +function pushOptions(options: CalendarOptions) { + beforeEach(() => { + optionsStack.push(options) + }) +} + +// called within an `it` +// needs to be called *before* initCalendar +function spyOnCalendarCallback(name, func?) { + let options = {} as any + + options[name] = func || (() => {}) + spyOn(options, name).and.callThrough() + + optionsStack.push(options) + + return options[name] +} + +function initCalendar(moreOptions?: CalendarOptions, el?) { + let $el + + if (moreOptions) { + optionsStack.push(moreOptions) + } + + if (el) { + $el = $(el) + } else { + $el = $('<div id="calendar">').appendTo('body') + } + + if (window.currentCalendar) { + window.currentCalendar.destroy() + } + + let options = getCurrentOptions() + let newCalendar = null + + options.plugins = options.plugins.concat([ + createPlugin({ + name: 'current-calendar-' + Date.now(), // ugh, might be called twice per calendar + contextInit(context) { + newCalendar = window.currentCalendar = context.calendarApi as Calendar + }, + }), + ]) + + let cool = new Calendar($el[0], options) + + if (newCalendar === window.currentCalendar) { + newCalendar.render() + } else { + newCalendar.destroy() + } + + return cool +} + +function getCurrentOptions() { + let args = [{}].concat(optionsStack) as any + return $.extend.apply($, args) // eslint-disable-line prefer-spread +} + +// Categorizing Tests +// --------------------------------------------------------------------------------------------------------------------- + +/* +describeOptions(optionName, descriptionAndValueHash, callback) +describeOptions(descriptionAndOptionsHash, callback) + */ +function describeOptions(optName, hash?, callback?) { + if ($.type(optName) === 'object') { + callback = hash + hash = optName + optName = null + } + + $.each( + hash, + (desc, val) => { + let opts + + if (optName) { + opts = {} + opts[optName] = val + } else { + opts = val + } + opts = $.extend(true, {}, opts) + + describe(desc as string, () => { + pushOptions(opts) + callback(val) + }) + }, + ) +} + +function describeValues(hash, callback) { + $.each( + hash, + /** + * @param desc {string} + */ + (desc, val) => { + describe(desc as string, () => { + callback(val) + }) + }, + ) +} + +// Timezone Tests (needed?) +// --------------------------------------------------------------------------------------------------------------------- + +const timeZoneScenarios = { + local: { + description: 'when local timezone', + value: 'local', + parseDate: parseLocalDate, + }, + UTC: { + description: 'when UTC timezone', + value: 'UTC', + parseDate: parseUtcDate, + }, +} + +function describeTimeZones(callback) { + $.each(timeZoneScenarios, (name, scenario) => { + describe(scenario.description, () => { + pushOptions({ + timeZone: name, + }) + callback(scenario) + }) + }) +} + +function describeTimeZone(name, callback) { + let scenario = timeZoneScenarios[name] + + describe(scenario.description, () => { + pushOptions({ + timeZone: name, + }) + callback(scenario) + }) +} + +// Misc +// --------------------------------------------------------------------------------------------------------------------- + +function oneCall(func) { + let called + called = false + return function () { // eslint-disable-line func-names + if (!called) { + called = true + return func.apply(this, arguments) // eslint-disable-line prefer-rest-params + } + return null + } +} + +function spyOnMethod(Class, methodName, dontCallThrough) { + let origMethod = Class.prototype.hasOwnProperty(methodName) // eslint-disable-line no-prototype-builtins + ? Class.prototype[methodName] + : null + + let spy = spyOn(Class.prototype, methodName) + + if (!dontCallThrough) { + spy = spy.and.callThrough() + } + + (spy as any).restore = () => { + if (origMethod) { + Class.prototype[methodName] = origMethod + } else { + delete Class.prototype[methodName] + } + } + + return spy +} + +// wraps an existing function in a spy, calling through to the function +function spyCall(func?) { + func = func || (() => {}) + const obj = { func } + spyOn(obj, 'func').and.callThrough() + return obj.func +} + +type spyOnCalendarCallbackType = typeof spyOnCalendarCallback +type pushOptionsType = typeof pushOptions +type initCalendarType = typeof initCalendar +type getCurrentOptionsType = typeof getCurrentOptions +type describeOptionsType = typeof describeOptions +type describeValuesType = typeof describeValues +type describeTimeZonesType = typeof describeTimeZones +type describeTimeZoneType = typeof describeTimeZone +type oneCallType = typeof oneCall +type spyOnMethodType = typeof spyOnMethod +type spyCallType = typeof spyCall + +declare global { + + let currentCalendar: Calendar + let spyOnCalendarCallback: spyOnCalendarCallbackType + let pushOptions: pushOptionsType + let initCalendar: initCalendarType + let getCurrentOptions: getCurrentOptionsType + let describeOptions: describeOptionsType + let describeValues: describeValuesType + let describeTimeZones: describeTimeZonesType + let describeTimeZone: describeTimeZoneType + let oneCall: oneCallType + let spyOnMethod: spyOnMethodType + let spyCall: spyCallType + + interface Window { // how to unify this with the above let statements? + currentCalendar: Calendar + karmaConfig: any + } + + interface Function { + calls: any // for jasmine spies + } + + interface JQueryStatic { + simulate: any + simulateMouseClick: any + simulateTouchClick: any + simulateByPoint: any + _data: any + } + + interface JQuery { + simulate: any + draggable: any + sortable: any + } + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jasmine { + interface Matchers<T> { + toEqualDate: any + toEqualLocalDate: any + toEqualNow: any + toBeBoundedBy: any + toIntersectWith: any + toBeAbove: any + toBeBelow: any + toBeRightOf: any + toBeLeftOf: any + toHaveScrollbars: any + toBeMostlyHBoundedBy: any + toBeMostlyAbove: any + toBeMostlyLeftOf: any + toBeMostlyRightOf: any + } + } + +} + +Object.assign(window, { + spyOnCalendarCallback, + pushOptions, + initCalendar, + getCurrentOptions, + describeOptions, + describeValues, + describeTimeZones, + describeTimeZone, + oneCall, + spyOnMethod, + spyCall, +}) + +pushOptions({ + timeZone: 'UTC', + eventDisplay: 'auto', +}) diff --git a/fullcalendar-main/tests/src/lib/global.css b/fullcalendar-main/tests/src/lib/global.css new file mode 100644 index 0000000..dbb888b --- /dev/null +++ b/fullcalendar-main/tests/src/lib/global.css @@ -0,0 +1,12 @@ +/* BAD: premium's css copies this css */ + +body { + margin-top: 40px; + font-size: 13px; + font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif; +} + +.fc { + max-width: 900px; + margin: 20px auto; +} diff --git a/fullcalendar-main/tests/src/lib/global.ts b/fullcalendar-main/tests/src/lib/global.ts new file mode 100644 index 0000000..7cdc67a --- /dev/null +++ b/fullcalendar-main/tests/src/lib/global.ts @@ -0,0 +1,2 @@ +import './global-utils.js' +import './global-plugins.js' diff --git a/fullcalendar-main/tests/src/lib/hacks.ts b/fullcalendar-main/tests/src/lib/hacks.ts new file mode 100644 index 0000000..c5f57c4 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/hacks.ts @@ -0,0 +1,11 @@ +import { config } from '@fullcalendar/core/internal' + +beforeEach(() => { + // On real devices, when a click-like touch interaction happens, there is a preiod of time where mouse events + // are ignores. Since ignore peroid is global, and might span across tests, disable it. + // The simulates touch events do not fire these mouse events anyway. + config.touchMouseIgnoreWait = 0 + + // increase the default timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 +}) diff --git a/fullcalendar-main/tests/src/lib/segs.ts b/fullcalendar-main/tests/src/lib/segs.ts new file mode 100644 index 0000000..11a851b --- /dev/null +++ b/fullcalendar-main/tests/src/lib/segs.ts @@ -0,0 +1,38 @@ +import { isRectsSimilar } from './geom.js' +import { getBoundingRects } from './dom-geom.js' + +export function doElsMatchSegs(els, segs, segToRectFunc) { + let elRect + let found + let i + let j + let k + let len + let len1 + let seg + let segRect + let unmatchedRects = getBoundingRects(els) + + if (unmatchedRects.length !== segs.length) { + return false + } + + for (j = 0, len = segs.length; j < len; j += 1) { + seg = segs[j] + segRect = segToRectFunc(seg) + found = false + for (i = k = 0, len1 = unmatchedRects.length; k < len1; i = (k += 1)) { + elRect = unmatchedRects[i] + if (isRectsSimilar(elRect, segRect)) { + unmatchedRects.splice(i, 1) // remove + found = true + break + } + } + if (!found) { + return false + } + } + + return !unmatchedRects.length +} diff --git a/fullcalendar-main/tests/src/lib/simulate.ts b/fullcalendar-main/tests/src/lib/simulate.ts new file mode 100644 index 0000000..ed2c8ba --- /dev/null +++ b/fullcalendar-main/tests/src/lib/simulate.ts @@ -0,0 +1,326 @@ +/* General Utils +---------------------------------------------------------------------------------------------------------------------- */ + +$.simulateByPoint = (type, options) => { + let docEl = $(document) + let point = options.point + let clientX + let clientY + let node + + if (point) { + clientX = point.left - docEl.scrollLeft() + clientY = point.top - docEl.scrollTop() + node = document.elementFromPoint(clientX, clientY) + $(node).simulate(type, options) + } +} + +/* Touch +---------------------------------------------------------------------------------------------------------------------- */ + +let origSimulateEvent = $.simulate.prototype.simulateEvent +let touchUID = Date.now() + +$.simulate.prototype.simulateEvent = function (elem, type, options) { // eslint-disable-line func-names + if (elem === window && type === 'resize') { + return this.simulateWindowResize() + } if (/^touch/.test(type)) { + return this.simulateTouchEvent(elem, type, options) + } + return origSimulateEvent.apply(this, arguments) // eslint-disable-line prefer-rest-params +} + +$.simulate.prototype.simulateWindowResize = function () { // eslint-disable-line func-names + // from https://stackoverflow.com/a/1818513/96342 + let event + + if (typeof Event !== 'undefined') { + try { + event = new Event('resize') + } catch (ex) { + // why would fail? + } + } + + if (!event) { + event = document.createEvent('UIEvents') + event.initUIEvent('resize', true, false, window, 0) + } + + this.dispatchEvent(window, 'resize', event) +} + +$.simulate.prototype.simulateTouchEvent = function (elem, type, options) { // eslint-disable-line func-names + // http://stackoverflow.com/a/29019278/96342 + let event = document.createEvent('Event') + + event.initEvent(type, true, true); // cancelable, bubbleable + (event as any).touches = [{ + target: elem, + identifier: touchUID, + pageX: options.clientX, + pageY: options.clientY, + screenX: options.clientX, + screenY: options.clientY, + clientX: options.clientX, + clientY: options.clientY, + }] + touchUID += 1 + + this.dispatchEvent(elem, type, event, options) +} + +$.simulateMouseClick = function (elem) { // eslint-disable-line func-names + let $elem = $(elem) + let clientCoords = { + clientX: $elem.offset().left + $elem.outerWidth() / 2, + clientY: $elem.offset().top + $elem.outerHeight() / 2, + } + $elem.simulate('mousemove', clientCoords) + $elem.simulate('mousedown', clientCoords) + $elem.simulate('mouseup', clientCoords) + $elem.simulate('click', clientCoords) +} + +$.simulateTouchClick = function (elem) { // eslint-disable-line func-names + let $elem = $(elem) + let clientCoords = { + clientX: $elem.offset().left + $elem.outerWidth() / 2, + clientY: $elem.offset().top + $elem.outerHeight() / 2, + } + $elem.simulate('touchstart', clientCoords) + $elem.simulate('touchend', clientCoords) + $elem.simulate('mousemove', clientCoords) + $elem.simulate('mousedown', clientCoords) + $elem.simulate('mouseup', clientCoords) + $elem.simulate('click', clientCoords) +} + +/* Drag-n-drop +---------------------------------------------------------------------------------------------------------------------- */ + +let DEBUG_DELAY = 500 +let DEBUG_MIN_DURATION = 2000 +let DEBUG_MIN_MOVES = 100 +let DRAG_DEFAULTS = { + point: null, // the start point + localPoint: { left: '50%', top: '50%' }, + end: null, // can be a point or an el + localEndPoint: { left: '50%', top: '50%' }, + dx: 0, + dy: 0, + moves: 5, + duration: 100, // ms +} + +let dragStackCnt = 0 + +$.simulate.prototype.simulateDrag = function () { // eslint-disable-line func-names + let options = $.extend({}, DRAG_DEFAULTS, this.options) + let targetNode = this.target // raw DOM node + let targetEl = $(targetNode) // jq object + let dx = options.dx + let dy = options.dy + let duration = options.duration + let moves = options.moves + let startPoint + let endEl + let endPoint + let localPoint + let offset + + // compute start point + if (options.point) { + startPoint = options.point + } else { + localPoint = normalizeElPoint(options.localPoint, targetEl) + offset = targetEl.offset() + startPoint = { + left: offset.left + localPoint.left, + top: offset.top + localPoint.top, + } + } + + // compute end point + if (options.end) { + if (isPoint(options.end)) { + endPoint = options.end + } else { // assume options.end is an element + endEl = $(options.end) + localPoint = normalizeElPoint(options.localEndPoint, endEl) + offset = endEl.offset() + endPoint = { + left: offset.left + localPoint.left, + top: offset.top + localPoint.top, + } + } + } + + if (endPoint) { + dx = endPoint.left - startPoint.left + dy = endPoint.top - startPoint.top + } + + moves = Math.max(moves, options.debug ? DEBUG_MIN_MOVES : 1) + duration = Math.max(duration, options.debug ? DEBUG_MIN_DURATION : 10) + + simulateDrag( + this, + targetNode, + startPoint, + dx, + dy, + moves, + duration, + options, + ) +} + +function simulateDrag(self, targetNode, startPoint, dx, dy, moveCnt, duration, options) { + let debug = options.debug + let isTouch = options.isTouch + let docNode = targetNode.ownerDocument + let docEl = $(docNode) + let waitTime = duration / moveCnt + let moveIndex = 0 + let clientCoords + let intervalId + let dotEl + let dragId + + if (debug) { + dotEl = $('<div>') + .css({ + position: 'absolute', + zIndex: 99999, + border: '5px solid red', + borderRadius: '5px', + margin: '-5px 0 0 -5px', + }) + .appendTo('body') + } + + function updateCoords() { + let progress = moveIndex / moveCnt + let left = startPoint.left + dx * progress + let top = startPoint.top + dy * progress + + clientCoords = { + clientX: left - docEl.scrollLeft(), + clientY: top - docEl.scrollTop(), + } + + if (debug) { + dotEl.css({ left, top }) + } + } + + function startDrag() { + updateCoords() + dragStackCnt += 1 + dragId = dragStackCnt + + // simulate a drag-start only if another drag isn't already happening + if (dragStackCnt === 1) { + self.simulateEvent( + targetNode, // can have an inner drag-start el. targetNode will still be source of emitted events + isTouch ? 'touchstart' : 'mousedown', + clientCoords, + ) + } + + let delay = options.delay || 0 + if (debug) { + delay = Math.max(delay, DEBUG_DELAY) + } + + if (delay) { + setTimeout(() => { + startMoving() + }, delay) + } else { + startMoving() + } + } + + function startMoving() { + intervalId = setInterval(tick, waitTime) + } + + function tick() { // called one interval after start + moveIndex += 1 + updateCoords() // update clientCoords before mousemove + + if (isTouch) { + // touchmove happens on the originating element + self.simulateEvent(targetNode, 'touchmove', clientCoords) + } else { + self.simulateEvent(docNode, 'mousemove', clientCoords) + } + + if (moveIndex >= moveCnt) { + stopMoving() + } + } + + function stopMoving() { + clearInterval(intervalId) + if (debug) { + setTimeout(() => { + dotEl.remove() // do this before calling stopDrag/callback. don't want dot picked up by elementFromPoint + stopDrag() + }, DEBUG_DELAY) + } else { + stopDrag() + } + } + + function stopDrag() { // progress at 1, coords already up to date at this point + (options.onBeforeRelease || (() => {}))() + + // only simulate a drop if the current drag is still the active one. + // otherwise, this means another drag has begun via onBeforeRelease. + if (dragId === dragStackCnt) { + if ( + $.contains(docNode, targetNode) || + isTouch // touch will always first touchend on original node, even if removed from DOM + // https://stackoverflow.com/a/45760014 + ) { + self.simulateEvent(targetNode, isTouch ? 'touchend' : 'mouseup', clientCoords) + self.simulateEvent(targetNode, 'click', clientCoords) + } else { + self.simulateEvent(docNode, isTouch ? 'touchend' : 'mouseup', clientCoords) + } + } + + dragStackCnt -= 1 + + let callback: (() => void) = options.onRelease || options.callback || (() => {}) + + // we wait because the there might be a FullCalendar drag interaction that finishes asynchronously + // after the mouseend/touchend happens, and it's really convenient if our callback fires after that. + setTimeout(callback, 0) + } + + startDrag() +} + +function normalizeElPoint(point, el) { + let left = point.left + let top = point.top + + if (/%$/.test(left)) { + left = (parseInt(left, 10) / 100) * el.outerWidth() + } + if (/%$/.test(top)) { + top = (parseInt(top, 10) / 100) * el.outerHeight() + } + + return { left, top } +} + +function isPoint(input) { + return typeof input === 'object' && // `in` operator only works on objects + 'left' in input && 'top' in input +} diff --git a/fullcalendar-main/tests/src/lib/string.ts b/fullcalendar-main/tests/src/lib/string.ts new file mode 100644 index 0000000..c92eb0e --- /dev/null +++ b/fullcalendar-main/tests/src/lib/string.ts @@ -0,0 +1,3 @@ +export function removeLtrCharCodes(s) { + return s.replace(/\u200e/g, '') +} diff --git a/fullcalendar-main/tests/src/lib/timeZoneImpl.ts b/fullcalendar-main/tests/src/lib/timeZoneImpl.ts new file mode 100644 index 0000000..67d0626 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/timeZoneImpl.ts @@ -0,0 +1,27 @@ +import dayGridPlugin from '@fullcalendar/daygrid' + +export function testTimeZoneImpl(timeZoneImplPlugin) { + describe('named tz implementation', () => { + pushOptions({ + plugins: [timeZoneImplPlugin, dayGridPlugin], + }) + + it('computes correct offset for named timezone for View dates', () => { + initCalendar({ + initialView: 'dayGridMonth', + now: '2018-09-01', + timeZone: 'Europe/Moscow', + events: [ + { start: '2018-09-05' }, + ], + }) + + let view = currentCalendar.view + expect(view.currentStart).toEqualDate('2018-09-01T00:00:00+03:00') + + // interprets the ambug iso date string correctly + let event = currentCalendar.getEvents()[0] + expect(event.start).toEqualDate('2018-09-05T00:00:00+03:00') + }) + }) +} diff --git a/fullcalendar-main/tests/src/lib/vdom-misc.ts b/fullcalendar-main/tests/src/lib/vdom-misc.ts new file mode 100644 index 0000000..e270bdb --- /dev/null +++ b/fullcalendar-main/tests/src/lib/vdom-misc.ts @@ -0,0 +1,25 @@ +import { render, createElement, flushSync } from '@fullcalendar/core/preact' +import { ListenerCounter } from './ListenerCounter.js' + +let standardElListenerCount + +export function prepareStandardListeners() { + if (standardElListenerCount === undefined) { + standardElListenerCount = _prepareStandardListeners() + } + return standardElListenerCount +} + +export function _prepareStandardListeners() { + let el = document.createElement('div') + document.body.appendChild(el) + + const elListenerCounter = new ListenerCounter(el) + elListenerCounter.startWatching() + + flushSync(() => { + render(createElement('div', {}), el) + }) + + return elListenerCounter.stopWatching() +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/CalendarWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/CalendarWrapper.ts new file mode 100644 index 0000000..1dc8256 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/CalendarWrapper.ts @@ -0,0 +1,102 @@ +import { Calendar } from '@fullcalendar/core' +import { findElements } from '@fullcalendar/core/internal' +import { ToolbarWrapper } from './ToolbarWrapper.js' + +export class CalendarWrapper { + static EVENT_CLASSNAME = 'fc-event' // TODO: put this everywhere? + static EVENT_IS_START_CLASSNAME = 'fc-event-start' + static EVENT_IS_END_CLASSNAME = 'fc-event-end' + static EVENT_TIME_CLASSNAME = 'fc-event-time' + static EVENT_TITLE_CLASSNAME = 'fc-event-title' + static EVENT_RESIZER_CLASSNAME = 'fc-event-resizer' + static EVENT_START_RESIZER_CLASSNAME = 'fc-event-resizer-start' + static EVENT_END_RESIZER_CLASSNAME = 'fc-event-resizer-end' + static BG_EVENT_CLASSNAME = 'fc-bg-event' + static DAY_PAST_CLASSNAME = 'fc-day-past' + static DAY_FUTURE_CLASSNAME = 'fc-day-future' + static SLOT_PAST_CLASSNAME = 'fc-slot-past' + static SLOT_FUTURE_CLASSNAME = 'fc-slot-future' + static TODAY_CLASSNAME = 'fc-day-today' + static SLOT_TODAY_CLASSNAME = 'fc-slot-today' + static DOW_CLASSNAMES = ['fc-day-sun', 'fc-day-mon', 'fc-day-tue', 'fc-day-wed', 'fc-day-thu', 'fc-day-fri', 'fc-day-sat'] + static DOW_SLOT_CLASSNAMES = ['fc-slot-sun', 'fc-slot-mon', 'fc-slot-tue', 'fc-slot-wed', 'fc-slot-thu', 'fc-slot-fri', 'fc-slot-sat'] + static LTR_CLASSNAME = 'fc-direction-ltr' + static RTL_CLASSNAME = 'fc-direction-rtl' + static BOOTSTRAP_CLASSNAME = 'fc-theme-bootstrap' + static UNTHEMED_CLASSNAME = 'fc-theme-standard' + static ROOT_CLASSNAME = 'fc' + + constructor(private calendar: Calendar) { + } + + // TODO: distinguish between header/footerToolbar + get toolbar() { + let toolbarEl = this.calendar.el.querySelector('.fc-toolbar') as HTMLElement + return toolbarEl ? new ToolbarWrapper(toolbarEl) : null + } + + get footerToolbar() { + let toolbarEl = this.calendar.el.querySelector('.fc-footer-toolbar') as HTMLElement + return toolbarEl ? new ToolbarWrapper(toolbarEl) : null + } + + getViewContainerEl() { + return this.calendar.el.querySelector('.fc-view-harness') as HTMLElement + } + + getViewEl() { + return this.calendar.el.querySelector('.fc-view') as HTMLElement + } + + getViewName() { + return this.getViewEl().getAttribute('class').match(/fc-(\w+)-view/)[1] + } + + // DISCOURAGE use of the following... + + getNonBusinessDayEls() { + return findElements(this.calendar.el, '.fc-non-business') + } + + getEventEls() { // FG only + return findElements(this.calendar.el, '.fc-event:not(.fc-bg-event)') + } + + getFirstEventEl() { + return this.calendar.el.querySelector('.fc-event:not(.fc-bg-event)') as HTMLElement + } + + getTodayEls() { + return findElements(this.calendar.el, '.fc-day-today') + } + + getEventElInfo(eventEl: HTMLElement) { + return { + isStart: eventEl.classList.contains(CalendarWrapper.EVENT_IS_START_CLASSNAME), + isEnd: eventEl.classList.contains(CalendarWrapper.EVENT_IS_END_CLASSNAME), + timeText: $(eventEl).find('.' + CalendarWrapper.EVENT_TIME_CLASSNAME).text() || '', + titleEl: eventEl.querySelector('.' + CalendarWrapper.EVENT_TITLE_CLASSNAME), + resizerEl: eventEl.querySelector('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME), + } + } + + getBgEventEls() { + return findElements(this.calendar.el, '.' + CalendarWrapper.BG_EVENT_CLASSNAME) + } + + getFirstDateEl() { + return this.calendar.el.querySelector('.fc [data-date]') + } + + getDateCellEl(dateStr: string) { + return this.calendar.el.querySelector('td.fc-day[data-date="' + dateStr + '"]') + } + + getLicenseMessage() { + return $('.fc-license-message', this.calendar.el).text() + } + + isAllowingDragging() { + return !$('body').hasClass('fc-not-allowed') + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/DayGridViewWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/DayGridViewWrapper.ts new file mode 100644 index 0000000..0c4a3d5 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/DayGridViewWrapper.ts @@ -0,0 +1,23 @@ +import { Calendar } from '@fullcalendar/core' +import { ViewWrapper } from './ViewWrapper.js' +import { DayGridWrapper } from './DayGridWrapper.js' +import { DayHeaderWrapper } from './DayHeaderWrapper.js' + +export class DayGridViewWrapper extends ViewWrapper { + constructor(calendar: Calendar) { + super(calendar, 'fc-daygrid') + } + + get header() { + let headerEl = this.el.querySelector('.fc-col-header') as HTMLElement + return headerEl ? new DayHeaderWrapper(headerEl) : null + } + + get dayGrid() { + return new DayGridWrapper(this.el.querySelector('.fc-daygrid-body')) + } + + getScrollerEl() { + return this.el.querySelector('.fc-daygrid-body').parentElement // TODO: use closest + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/DayGridWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/DayGridWrapper.ts new file mode 100644 index 0000000..aef06ab --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/DayGridWrapper.ts @@ -0,0 +1,295 @@ +import { findElements } from '@fullcalendar/core/internal' +import { formatIsoDay } from '../datelib-utils.js' +import { getRectCenter, intersectRects, addPoints, subtractPoints } from '../geom.js' +import { CalendarWrapper } from './CalendarWrapper.js' + +export class DayGridWrapper { + static EVENT_IS_START_CLASSNAME = 'fc-event-start' + static EVENT_IS_END_CLASSNAME = 'fc-event-end' + + constructor(private el: HTMLElement) { + } + + getRootTableEl() { + return $(this.el).find('> table')[0] as HTMLElement + } + + getAllDayEls() { + return findElements(this.el, '.fc-day[data-date]') + } + + getMirrorEls() { + return findElements(this.el, '.fc-event.fc-event-mirror') + } + + getDayEl(date) { + if (typeof date === 'string') { + date = new Date(date) + } + return this.el.querySelector('.fc-day[data-date="' + formatIsoDay(date) + '"]') + } + + getDayEls(date) { // TODO: return single el??? accept 'tues' + if (typeof date === 'number') { + return findElements(this.el, `.fc-day.${CalendarWrapper.DOW_CLASSNAMES[date]}`) + } + if (typeof date === 'string') { + date = new Date(date) + } + return findElements(this.el, '.fc-day[data-date="' + formatIsoDay(date) + '"]') + } + + getDayNumberText(date) { + return $(this.getDayEl(date).querySelector('.fc-daygrid-day-top')).text() + } + + getDayElsInRow(row) { + return findElements(this.getRowEl(row), '.fc-day') + } + + // TODO: discourage use + getNonBusinessDayEls() { + return findElements(this.el, '.fc-non-business') + } + + // example: gets all the Mondays in the first row of days + // TODO: discourage use + getDowEls(dayAbbrev) { + return findElements(this.el, `tr:first-child > td.fc-day-${dayAbbrev}`) + } + + getMonthStartEls() { + return findElements(this.el, '.fc-daygrid-month-start') + } + + getDisabledDayEls() { + return findElements(this.el, '.fc-day-disabled') + } + + getMoreEl() { + return this.el.querySelector('.fc-daygrid-more-link') + } + + getMoreEls() { + return findElements(this.el, '.fc-daygrid-more-link') + } + + getWeekNavLinkEls() { + return findElements(this.el, '.fc-daygrid-week-number[data-navlink]') + } + + getWeekNumberEls() { + return findElements(this.el, '.fc-daygrid-week-number') + } + + getWeekNumberEl(rowIndex) { + return this.getRowEl(rowIndex).querySelector('.fc-daygrid-week-number') + } + + getWeekNumberText(rowIndex) { + return $(this.getWeekNumberEl(rowIndex)).text() + } + + getNavLinkEl(date) { + return this.getDayEl(date).querySelector('.fc-daygrid-day-number[data-navlink]') + } + + clickNavLink(date) { + $.simulateMouseClick(this.getNavLinkEl(date)) + } + + openMorePopover(index?) { + if (index == null) { + $(this.getMoreEl()).simulate('click') + } else { + $(this.el.querySelectorAll('.fc-daygrid-more-link')[index]).simulate('click') + } + } + + getMorePopoverEl() { + let viewWrapperEl = this.el.closest('.fc-view-harness') + return viewWrapperEl.querySelector('.fc-more-popover') as HTMLElement + } + + getMorePopoverHeaderEl() { + return this.getMorePopoverEl().querySelector('.fc-popover-header') as HTMLElement + } + + getMorePopoverEventEls() { + return findElements(this.getMorePopoverEl(), '.fc-event') + } + + getMorePopoverEventCnt() { // fg + return this.getMorePopoverEventEls().length + } + + getMorePopoverEventTitles() { + return this.getMorePopoverEventEls().map((el) => $(el.querySelector('.fc-event-title')).text()) + } + + getMorePopoverBgEventCnt() { + return this.getMorePopoverEl().querySelectorAll('.fc-bg-event').length + } + + closeMorePopover() { + $(this.getMorePopoverEl().querySelector('.fc-popover-close')).simulate('click') + } + + getMorePopoverTitle() { + return $(this.getMorePopoverEl().querySelector('.fc-popover-title')).text() + } + + getRowEl(i) { + return this.el.querySelector(`tr:nth-child(${i + 1})`) as HTMLElement // nth-child is 1-indexed! + } + + getRowEls() { + return findElements(this.el, 'tr') + } + + getBgEventEls(row?) { + let parentEl = row == null ? this.el : this.getRowEl(row) + return findElements(parentEl, '.fc-bg-event') + } + + getEventEls() { // FG events + return findElements(this.el, '.fc-daygrid-event') + } + + isEventListItem(el: HTMLElement) { + return el.classList.contains('fc-daygrid-dot-event') + } + + getFirstEventEl() { + return this.el.querySelector('.fc-daygrid-event') as HTMLElement + } + + getHighlightEls() { // FG events + return findElements(this.el, '.fc-highlight') + } + + static getEventElInfo(eventEl) { + return { + title: $(eventEl).find('.fc-event-title').text(), + timeText: $(eventEl).find('.fc-event-time').text(), + } + } + + clickDate(date) { + $.simulateMouseClick(this.getDayEl(date)) + } + + selectDates(start, inclusiveEnd) { + return new Promise<void>((resolve) => { + $(this.getDayEls(start)).simulate('drag', { + point: getRectCenter(this.getDayEl(start).getBoundingClientRect()), + end: getRectCenter(this.getDayEl(inclusiveEnd).getBoundingClientRect()), + onRelease: () => resolve(), + }) + }) + } + + selectDatesTouch(start, inclusiveEnd, debug = false) { + return new Promise<void>((resolve) => { + let startEl = this.getDayEl(start) + + setTimeout(() => { // wait for calendar to accept touch :( + // QUESTION: why do we not need to do press-down first? + $(startEl).simulate('drag', { + debug, + isTouch: true, + end: getRectCenter(this.getDayEl(inclusiveEnd).getBoundingClientRect()), + onRelease: () => resolve(), + }) + }, 0) + }) + } + + dragEventToDate(eventEl: HTMLElement, startDate, endDate, isTouch?, onBeforeRelease?) { + return new Promise<void>((resolve) => { + if (!startDate) { + let rect1 = this.getDayEl(endDate).getBoundingClientRect() + let point1 = getRectCenter(rect1) + + $(eventEl).simulate('drag', { + isTouch: isTouch || false, + delay: isTouch ? 200 : 0, // bad to hardcode ms + end: point1, + onBeforeRelease, + onRelease: () => resolve(), + }) + } else { + let rect0 = this.getDayEl(startDate).getBoundingClientRect() + let rect1 = this.getDayEl(endDate).getBoundingClientRect() + + let eventRect = eventEl.getBoundingClientRect() + let point0 = getRectCenter(intersectRects(eventRect, rect0)) + let point1 = getRectCenter(rect1) + + $(eventEl).simulate('drag', { + isTouch: isTouch || false, + delay: isTouch ? 200 : 0, // bad to hardcode ms + point: point0, + end: point1, + onBeforeRelease, + onRelease: () => resolve(), + }) + } + }) + } + + resizeEvent(eventEl: HTMLElement, origEndDate, newEndDate, fromStart?, onBeforeRelease?) { + return new Promise<void>((resolve) => { + let rect0 = this.getDayEl(origEndDate).getBoundingClientRect() + let rect1 = this.getDayEl(newEndDate).getBoundingClientRect() + + let resizerEl = $(eventEl).find( + '.' + (fromStart ? CalendarWrapper.EVENT_START_RESIZER_CLASSNAME : CalendarWrapper.EVENT_END_RESIZER_CLASSNAME), + ).css('display', 'block')[0] // usually only displays on hover. force display + + let resizerRect = resizerEl.getBoundingClientRect() + let resizerCenter = getRectCenter(resizerRect) + + let vector = subtractPoints(resizerCenter, rect0) + let endPoint = addPoints(rect1, vector) + + $(resizerEl).simulate('drag', { + point: resizerCenter, + end: endPoint, + onBeforeRelease, + onRelease: () => resolve(), + }) + }) + } + + resizeEventTouch(eventEl: HTMLElement, origEndDate, newEndDate, fromStart?) { + return new Promise<void>((resolve) => { + let rect0 = this.getDayEl(origEndDate).getBoundingClientRect() + let rect1 = this.getDayEl(newEndDate).getBoundingClientRect() + + setTimeout(() => { // wait for calendar to accept touch :( + $(eventEl).simulate('drag', { + isTouch: true, + delay: 200, + onRelease: () => { + let resizerEl = eventEl.querySelector( + '.' + (fromStart ? CalendarWrapper.EVENT_START_RESIZER_CLASSNAME : CalendarWrapper.EVENT_END_RESIZER_CLASSNAME), + ) + let resizerRect = resizerEl.getBoundingClientRect() + let resizerCenter = getRectCenter(resizerRect) + + let vector = subtractPoints(resizerCenter, rect0) + let endPoint = addPoints(rect1, vector) + + $(resizerEl).simulate('drag', { + isTouch: true, + point: resizerCenter, + end: endPoint, + onRelease: () => resolve(), + }) + }, + }) + }, 0) + }) + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/DayHeaderWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/DayHeaderWrapper.ts new file mode 100644 index 0000000..a18faf6 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/DayHeaderWrapper.ts @@ -0,0 +1,54 @@ +import { findElements } from '@fullcalendar/core/internal' +import { parseIsoAsUtc, formatIsoDay } from '../datelib-utils.js' +import { parseUtcDate } from '../date-parsing.js' +import { CalendarWrapper } from './CalendarWrapper.js' + +export class DayHeaderWrapper { + constructor(public el: HTMLElement) { + } + + getDates() { + return this.getCellEls().map((cellEl) => parseIsoAsUtc(cellEl.getAttribute('data-date'))) + } + + getCellEls() { + return findElements(this.el, '.fc-col-header-cell') + } + + getCellEl(dateOrDow) { + if (typeof dateOrDow === 'number') { + return this.el.querySelector(`.fc-col-header-cell.${CalendarWrapper.DOW_CLASSNAMES[dateOrDow]}`) + } + if (typeof dateOrDow === 'string') { + dateOrDow = parseUtcDate(dateOrDow) + } + return this.el.querySelector(`.fc-col-header-cell[data-date="${formatIsoDay(dateOrDow)}"]`) + } + + getCellText(dateOrDow) { + return $(this.getCellEl(dateOrDow)).text() + } + + getCellInfo() { // all + return this.getCellEls().map((cellEl) => ({ + text: $(cellEl).text(), + date: parseIsoAsUtc(cellEl.getAttribute('data-date')), + isToday: cellEl.classList.contains('fc-day-today'), + })) + } + + getNavLinkEls() { + return findElements(this.el, '.fc-col-header-cell[data-date] a[data-navlink]') + } + + getNavLinkEl(dayDate) { + if (typeof dayDate === 'string') { + dayDate = new Date(dayDate) + } + return this.el.querySelector('.fc-col-header-cell[data-date="' + formatIsoDay(dayDate) + '"] a') + } + + clickNavLink(date) { + $.simulateMouseClick(this.getNavLinkEl(date)) + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/ListViewWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/ListViewWrapper.ts new file mode 100644 index 0000000..a758bd8 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/ListViewWrapper.ts @@ -0,0 +1,57 @@ +import { Calendar } from '@fullcalendar/core' +import { findElements } from '@fullcalendar/core/internal' +import { ViewWrapper } from './ViewWrapper.js' +import { formatIsoDay } from '../datelib-utils.js' + +export class ListViewWrapper extends ViewWrapper { + static EVENT_DOT_CLASSNAME = 'fc-list-event-dot' + + constructor(calendar: Calendar) { + super(calendar, 'fc-list') + } + + getEventEls() { + return findElements(this.el, '.fc-list-event') + } + + getEventInfo() { + return this.getEventEls().map((eventEl) => ({ + title: $(eventEl).find('.fc-list-event-title').text(), + timeText: $(eventEl).find('.fc-list-event-time').text(), + })) + } + + getDayInfo() { + return this.getHeadingEls().map((el) => { + let $el = $(el) + return { + mainText: $el.find('.fc-list-day-text').text() || '', + altText: $el.find('.fc-list-day-side-text').text() || '', + date: new Date(el.getAttribute('data-date')), + } + }) + } + + getHeadingEls() { + return findElements(this.el, '.fc-list-day') + } + + getScrollerEl() { + return this.el.querySelector('.fc-scroller') + } + + hasEmptyMessage() { + return Boolean(this.el.querySelector('.fc-list-empty')) + } + + getNavLinkEl(dayDate) { + if (typeof dayDate === 'string') { + dayDate = new Date(dayDate) + } + return this.el.querySelector('.fc-list-day[data-date="' + formatIsoDay(dayDate) + '"] a.fc-list-day-text') + } + + clickNavLink(dayDate) { + $.simulateMouseClick(this.getNavLinkEl(dayDate)) + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/MultiMonthViewWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/MultiMonthViewWrapper.ts new file mode 100644 index 0000000..821dcbc --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/MultiMonthViewWrapper.ts @@ -0,0 +1,33 @@ +import { Calendar } from '@fullcalendar/core' +import { findElements } from '@fullcalendar/core/internal' +import { ViewWrapper } from './ViewWrapper.js' +import { DayGridWrapper } from './DayGridWrapper.js' + +export class MultiMonthViewWrapper extends ViewWrapper { + constructor(calendar: Calendar) { + super(calendar, 'fc-multimonth') + } + + getMonths() { + const monthEls = findElements(this.el, '.fc-multimonth-month') + + return monthEls.map((monthEl) => ({ + el: monthEl, + title: (monthEl.querySelector('.fc-multimonth-title') as HTMLElement).innerText, + columnCnt: monthEl.querySelectorAll('th').length, + })) + } + + getDayGrid(i) { + const dayGridEls = findElements(this.el, '.fc-multimonth-daygrid') + return new DayGridWrapper(dayGridEls[i]) + } + + getEventEls() { // FG events + return findElements(this.el, '.fc-daygrid-event') + } + + getScrollerEl() { + return this.el // the view itself + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/TimeGridViewWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/TimeGridViewWrapper.ts new file mode 100644 index 0000000..6f83969 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/TimeGridViewWrapper.ts @@ -0,0 +1,48 @@ +import { ViewWrapper } from './ViewWrapper.js' +import { TimeGridWrapper } from './TimeGridWrapper.js' +import { DayGridWrapper } from './DayGridWrapper.js' +import { DayHeaderWrapper } from './DayHeaderWrapper.js' + +export class TimeGridViewWrapper extends ViewWrapper { + constructor(calendar) { + super(calendar, 'fc-timegrid') + } + + get header() { + let headerEl = this.el.querySelector('.fc-col-header') as HTMLElement + return headerEl ? new DayHeaderWrapper(headerEl) : null + } + + get timeGrid() { + return new TimeGridWrapper(this.el.querySelector('.fc-timegrid-body')) + } + + get dayGrid() { // the all-day area + let dayGridEl = this.el.querySelector('.fc-daygrid-body') as HTMLElement + return dayGridEl ? new DayGridWrapper(dayGridEl) : null + } + + getScrollerEl() { + return this.el.querySelector('.fc-timegrid-body').parentElement // TODO: use closest + } + + getHeaderAxisEl() { + return this.el.querySelector('.fc-col-header .fc-timegrid-axis') + } + + getHeaderWeekNumberLink() { + return this.getHeaderAxisEl().querySelector('a') + } + + getHeaderWeekText() { // the title + return $(this.getHeaderWeekNumberLink()).text() + } + + getAllDayAxisEl() { + return this.el.querySelector('.fc-daygrid-body .fc-timegrid-axis') + } + + getAllDayAxisElText() { + return $(this.getAllDayAxisEl()).text() + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/TimeGridWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/TimeGridWrapper.ts new file mode 100644 index 0000000..28b3011 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/TimeGridWrapper.ts @@ -0,0 +1,627 @@ +import { findElements, startOfDay, createDuration, parseMarker, addDays, addMs, getRectCenter, asRoughMs } from '@fullcalendar/core/internal' +import { formatIsoDay, formatIsoTime, ensureDate } from '../datelib-utils.js' +import { parseUtcDate } from '../date-parsing.js' +import { getBoundingRect } from '../dom-geom.js' +import { addPoints } from '../geom.js' +import { CalendarWrapper } from './CalendarWrapper.js' + +export class TimeGridWrapper { + constructor(public el: HTMLElement) { + } + + getAllDayEls() { + return findElements(this.el, '.fc-day[data-date]') + } + + getMirrorEls() { + return findElements(this.el, '.fc-event.fc-event-mirror') + } + + getDayEls(date) { // TODO: rename. make singular name + date = ensureDate(date) + return findElements(this.el, '.fc-day[data-date="' + formatIsoDay(date) + '"]') + } + + getSlotEls() { + return findElements(this.el, '.fc-timegrid-slot-label[data-time]') + } + + getAxisTexts() { + return this.getSlotAxisEls().map((el) => $(el).text()) + } + + getSlotAxisEls() { // TODO: rename to label + return findElements(this.el, '.fc-timegrid-slot-label[data-time]') + } + + getSlotLaneEls() { + return findElements(this.el, '.fc-timegrid-slot-lane[data-time]') + } + + getSlotElByIndex(index) { // TODO: rename "slat" + return $(`.fc-timegrid-slots tr:eq(${index})`, this.el).get() + } + + getMainSlotTable() { + return $('.fc-timegrid-slots > table')[0] + } + + getSeparateSlotAxisTable() { + return $('.fc-timegrid-axis-chunk > table')[0] + } + + getSlotElByTime(timeMs) { + let date = parseUtcDate('2016-01-01') + date = new Date(date.valueOf() + timeMs) + + if (date.getUTCDate() === 1) { // ensure no time overflow/underflow + return this.el.querySelector('.fc-timegrid-slot-label[data-time="' + formatIsoTime(date) + '"]') + } + + return null + } + + getNonBusinessDayEls() { + return findElements(this.el, '.fc-non-business') + } + + getColEl(col) { + return this.el.querySelectorAll('.fc-timegrid-col:not(.fc-timegrid-axis)')[col] as HTMLElement + } + + queryBgEventsInCol(col) { + return findElements(this.getColEl(col), '.fc-bg-event') + } + + queryNonBusinessSegsInCol(col) { + return findElements(this.getColEl(col), '.fc-non-business') + } + + getHighlightEls() { // FG events + return findElements(this.el, '.fc-highlight') + } + + // TODO: discourage use + getDowEls(dayAbbrev) { + return findElements(this.el, `.fc-day-${dayAbbrev}`) + } + + // for https://github.com/fullcalendar/fullcalendar-scheduler/issues/363 + isStructureValid() { + return Boolean(this.el.querySelector('.fc-timegrid-slots')) + } + + getMoreEls() { + return findElements(this.el, '.fc-timegrid-more-link') + } + + openMorePopover(index?) { + $(this.getMoreEls()[index || 0]).simulate('click') + } + + getMorePopoverEl() { + let viewWrapperEl = this.el.closest('.fc-view-harness') + return viewWrapperEl.querySelector('.fc-more-popover') as HTMLElement + } + + getMorePopoverEventEls() { + return findElements(this.getMorePopoverEl(), '.fc-event') + } + + hasNowIndicator() { + let hasArrow = Boolean(this.getNowIndicatorArrowEl()) + let hasLine = Boolean(this.getNowIndicatorLineEl()) + + if (hasArrow !== hasLine) { + throw new Error('Inconsistent now-indicator rendering state') + } else { + return hasArrow + } + } + + getNowIndicatorArrowEl() { + return this.el.querySelector('.fc-timegrid-now-indicator-arrow') + } + + getNowIndicatorLineEl() { + return this.el.querySelector('.fc-timegrid-now-indicator-line') + } + + getTimeAxisInfo() { + return $('.fc-timegrid-slot-label[data-time]', this.el).map((i, td) => ({ + text: $(td).text(), + isMajor: !$(td).hasClass('fc-timegrid-slot-minor'), + })).get() + } + + getLastMajorAxisInfo() { + let cells = this.getTimeAxisInfo() + + for (let i = cells.length - 1; i >= 0; i -= 1) { + if (cells[i].isMajor) { + return cells[i] + } + } + + return null + } + + dragEventToDate(eventEl: HTMLElement, dropDate, onBeforeRelease?) { + return new Promise<void>((resolve) => { + $(eventEl).simulate('drag', { + localPoint: { left: '50%', top: 5 }, // ahhh 5. overcome divider sometimes + end: this.getPoint(dropDate), + onBeforeRelease, + onRelease: () => resolve(), + }) + }) + } + + resizeEvent(eventEl: HTMLElement, origEndDate, newEndDate, onBeforeRelease?) { + return new Promise<void>((resolve) => { + let resizerEl = $(eventEl).find('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME) + .css('display', 'block')[0] // usually only displays on hover. force display + + let resizerPoint = getRectCenter(resizerEl.getBoundingClientRect()) + let origPoint = this.getPoint(origEndDate) + let yCorrect = resizerPoint.top - origPoint.top + let destPoint = this.getPoint(newEndDate) + destPoint = addPoints(destPoint, { left: 0, top: yCorrect }) + + $(resizerEl).simulate('drag', { + end: destPoint, + onBeforeRelease, + onRelease: () => resolve(), + }) + }) + } + + resizeEventTouch(eventEl: HTMLElement, origEndDate, newEndDate) { + return new Promise<void>((resolve) => { + setTimeout(() => { // wait for calendar to accept touch :( + $(eventEl).simulate('drag', { + isTouch: true, + localPoint: { left: '50%', top: '90%' }, + delay: 200, + onRelease: () => { + let resizerEl = eventEl.querySelector('.' + CalendarWrapper.EVENT_RESIZER_CLASSNAME) + let resizerPoint = getRectCenter(resizerEl.getBoundingClientRect()) + let origPoint = this.getPoint(origEndDate) + let yCorrect = resizerPoint.top - origPoint.top + let destPoint = this.getPoint(newEndDate) + destPoint = addPoints(destPoint, { left: 0, top: yCorrect }) + + $(resizerEl).simulate('drag', { + isTouch: true, + end: destPoint, + onRelease: () => resolve(), + }) + }, + }) + }, 0) + }) + } + + selectDates(start, end) { + let startPoint = this.getPoint(start) + let endPoint = this.getPoint(end, true) + + startPoint.top += 2 + endPoint.top -= 2 + + return new Promise<void>((resolve) => { + $(this.getDayEls(start)).simulate('drag', { + point: startPoint, + end: endPoint, + onRelease: () => resolve(), + }) + }) + } + + selectDatesTouch(start, end, debug = false) { + let dayEls = this.getDayEls(start) + let startPoint = this.getPoint(start) + let endPoint = this.getPoint(end, true) + + startPoint.top += 2 + endPoint.top -= 2 + + return new Promise<void>((resolve) => { + setTimeout(() => { // wait for calendar to accept touch :( + // QUESTION: why do we not need to do press-down first? + $(dayEls).simulate('drag', { + debug, + isTouch: true, + point: startPoint, + end: endPoint, + onRelease: () => resolve(), + }) + }, 0) + }) + } + + clickDate(date) { + return new Promise<void>((resolve) => { + $(this.getDayEls(date)).simulate('drag', { + point: this.getPoint(date), + onRelease: () => resolve(), + }) + }) + } + + getRect(start, end) { + let obj + if (typeof start === 'object') { + obj = start + start = obj.start + end = obj.end + } + + start = ensureDate(start) + end = ensureDate(end) + + let startDay = startOfDay(start) + let endDay = startOfDay(end) + let startTimeMs = start.valueOf() - startDay.valueOf() + let endTimeMs = end.valueOf() - endDay.valueOf() + + if (startDay.valueOf() === endDay.valueOf()) { + endTimeMs = end.valueOf() - endDay.valueOf() + } else if (end < start) { + endTimeMs = startTimeMs + } else { + endTimeMs = 1000 * 60 * 60 * 24 // whole day + } + + let dayEls = this.getDayEls(start) + let dayRect = getBoundingRect(dayEls) + return { + left: dayRect.left, + right: dayRect.right, + top: this.getTimeTop(startTimeMs), + bottom: this.getTimeTop(endTimeMs), + } + } + + getPoint(date, isEnd?) { // gives offset to window topleft, like getBoundingClientRect + date = ensureDate(date) + + let day = startOfDay(date) + let timeMs = date.valueOf() - day.valueOf() + + if (isEnd && !timeMs) { + day = addDays(day, -1) + timeMs = date.valueOf() - day.valueOf() + } + + let top = this.getTimeTop(timeMs) + let dayEls = this.getDayEls(day) + let dayRect + + expect(dayEls.length).toBe(1) + dayRect = getBoundingRect(dayEls[0]) + + return { + left: (dayRect.left + dayRect.right) / 2, + top, + } + } + + getLine(date) { + date = ensureDate(date) + + let day = startOfDay(date) + let timeMs = date.valueOf() - day.valueOf() + let top = this.getTimeTop(timeMs) + let dayEls = this.getDayEls(date) + let dayRect + + expect(dayEls.length).toBe(1) + dayRect = getBoundingRect(dayEls[0]) + + return { + left: dayRect.left, + right: dayRect.right, + top, + bottom: top, + } + } + + getTimeTop(targetTimeMs) { + if (typeof targetTimeMs !== 'number') { + targetTimeMs = asRoughMs(createDuration(targetTimeMs)) + } + + const topBorderWidth = 1 // TODO: kill + + let singleSlotEl = this.getSlotElByTime(targetTimeMs) + if (singleSlotEl) { // exact slot match + return $(singleSlotEl).offset().top + topBorderWidth + } + + let $slotEl // used within loop, but we access last val + let slotEls = this.getSlotEls() // all slots + let slotTimeMs = null + let prevSlotTimeMs = null + + for (let i = 0; i < slotEls.length; i += 1) { // traverse earlier to later + let slotEl = slotEls[i] + $slotEl = $(slotEl) + + prevSlotTimeMs = slotTimeMs + slotTimeMs = createDuration(slotEl.getAttribute('data-time')).milliseconds + + // is target time between start of previous slot but before this one? + if (targetTimeMs < slotTimeMs) { + // before first slot + if (!prevSlotTimeMs) { + return $slotEl.offset().top + topBorderWidth + } + + let $prevSlotEl = $(slotEls[i - 1]) + return $prevSlotEl.offset().top + // previous slot top + topBorderWidth + + ($prevSlotEl.outerHeight() * + ((targetTimeMs - prevSlotTimeMs) / (slotTimeMs - prevSlotTimeMs))) + } + } + + // target time must be after the start time of the last slot. + // `slotTimeMs` is set to the start time of the last slot. + + // guess the duration of the last slot, based on previous duration + const slotMsDuration = slotTimeMs - prevSlotTimeMs + + return $slotEl.offset().top + // last slot's top + topBorderWidth + + ($slotEl.outerHeight() * + Math.min(1, (targetTimeMs - slotTimeMs) / slotMsDuration)) // don't go past end of last slot + } + + computeSpanRects(start, end) { + start = ensureDate(start) + end = ensureDate(end) + + let dayStructs = this.computeDayInfo() + let slotStructs = this.computeSlotInfo() + let dayI + let dayStruct + let slotI + let slotStruct + let slotDayStart + let slotStart + let slotEnd + let coverage + let startTop = null + let endTop = null + let rects = [] + + for (dayI = 0; dayI < dayStructs.length; dayI += 1) { + dayStruct = dayStructs[dayI] + + for (slotI = 0; slotI < slotStructs.length; slotI += 1) { + slotStruct = slotStructs[slotI] + + slotDayStart = addDays( + dayStruct.date, + slotStruct.dayOffset, + ) + + slotStart = addMs( + slotDayStart, + slotStruct.startTimeMs, + ) + + slotEnd = addMs( + slotDayStart, + slotStruct.endTimeMs, + ) + + if (startTop === null) { // looking for the start + coverage = (start - slotStart.valueOf()) / (slotEnd.valueOf() - slotStart.valueOf()) + startTop = (coverage > 0 && coverage <= 1) + ? (slotStruct.top + slotStruct.height * coverage) + : null + } else { // looking for the end + coverage = (end - slotStart.valueOf()) / (slotEnd.valueOf() - slotStart.valueOf()) + endTop = (coverage >= 0 && coverage < 1) // exclusive + ? (slotStruct.top + slotStruct.height * coverage) + : null + + if (endTop !== null) { // found end + rects.push({ + left: dayStruct.left, + right: dayStruct.right, + top: startTop, + bottom: endTop, + width: dayStruct.right - dayStruct.left, + height: endTop - startTop, + }) + startTop = null + } + } + } + + if (startTop !== null) { // could not find the start in this day + rects.push({ + left: dayStruct.left, + right: dayStruct.right, + top: startTop, + bottom: slotStruct.bottom, + width: dayStruct.right - dayStruct.left, + height: slotStruct.bottom - startTop, + }) + startTop = slotStructs[0].top // top of next column + } + } + + return rects + } + + private computeDayInfo() { + let dayEls = this.getAllDayEls() + + let days = dayEls.map((node) => { + let rect = node.getBoundingClientRect() + return $.extend({}, rect, { + date: parseMarker( + node.getAttribute('data-date'), + ).marker, + }) + }) + + return days + } + + private computeSlotInfo() { + let slotEls = this.getSlotEls() + let slots = slotEls.map((node) => { + let rect = node.getBoundingClientRect() + return $.extend({}, rect, { + startTimeMs: createDuration( + node.getAttribute('data-time'), + ).milliseconds, + }) as any + }) + + let len = slots.length + if (len < 3) { + console.log('need at least 3 slots') // eslint-disable-line no-console + return [] + } + + let mid = Math.floor(len / 2) + let i = mid - 1 + let standardMs = slots[mid + 1].startTimeMs - slots[mid].startTimeMs + let ms + let dayOffset = 0 + + // iterate from one-before middle to beginning + for (i = mid - 1; i >= 0; i -= 1) { + ms = slots[i + 1].startTimeMs - slots[i].startTimeMs + + // big deviation? assume moved to previous day (b/c of special slotMinTime) + if (Math.abs(ms - standardMs) > standardMs * 2) { + dayOffset -= 1 + slots[i].endTimeMs = slots[i].startTimeMs + standardMs + } else { // otherwise, current slot's end is next slot's beginning + slots[i].endTimeMs = slots[i + 1].startTimeMs + } + + slots[i].dayOffset = dayOffset + } + + dayOffset = 0 + + // iterate from middle to one-before last + for (i = mid; i < len - 1; i += 1) { + ms = slots[i + 1].startTimeMs - slots[i].startTimeMs + + slots[i].dayOffset = dayOffset + + // big deviation? assume moved to next day (b/c of special slotMaxTime) + if (Math.abs(ms - standardMs) > standardMs * 2) { + dayOffset += 1 // will apply to the next slotStruct + slots[i].endTimeMs = slots[i].startTimeMs + standardMs + } else { // otherwise, current slot's end is next slot's beginning + slots[i].endTimeMs = slots[i + 1].startTimeMs + } + } + + // assume last slot has the standard duration + slots[i].endTimeMs = slots[i].startTimeMs + standardMs + slots[i].dayOffset = dayOffset + + // if last slot went over the day threshold + if (slots[i].endTimeMs > 1000 * 60 * 60 * 24) { + slots[i].endTimeMs -= 1000 * 60 * 60 * 24 + slots[i].dayOffset += 1 + } + + return slots + } + + getEventEls() { // FG events + return findElements(this.el, '.fc-timegrid-event') + } + + getFirstEventEl() { + return this.el.querySelector('.fc-timegrid-event') as HTMLElement + } + + getBgEventEls() { + return findElements(this.el, '.fc-bg-event') + } + + getEventTimeTexts() { + return this.getEventEls().map((eventEl) => $(eventEl.querySelector('.fc-event-time')).text()) + } + + static getEventElInfo(eventEl) { + return { + title: $(eventEl).find('.fc-event-title').text(), + timeText: $(eventEl).find('.fc-event-time').text(), + } + } + + /* + Returns a boolean. + TODO: check isStart/isEnd. + */ + checkEventRendering(start, end) { + if (typeof start === 'string') { + start = new Date(start) + } + if (typeof end === 'string') { + end = new Date(end) + } + + let expectedRects = this.computeSpanRects(start, end) + let eventEls = this.getEventEls() // sorted by DOM order. not good for RTL + let isMatch = checkEventRenderingMatch(expectedRects, eventEls) + + return { + rects: expectedRects, + els: eventEls, + length: eventEls.length, + isMatch, + } + } +} + +function checkEventRenderingMatch(expectedRects, eventEls) { + let expectedLength = expectedRects.length + let i + let expectedRect + let elRect + + if (eventEls.length !== expectedLength) { + console.log('does not match element count') // eslint-disable-line no-console + return false + } + + for (i = 0; i < expectedLength; i += 1) { + expectedRect = expectedRects[i] + elRect = eventEls[i].getBoundingClientRect() + + // horizontally contained AND vertically really similar? + if (!( + elRect.left >= expectedRect.left && + elRect.right <= expectedRect.right && + Math.abs(elRect.top - expectedRect.top) < 1 && + Math.abs(elRect.bottom + 1 - expectedRect.bottom) < 1 // add 1 because of bottom margin! + )) { + console.log('rects do not match') // eslint-disable-line no-console + return false + } + } + + return true +} + +export function queryEventElInfo(eventEl: HTMLElement) { + return { + timeText: $(eventEl.querySelector('.fc-event-time')).text(), + isShort: eventEl.classList.contains('fc-timegrid-event-short'), + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/ToolbarWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/ToolbarWrapper.ts new file mode 100644 index 0000000..a3b2cf5 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/ToolbarWrapper.ts @@ -0,0 +1,65 @@ +export class ToolbarWrapper { + constructor(private el: HTMLElement) { + } + + getButtonEnabled(name) { + let buttonEl = this.el.querySelector('.fc-' + name + '-button') as HTMLButtonElement + return buttonEl && !buttonEl.disabled + } + + getButtonInfo(name, iconPrefix = 'fc-icon') { // prefix doesnt have dash + let el = this.getButtonEl(name) + + if (el) { + let iconEl = el.querySelector(`.${iconPrefix}`) + let iconNameMatch = iconEl && iconEl.className.match(new RegExp(`${iconPrefix}-([^ ]+)`)) + + return { + text: $(el).text(), + iconEl, + iconName: iconNameMatch ? iconNameMatch[1] : '', + } + } + + return null + } + + getButtonEl(name) { // for custom or standard buttons + return this.el.querySelector(`.fc-${name}-button`) + } + + getTitleText() { + return (this.el.querySelector('.fc-toolbar-title') as HTMLElement).innerText.trim() + } + + getSectionContent(index) { // 0=start, 1=center, 2=end + return processSectionItems( + this.el.querySelectorAll('.fc-toolbar-chunk')[index] as HTMLElement, + ) + } +} + +function processSectionItems(sectionEl: HTMLElement) { + let children = Array.prototype.slice.call(sectionEl.children) as HTMLElement[] + + return children.map((childEl) => { + if (childEl.classList.contains('fc-button')) { + return { + type: 'button', + name: childEl.className.match(/fc-(\w+)-button/)[1], + } + } + if (childEl.classList.contains('fc-button-group')) { + return { + type: 'button-group', + children: processSectionItems(childEl), + } + } + if (childEl.nodeName === 'H2') { + return { + type: 'title', + } + } + throw new Error('Unknown type of content in toolbar') + }) +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/ViewWrapper.ts b/fullcalendar-main/tests/src/lib/wrappers/ViewWrapper.ts new file mode 100644 index 0000000..e073bcb --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/ViewWrapper.ts @@ -0,0 +1,13 @@ +import { Calendar } from '@fullcalendar/core' + +export class ViewWrapper { + el: HTMLElement // TODO: make protected? + + constructor(calendar: Calendar, className: string) { + let viewEl = calendar.el.querySelector('.fc-view') as HTMLElement + if (!viewEl || !viewEl.classList.contains(className)) { + throw new Error(`Can't find view with className '${className}' in test model`) + } + this.el = viewEl + } +} diff --git a/fullcalendar-main/tests/src/lib/wrappers/interaction-util.ts b/fullcalendar-main/tests/src/lib/wrappers/interaction-util.ts new file mode 100644 index 0000000..95b0913 --- /dev/null +++ b/fullcalendar-main/tests/src/lib/wrappers/interaction-util.ts @@ -0,0 +1,105 @@ +import { Calendar } from '@fullcalendar/core' + +export function waitEventDrag(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let modifiedEvent: any = false + + calendar.on('eventDrop', (arg) => { + modifiedEvent = arg.event + }) + + calendar.on('_noEventDrop', () => { + resolve(false) + }) + + dragging.then(() => { + setTimeout(() => { // wait for eventDrop to fire + resolve(modifiedEvent) + }) + }) + }) +} + +export function waitEventDrag2(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let theArg: any = false + + calendar.on('eventDrop', (arg) => { + theArg = arg + }) + + calendar.on('_noEventDrop', () => { + resolve(false) + }) + + dragging.then(() => { + setTimeout(() => { // wait for eventDrop to fire + resolve(theArg) + }) + }) + }) +} + +export function waitEventResize(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let modifiedEvent: any = false + + calendar.on('eventResize', (arg) => { + modifiedEvent = arg.event + }) + + dragging.then(() => { + setTimeout(() => { // wait for eventResize to fire + resolve(modifiedEvent) + }) + }) + }) +} + +export function waitEventResize2(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let theArg: any = false + + calendar.on('eventResize', (arg) => { + theArg = arg + }) + + dragging.then(() => { + setTimeout(() => { // wait for eventResize to fire + resolve(theArg) + }) + }) + }) +} + +export function waitDateSelect(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let selectInfo = null + + calendar.on('select', (arg) => { + selectInfo = arg + }) + + dragging.then(() => { + setTimeout(() => { // wait for select to fire + resolve(selectInfo) + }) + }) + }) +} + +export function waitDateClick(calendar: Calendar, dragging: Promise<any>) { + return new Promise<any>((resolve) => { + let dateClickArg = null + + calendar.on('dateClick', (arg) => { + dateClickArg = arg + }) + + dragging.then(() => { + setTimeout(() => { // wait for dateClick to fire + resolve(dateClickArg) + }) + }) + }) +} diff --git a/fullcalendar-main/tests/src/performance/daygrid-rerenders.ts b/fullcalendar-main/tests/src/performance/daygrid-rerenders.ts new file mode 100644 index 0000000..69a8afd --- /dev/null +++ b/fullcalendar-main/tests/src/performance/daygrid-rerenders.ts @@ -0,0 +1,57 @@ +it('daygrid view rerenders well', (done) => { + let dayHeaderRenderCnt = 0 + let dayCellRenderCnt = 0 + let eventRenderCnt = 0 + + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-10-04', + windowResizeDelay: 0, + events: [ + { title: 'event 0', start: '2017-10-04' }, + ], + dayHeaderContent() { + dayHeaderRenderCnt += 1 + }, + dayCellContent() { + dayCellRenderCnt += 1 + }, + eventContent() { + eventRenderCnt += 1 + }, + }) + + function resetCounts() { + dayHeaderRenderCnt = 0 + dayCellRenderCnt = 0 + eventRenderCnt = 0 + } + + expect(dayHeaderRenderCnt).toBe(7) + expect(dayCellRenderCnt).toBe(42) + expect(eventRenderCnt).toBe(1) + + resetCounts() + calendar.next() + + expect(dayHeaderRenderCnt).toBe(0) // same day-of-week headers + expect(dayCellRenderCnt).toBe(42) + expect(eventRenderCnt).toBe(0) // event will be out of view + + calendar.changeView('listWeek') // switch away + resetCounts() + calendar.changeView('dayGridMonth') // return to view + expect(dayHeaderRenderCnt).toBe(7) + expect(dayCellRenderCnt).toBe(42) + expect(eventRenderCnt).toBe(0) // event still out of view + + resetCounts() + $(window).simulate('resize') + setTimeout(() => { + expect(dayHeaderRenderCnt).toBe(0) + expect(dayCellRenderCnt).toBe(0) + expect(eventRenderCnt).toBe(0) + + done() + }, 1) // more than windowResizeDelay +}) diff --git a/fullcalendar-main/tests/src/performance/list-rerenders.ts b/fullcalendar-main/tests/src/performance/list-rerenders.ts new file mode 100644 index 0000000..ae0af6e --- /dev/null +++ b/fullcalendar-main/tests/src/performance/list-rerenders.ts @@ -0,0 +1,56 @@ +it('list view rerenders well', (done) => { + let dayRenderCnt = 0 + let eventRenderCnt = 0 + let noEventsRenderCnt = 0 + + let calendar = initCalendar({ + initialView: 'listWeek', + initialDate: '2017-10-04', + windowResizeDelay: 0, + events: [ + { title: 'event 0', start: '2017-10-04' }, + ], + dayHeaderContent() { + dayRenderCnt += 1 + }, + eventContent() { + eventRenderCnt += 1 + }, + noEventsContent() { + noEventsRenderCnt += 1 + }, + }) + + function resetCounts() { + dayRenderCnt = 0 + eventRenderCnt = 0 + noEventsRenderCnt = 0 + } + + expect(dayRenderCnt).toBe(1) + expect(eventRenderCnt).toBe(1) + expect(noEventsRenderCnt).toBe(0) + + resetCounts() + calendar.next() + expect(dayRenderCnt).toBe(0) // no days + expect(eventRenderCnt).toBe(0) // event will be out of view + expect(noEventsRenderCnt).toBe(1) + + calendar.changeView('dayGridWeek') // switch away + resetCounts() + calendar.changeView('listWeek') // return to view + expect(dayRenderCnt).toBe(0) + expect(eventRenderCnt).toBe(0) + expect(noEventsRenderCnt).toBe(1) + + resetCounts() + $(window).simulate('resize') + setTimeout(() => { + expect(dayRenderCnt).toBe(0) + expect(eventRenderCnt).toBe(0) + expect(noEventsRenderCnt).toBe(0) + + done() + }, 1) // more than windowResizeDelay +}) diff --git a/fullcalendar-main/tests/src/performance/mutateOptions.ts b/fullcalendar-main/tests/src/performance/mutateOptions.ts new file mode 100644 index 0000000..3921a53 --- /dev/null +++ b/fullcalendar-main/tests/src/performance/mutateOptions.ts @@ -0,0 +1,78 @@ +import { Calendar } from '@fullcalendar/core' +import timeGridPlugin from '@fullcalendar/timegrid' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +function buildOptions() { + return { + plugins: [timeGridPlugin], + initialView: 'timeGridWeek', + initialDate: '2019-04-01', + scrollTime: '00:00', + allDaySlot: true, + events: [ + { start: '2019-04-01T00:00:00' }, + { start: '2019-04-01T02:00:00' }, + ], + } +} + +describe('mutateOptions', () => { // TODO: rename file + let $calendarEl + let calendar + + beforeEach(() => { + $calendarEl = $('<div>').appendTo('body') + }) + + afterEach(() => { + if (calendar) { calendar.destroy() } + $calendarEl.remove() + }) + + it('will react to a single option and keep scroll', () => { + calendar = new Calendar($calendarEl[0], buildOptions()) + calendar.render() + + let viewWrapper = new TimeGridViewWrapper(calendar) + let scrollEl = viewWrapper.getScrollerEl() + + scrollEl.scrollTop = 100 + let scrollTop = scrollEl.scrollTop + expect(scrollTop).toBeGreaterThan(0) + + calendar.setOption('allDaySlot', false) + + expect(calendar.getOption('allDaySlot')).toBe(false) + expect(viewWrapper.dayGrid).toBeFalsy() + expect(scrollEl.scrollTop).toBe(scrollTop) + }) + + it('rerenders events without rerendering view', () => { + calendar = new Calendar($calendarEl[0], buildOptions()) + calendar.render() + + let calendarWrapper = new CalendarWrapper(calendar) + let dateEl = calendarWrapper.getFirstDateEl() + + calendar.setOption('events', [ + { start: '2019-04-01T00:00:00' }, + ]) + + expect(calendarWrapper.getEventEls().length).toBe(1) + expect(calendarWrapper.getFirstDateEl()).toBe(dateEl) + }) + + it('doesn\'t rerender anything for a initialView change', () => { + calendar = new Calendar($calendarEl[0], buildOptions()) + calendar.render() + + let calendarWrapper = new CalendarWrapper(calendar) + let dateEl = calendarWrapper.getFirstDateEl() + + calendar.setOption('initialView', 'timeGridDay') + + expect(calendar.view.type).toBe('timeGridWeek') + expect(calendarWrapper.getFirstDateEl()).toBe(dateEl) + }) +}) diff --git a/fullcalendar-main/tests/src/performance/timegrid-rerenders.ts b/fullcalendar-main/tests/src/performance/timegrid-rerenders.ts new file mode 100644 index 0000000..ef90306 --- /dev/null +++ b/fullcalendar-main/tests/src/performance/timegrid-rerenders.ts @@ -0,0 +1,74 @@ +it('timegrid view rerenders well', (done) => { + let dayHeaderRenderCnt = 0 + let dayCellRenderCnt = 0 + let slotLabelRenderCnt = 0 + let slotLaneRenderCnt = 0 + let eventRenderCnt = 0 + + let calendar = initCalendar({ + initialView: 'timeGridWeek', + initialDate: '2017-10-04', + windowResizeDelay: 0, + events: [ + { title: 'event 0', start: '2017-10-04T00:00:00' }, + ], + dayHeaderContent() { + dayHeaderRenderCnt += 1 + }, + dayCellContent() { + dayCellRenderCnt += 1 + }, + slotLabelContent() { + slotLabelRenderCnt += 1 + }, + slotLaneContent() { + slotLaneRenderCnt += 1 + }, + eventContent() { + eventRenderCnt += 1 + }, + }) + + function resetCounts() { + dayHeaderRenderCnt = 0 + dayCellRenderCnt = 0 + slotLabelRenderCnt = 0 + slotLaneRenderCnt = 0 + eventRenderCnt = 0 + } + + expect(dayHeaderRenderCnt).toBe(7) + expect(dayCellRenderCnt).toBe(14) // all-day row AND time cols + expect(slotLabelRenderCnt).toBe(24) // one slot per every 2 lanes + expect(slotLaneRenderCnt).toBe(48) + expect(eventRenderCnt).toBe(1) + + resetCounts() + calendar.next() + expect(dayHeaderRenderCnt).toBe(7) + expect(dayCellRenderCnt).toBe(14) + expect(slotLabelRenderCnt).toBe(0) + expect(slotLaneRenderCnt).toBe(0) + expect(eventRenderCnt).toBe(0) // event will be out of view + + calendar.changeView('listWeek') // switch away + resetCounts() + calendar.changeView('timeGridWeek') // return to view + expect(dayHeaderRenderCnt).toBe(7) + expect(dayCellRenderCnt).toBe(14) + expect(slotLabelRenderCnt).toBe(24) + expect(slotLaneRenderCnt).toBe(48) + expect(eventRenderCnt).toBe(0) // event still out of view + + resetCounts() + $(window).simulate('resize') + setTimeout(() => { + expect(dayHeaderRenderCnt).toBe(0) + expect(dayCellRenderCnt).toBe(0) + expect(slotLabelRenderCnt).toBe(0) + expect(slotLaneRenderCnt).toBe(0) + expect(eventRenderCnt).toBe(0) + + done() + }, 1) // more than windowResizeDelay +}) diff --git a/fullcalendar-main/tests/src/theme/bootstrap4.ts b/fullcalendar-main/tests/src/theme/bootstrap4.ts new file mode 100644 index 0000000..b670ed4 --- /dev/null +++ b/fullcalendar-main/tests/src/theme/bootstrap4.ts @@ -0,0 +1,45 @@ +import bootstrapPlugin from '@fullcalendar/bootstrap' +import dayGridPlugin from '@fullcalendar/daygrid' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('bootstrap theme', () => { + pushOptions({ + plugins: [bootstrapPlugin, dayGridPlugin], + themeSystem: 'bootstrap', + }) + + describe('fa', () => { + pushOptions({ + headerToolbar: { left: '', center: '', right: 'next' }, + }) + + it('renders default', () => { + let calendar = initCalendar() + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('next', 'fa') + + expect(buttonInfo.iconName).toBe('chevron-right') + }) + + it('renders a customized icon', () => { + let calendar = initCalendar({ + bootstrapFontAwesome: { + next: 'asdf', + }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('next', 'fa') + + expect(buttonInfo.iconName).toBe('asdf') + }) + + it('renders text when specified as false', () => { + let calendar = initCalendar({ + bootstrapFontAwesome: false, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getButtonInfo('next', 'fa').iconName).toBeFalsy() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/theme/switching.ts b/fullcalendar-main/tests/src/theme/switching.ts new file mode 100644 index 0000000..0ae4359 --- /dev/null +++ b/fullcalendar-main/tests/src/theme/switching.ts @@ -0,0 +1,31 @@ +import bootstrapPlugin from '@fullcalendar/bootstrap' +import dayGridPlugin from '@fullcalendar/daygrid' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('theme switching', () => { + pushOptions({ + plugins: [bootstrapPlugin, dayGridPlugin], + }) + + it('can switch from standard to bootstrap', () => { + let calendar = initCalendar() + verifyStandardTheme(calendar) + currentCalendar.setOption('themeSystem', 'bootstrap') + verifyBootstrapTheme(calendar) + }) + + it('can switch from bootstrap to standard', () => { + let calendar = initCalendar({ themeSystem: 'bootstrap' }) + verifyBootstrapTheme(calendar) + currentCalendar.setOption('themeSystem', 'standard') + verifyStandardTheme(calendar) + }) + + function verifyStandardTheme(calendar) { + expect(calendar.el).toHaveClass(CalendarWrapper.UNTHEMED_CLASSNAME) + } + + function verifyBootstrapTheme(calendar) { + expect(calendar.el).toHaveClass(CalendarWrapper.BOOTSTRAP_CLASSNAME) + } +}) diff --git a/fullcalendar-main/tests/src/toolbar/customButtons.ts b/fullcalendar-main/tests/src/toolbar/customButtons.ts new file mode 100644 index 0000000..9d3852a --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/customButtons.ts @@ -0,0 +1,46 @@ +import bootstrapPlugin from '@fullcalendar/bootstrap' +import dayGridPlugin from '@fullcalendar/daygrid' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('customButtons', () => { + pushOptions({ + plugins: [bootstrapPlugin, dayGridPlugin], + }) + + it('can specify text', () => { + let calendar = initCalendar({ + customButtons: { + mybutton: { text: 'asdf' }, + }, + headerToolbar: { left: 'mybutton', center: '', right: '' }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('mybutton') + expect(buttonInfo.text).toBe('asdf') + }) + + it('can specify an icon', () => { + let calendar = initCalendar({ + customButtons: { + mybutton: { icon: 'asdf' }, + }, + headerToolbar: { left: 'mybutton', center: '', right: '' }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('mybutton') + expect(buttonInfo.iconName).toBe('asdf') + }) + + it('can specify a bootstrap font-awesome icon', () => { + let calendar = initCalendar({ + themeSystem: 'bootstrap', + customButtons: { + mybutton: { bootstrapFontAwesome: 'asdf' }, + }, + headerToolbar: { left: 'mybutton', center: '', right: '' }, + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let buttonInfo = toolbarWrapper.getButtonInfo('mybutton', 'fa') + expect(buttonInfo.iconName).toBe('asdf') + }) +}) diff --git a/fullcalendar-main/tests/src/toolbar/next-button.ts b/fullcalendar-main/tests/src/toolbar/next-button.ts new file mode 100644 index 0000000..83297a2 --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/next-button.ts @@ -0,0 +1,80 @@ +/* +TODO: +- quick test for when button is clicked + +SEE ALSO: +- visibleRange, dateAlignment, dateIncrement +*/ + +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('next button', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + }) + + describe('when there is no validRange', () => { + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when next date range is completely within validRange', () => { + pushOptions({ + validRange: { end: '2018-06-10' }, + dateIncrement: { years: 1 }, // next range is 2018-06-03 - 2018-06-10 + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when next date range is partially outside validRange', () => { + pushOptions({ + validRange: { end: '2018-06-05' }, + dateIncrement: { years: 1 }, // next range is 2018-06-03 - 2018-06-10 + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when next date range is completely beyond validRange', () => { + pushOptions({ + validRange: { end: '2018-06-03' }, + dateIncrement: { years: 1 }, // next range is 2018-06-03 - 2018-06-10 + }) + it('is disabled', () => { + expectEnabled(initCalendar(), false) + }) + }) + + describe('when day after current day is a hidden day', () => { + pushOptions({ + initialDate: '2017-03-31', + initialView: 'dayGridDay', + weekends: false, + dateIncrement: { years: 1 }, // next range is 2018-06-03 - 2018-06-10 + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when initialDate is constrained forward to validRange and next week is valid', () => { + pushOptions({ + initialDate: '2017-07-17', + initialView: 'timeGridWeek', + validRange: { start: '2036-05-03', end: '2036-06-01' }, + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + function expectEnabled(calendar, bool) { + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getButtonEnabled('next')).toBe(bool) + } +}) diff --git a/fullcalendar-main/tests/src/toolbar/prev-button.ts b/fullcalendar-main/tests/src/toolbar/prev-button.ts new file mode 100644 index 0000000..41aaf96 --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/prev-button.ts @@ -0,0 +1,73 @@ +/* +TODO: +- quick test for when button is clicked + +SEE ALSO: +- other range intersection tests handled by next-button +*/ + +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('prev button', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + }) + + describe('when there is no specified validRange', () => { + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when prev date range is completely before validRange', () => { + pushOptions({ + validRange: { start: '2018-06-12' }, + dateIncrement: { years: 1 }, // prev range is 2016-06-05 - 2016-06-12 + }) + it('is disabled', () => { + expectEnabled(initCalendar(), false) + }) + }) + + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-03-01', + validRange: { start: '2017-02-07' }, + dateIncrement: { years: 1 }, // prev range is 2016-06-05 - 2016-06-12 + }) + + it('when prev date range is partially before validRange', () => { + expectEnabled(initCalendar(), false) + }) + }) + + describe('when day before current day is a hidden day', () => { + pushOptions({ + initialDate: '2017-03-27', + initialView: 'dayGridDay', + weekends: false, + dateIncrement: { years: 1 }, // prev range is 2016-06-05 - 2016-06-12 + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when initialDate is constrained backward to validRange and prev week is valid', () => { + pushOptions({ + initialDate: '2017-07-17', + initialView: 'timeGridWeek', + validRange: { start: '2017-03-20', end: '2017-03-30' }, + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + function expectEnabled(calendar, bool) { + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getButtonEnabled('prev')).toBe(bool) + } +}) diff --git a/fullcalendar-main/tests/src/toolbar/title.ts b/fullcalendar-main/tests/src/toolbar/title.ts new file mode 100644 index 0000000..105afcd --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/title.ts @@ -0,0 +1,22 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('calendar title', () => { + pushOptions({ + now: '2017-03-29', + }) + + describe('when switching to and from a view', () => { + it('updates the title at each switch', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + }) + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + + expect(toolbarWrapper.getTitleText()).toBe('March 2017') + currentCalendar.changeView('timeGridWeek') + expect(toolbarWrapper.getTitleText()).toBe('Mar 26 – Apr 1, 2017') + currentCalendar.changeView('dayGridMonth') + expect(toolbarWrapper.getTitleText()).toBe('March 2017') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/toolbar/today-button.ts b/fullcalendar-main/tests/src/toolbar/today-button.ts new file mode 100644 index 0000000..5b38c84 --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/today-button.ts @@ -0,0 +1,60 @@ +/* +TODO: +- quick test for when button is clicked + +SEE ALSO: +- other range intersection tests handled by next-button +*/ + +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('today button', () => { + pushOptions({ + initialView: 'dayGridMonth', + now: '2017-06-30', + }) + + describe('when now is in current month', () => { + pushOptions({ + initialDate: '2017-06-01', + }) + it('is disabled', () => { + expectEnabled(initCalendar(), false) + }) + }) + + describe('when now is not current month, but still visible', () => { + pushOptions({ + initialDate: '2017-07-01', + }) + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when now is out of view', () => { + pushOptions({ + initialDate: '2017-08-01', + }) + + describe('when no specified validRange', () => { + it('is enabled', () => { + expectEnabled(initCalendar(), true) + }) + }) + + describe('when now\'s month is entirely before validRange', () => { + pushOptions({ + validRange: { start: '2017-07-02' }, // previous day is visible in the June + }) + it('is disabled', () => { + expectEnabled(initCalendar(), false) + }) + }) + }) + + function expectEnabled(calendar, bool) { + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + expect(toolbarWrapper.getButtonEnabled('today')).toBe(bool) + } +}) diff --git a/fullcalendar-main/tests/src/toolbar/toolbar-misc.ts b/fullcalendar-main/tests/src/toolbar/toolbar-misc.ts new file mode 100644 index 0000000..f0f4f15 --- /dev/null +++ b/fullcalendar-main/tests/src/toolbar/toolbar-misc.ts @@ -0,0 +1,31 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('toolbar rendering', () => { + it('produces type="button" attributes', () => { + let calendar = initCalendar({ + headerToolbar: { + left: 'today', + center: 'title', + right: 'prev,next', + }, + }) + + let toolbarWrapper = new CalendarWrapper(calendar).toolbar + let todayButtonEl = toolbarWrapper.getButtonEl('today') + let prevButtonEl = toolbarWrapper.getButtonEl('prev') + + expect(todayButtonEl.getAttribute('type')).toBe('button') + expect(prevButtonEl.getAttribute('type')).toBe('button') + }) + + it('if disabled, won\'t put aria-labelledby on view container', () => { + let calendar = initCalendar({ + headerToolbar: false, + }) + + const calendarWrapper = new CalendarWrapper(calendar) + const viewContainerEl = calendarWrapper.getViewContainerEl() + + expect(viewContainerEl).not.toHaveAttr('aria-labelledby') + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/dateAlignment.ts b/fullcalendar-main/tests/src/view-dates/dateAlignment.ts new file mode 100644 index 0000000..d807a62 --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/dateAlignment.ts @@ -0,0 +1,53 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' + +/* +SEE ALSO: next/prev +*/ +describe('dateAlignment', () => { + describe('when week alignment', () => { + pushOptions({ + initialView: 'timeGrid', + dateAlignment: 'week', + initialDate: '2017-06-15', + }) + + describe('when 3 day duration', () => { + pushOptions({ + duration: { days: 3 }, + }) + + it('aligns with the week', () => { + initCalendar() + expectActiveRange('2017-06-11', '2017-06-14') + }) + }) + + describe('when 5 day count', () => { + pushOptions({ + dayCount: 5, + weekends: false, + }) + + it('aligns with first visible day of the week', () => { + initCalendar() + expectActiveRange('2017-06-12', '2017-06-17') + }) + }) + }) + + // test in Safari! + // https://github.com/fullcalendar/fullcalendar/issues/4363 + describe('when year alignment', () => { + pushOptions({ + initialView: 'dayGrid', + duration: { months: 1 }, + dateAlignment: 'year', + initialDate: '2017-06-15', + }) + + it('aligns with first day of year', () => { + initCalendar() + expectActiveRange('2017-01-01', '2017-02-05') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/dateIncrement.ts b/fullcalendar-main/tests/src/view-dates/dateIncrement.ts new file mode 100644 index 0000000..de8e604 --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/dateIncrement.ts @@ -0,0 +1,4 @@ +/* +SEE: prev/next +SEE: dateIncrement tests in fullcalendar-scheduler +*/ diff --git a/fullcalendar-main/tests/src/view-dates/datesSet.ts b/fullcalendar-main/tests/src/view-dates/datesSet.ts new file mode 100644 index 0000000..1c19235 --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/datesSet.ts @@ -0,0 +1,80 @@ +import { Calendar } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' + +describe('datesSet', () => { + pushOptions({ + initialView: 'dayGridMonth', + now: '2020-06-21', + }) + + it('won\'t fire when a non-dateprofile-related option is reset', () => { + let fireCnt = 0 + let options = { + ...getCurrentOptions(), + weekNumbers: false, + datesSet() { + fireCnt += 1 + }, + } + let $calendarEl = $('<div>').appendTo('body') + let calendar = new Calendar($calendarEl[0], options) + calendar.render() + expect(fireCnt).toBe(1) + calendar.resetOptions({ + ...options, + weekNumbers: true, + }) + expect(fireCnt).toBe(1) + calendar.destroy() + $calendarEl.remove() + }) + + it('won\'t fire when a complex object-like option is reset', () => { + function buildHeaderToolbar() { + return { + left: 'today', + } + } + let fireCnt = 0 + let options = { + ...getCurrentOptions(), + headerToolbar: buildHeaderToolbar(), + datesSet() { + fireCnt += 1 + }, + } + let $calendarEl = $('<div>').appendTo('body') + let calendar = new Calendar($calendarEl[0], options) + calendar.render() + expect(fireCnt).toBe(1) + calendar.resetOptions({ + ...options, + headerToolbar: buildHeaderToolbar(), + }) + expect(fireCnt).toBe(1) + calendar.destroy() + $calendarEl.remove() + }) + + it('won\'t fire when plugins option is reset', () => { + let fireCnt = 0 + let options = { + ...getCurrentOptions(), + plugins: [dayGridPlugin], + datesSet() { + fireCnt += 1 + }, + } + let $calendarEl = $('<div>').appendTo('body') + let calendar = new Calendar($calendarEl[0], options) + calendar.render() + expect(fireCnt).toBe(1) + calendar.resetOptions({ + ...options, + plugins: [dayGridPlugin], + }) + expect(fireCnt).toBe(1) + calendar.destroy() + $calendarEl.remove() + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/dayCount.ts b/fullcalendar-main/tests/src/view-dates/dayCount.ts new file mode 100644 index 0000000..ca519bb --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/dayCount.ts @@ -0,0 +1,81 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' +import { expectDay } from '../lib/ViewRenderUtils.js' + +describe('dayCount', () => { + pushOptions({ + initialDate: '2017-03-15', // wed + weekends: false, + }) + + describeOptions({ + 'when specified as top-level options': { + initialView: 'dayGrid', + dayCount: 5, + }, + 'when specified as custom view': { + views: { + myCustomView: { + type: 'dayGrid', + dayCount: 5, + }, + }, + initialView: 'myCustomView', + }, + }, () => { + it('renders the exact day count', () => { + initCalendar() + expectActiveRange('2017-03-15', '2017-03-22') + expectDay('2017-03-15', true) + expectDay('2017-03-16', true) + expectDay('2017-03-17', true) + expectDay('2017-03-18', false) // sat + expectDay('2017-03-19', false) // sun + expectDay('2017-03-20', true) + expectDay('2017-03-21', true) + }) + }) + + it('can span multiple weeks', () => { + initCalendar({ + initialView: 'timeGrid', + dayCount: 9, + }) + expectActiveRange('2017-03-15', '2017-03-28') + expectDay('2017-03-15', true) + expectDay('2017-03-16', true) + expectDay('2017-03-17', true) + expectDay('2017-03-18', false) // sat + expectDay('2017-03-19', false) // sun + expectDay('2017-03-20', true) + expectDay('2017-03-21', true) + expectDay('2017-03-22', true) + expectDay('2017-03-23', true) + expectDay('2017-03-24', true) + expectDay('2017-03-25', false) // sat + expectDay('2017-03-26', false) // sun + expectDay('2017-03-27', true) + }) + + it('can navigate in reverse with a small dateIncrement split by hidden days', () => { + initCalendar({ + initialDate: '2018-06-11', + initialView: 'timeGridTwoDay', + headerToolbar: { + left: 'prev,next', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,timeGridTwoDay', + }, + hiddenDays: [0, 6], // sunday, saturday + views: { + timeGridTwoDay: { + type: 'timeGrid', + dayCount: 2, + dateIncrement: { days: 1 }, + buttonText: '2 days', + }, + }, + }) + currentCalendar.prev() + expectActiveRange('2018-06-08', '2018-06-12') + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/gotoDate.ts b/fullcalendar-main/tests/src/view-dates/gotoDate.ts new file mode 100644 index 0000000..4129d6f --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/gotoDate.ts @@ -0,0 +1,57 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' + +describe('gotoDate', () => { + it('will update calendar\'s date even if no navigation', () => { + initCalendar({ + initialDate: '2018-12-25', + initialView: 'dayGridMonth', + }) + + expect(currentCalendar.getDate()).toEqualDate('2018-12-25') + currentCalendar.gotoDate('2018-12-30') + expect(currentCalendar.getDate()).toEqualDate('2018-12-30') + }) + + describe('when asynchronicity', () => { + pushOptions({ + events(arg, callback) { + setTimeout(() => { + callback([]) + }, 0) + }, + }) + + it('works when called right after initialization', () => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-03-30', + }) + currentCalendar.gotoDate('2017-06-01') + }) + + it('works when called right after initialization when date already in range', () => { + initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2017-03-30', + }) + currentCalendar.gotoDate('2017-03-01') + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4929 + it('moves view\'s date range when small dateAlignment', () => { + let calendar = initCalendar({ + initialDate: '2019-04-09', + initialView: 'dayGridFourDays', + views: { + dayGridFourDays: { + type: 'dayGrid', + duration: { days: 4 }, + dateAlignment: 'day', + }, + }, + }) + calendar.gotoDate('2019-04-10') + expectActiveRange('2019-04-10', '2019-04-14') + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/next.ts b/fullcalendar-main/tests/src/view-dates/next.ts new file mode 100644 index 0000000..0203416 --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/next.ts @@ -0,0 +1,145 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('next', () => { + pushOptions({ + initialDate: '2017-06-08', + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + describe('when dateIncrement not specified', () => { + it('moves forward by one week', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-11', '2017-06-18') + }) + }) + + describeOptions('dateIncrement', { + 'when two week dateIncrement specified as a plain object': { weeks: 2 }, + 'when two week dateIncrement specified as a string': '14.00:00:00', + }, () => { + it('moves forward by two weeks', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-18', '2017-06-25') + }) + }) + + it('does not duplicate-render skeleton', () => { + let calendar = initCalendar() + calendar.next() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.isStructureValid()).toBe(true) + }) + }) + + describe('when in a month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + }) + + describe('when dateIncrement not specified', () => { + it('moves forward by one month', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-25', '2017-08-06') + }) + }) + + describe('when two month dateIncrement is specified', () => { + pushOptions({ + dateIncrement: { months: 2 }, + }) + + it('moves forward by two months', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-07-30', '2017-09-10') + }) + }) + }) + + describe('when in custom three day view', () => { + pushOptions({ + initialView: 'dayGrid', + duration: { days: 3 }, + }) + + describe('when no dateAlignment is specified', () => { + describe('when dateIncrement not specified', () => { + it('moves forward three days', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-11', '2017-06-14') + }) + }) + + describe('when two day dateIncrement is specified', () => { + pushOptions({ + dateIncrement: { days: 2 }, + }) + it('moves forward two days', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-10', '2017-06-13') + }) + }) + }) + + describe('when week dateAlignment is specified', () => { + pushOptions({ + dateAlignment: 'week', + }) + + describe('when dateIncrement not specified', () => { + it('moves forward one week', () => { + initCalendar() + currentCalendar.next() + expectActiveRange('2017-06-11', '2017-06-14') + }) + }) + + describe('when two day dateIncrement is specified', () => { + pushOptions({ + dateIncrement: { days: 2 }, + }) + + it('does not navigate nor rerender', () => { + let called + + initCalendar({ + dayCellDidMount() { + called = true + }, + }) + + called = false + currentCalendar.next() + + expectActiveRange('2017-06-04', '2017-06-07') // the same as how it started + expect(called).toBe(false) + }) + }) + }) + }) + + describe('when in a custom two day view and weekends:false', () => { + pushOptions({ + weekends: false, + initialView: 'timeGrid', + duration: { days: 2 }, + }) + + it('skips over weekends if there would be alignment with weekend', () => { + initCalendar({ + initialDate: '2017-11-09', + }) + currentCalendar.next() + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/prev.ts b/fullcalendar-main/tests/src/view-dates/prev.ts new file mode 100644 index 0000000..7eaf5ff --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/prev.ts @@ -0,0 +1,79 @@ +/* +SEE ALSO: +- next (does core of date switching) +*/ + +import { expectActiveRange } from '../lib/ViewDateUtils.js' + +describe('prev', () => { + pushOptions({ + initialDate: '2017-06-08', + }) + + describe('when in a week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + }) + + it('moves back by one week', () => { + initCalendar() + currentCalendar.prev() + expectActiveRange('2017-05-28', '2017-06-04') + }) + + describe('when two week dateIncrement', () => { + pushOptions({ + dateIncrement: { weeks: 2 }, + }) + + it('moves back by two weeks', () => { + initCalendar() + currentCalendar.prev() + expectActiveRange('2017-05-21', '2017-05-28') + }) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4595 + it('can navigate back when starting late in month', () => { + initCalendar({ + initialDate: '2019-03-31T12:00', + initialView: 'dayGridMonth', + }) + expectActiveRange('2019-02-24', '2019-04-07') + currentCalendar.prev() + expectActiveRange('2019-01-27', '2019-03-10') + }) + + // related to #4595 + it('can navigate forward when starting late in month', () => { + initCalendar({ + initialDate: '2019-03-31T12:00', + initialView: 'dayGridMonth', + }) + expectActiveRange('2019-02-24', '2019-04-07') + currentCalendar.next() + expectActiveRange('2019-03-31', '2019-05-12') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/5319 + it('can navigate back twice when duration greater than dateIncrement', () => { + let calendar = initCalendar({ + firstDay: 1, // monday + initialDate: '2021-06-14', + initialView: 'dayGridFourWeeks', + views: { + dayGridFourWeeks: { + type: 'dayGrid', + duration: { weeks: 4 }, + dateIncrement: { weeks: 1 }, + }, + }, + }) + expectActiveRange('2021-06-14', '2021-07-12') + calendar.prev() // back a week + expectActiveRange('2021-06-07', '2021-07-05') + calendar.prev() // back a week + expectActiveRange('2021-05-31', '2021-06-28') + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/validRange.ts b/fullcalendar-main/tests/src/view-dates/validRange.ts new file mode 100644 index 0000000..0e0d15a --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/validRange.ts @@ -0,0 +1,147 @@ +import { expectActiveRange, expectRenderRange } from '../lib/ViewDateUtils.js' +import { parseUtcDate } from '../lib/date-parsing.js' + +describe('validRange', () => { + pushOptions({ + timeZone: 'UTC', + initialDate: '2017-06-08', + }) + + describe('when one week view', () => { // a view that has date-alignment by default + pushOptions({ + initialView: 'timeGridWeek', // default range = 2017-06-04 - 2017-06-11 + }) + + describe('when default range is partially before validRange', () => { + pushOptions({ + validRange: { start: '2017-06-06' }, + }) + + it('allows full renderRange but restricts activeRange', () => { + initCalendar() + expectRenderRange('2017-06-04', '2017-06-11') + expectActiveRange('2017-06-06', '2017-06-11') + }) + }) + + describe('when default range is partially after validRange', () => { + pushOptions({ + validRange: { end: '2017-06-05' }, + }) + + it('allows full renderRange but restricts activeRange', () => { + initCalendar() + expectRenderRange('2017-06-04', '2017-06-11') + expectActiveRange('2017-06-04', '2017-06-05') + }) + }) + + describe('when default range is completely before validRange', () => { + pushOptions({ + validRange: { start: '2017-06-14' }, // a Wednesday + }) + + it('initializes at earliest partially visible week', () => { + initCalendar() + expectRenderRange('2017-06-11', '2017-06-18') + expectActiveRange('2017-06-14', '2017-06-18') + }) + }) + + describe('when default range is completely before validRange', () => { + pushOptions({ + validRange: { end: '2017-05-24' }, // a Wednesday + }) + + it('initializes at latest partially visible week', () => { + initCalendar() + expectRenderRange('2017-05-21', '2017-05-28') + expectActiveRange('2017-05-21', '2017-05-24') + }) + }) + + describe('when validRange is a function', () => { + let nowInput = '2017-06-09T06:00:00' + + it('receives the nowDate, timezoneless', () => { + let validRangeSpy = spyOnCalendarCallback('validRange', (date) => { + expect(date instanceof Date).toBe(true) + expect(date).toEqualDate(nowInput + 'Z') + }) + + initCalendar({ + now: nowInput, + }) + + expect(validRangeSpy).toHaveBeenCalled() + }) + + it('can return a range object with strings', () => { + let validRangeSpy = spyOnCalendarCallback('validRange', () => ({ start: '2017-06-06' })) + + initCalendar() + + expect(validRangeSpy).toHaveBeenCalled() + expectRenderRange('2017-06-04', '2017-06-11') + expectActiveRange('2017-06-06', '2017-06-11') + }) + + it('can return a range object with Date objects', () => { + let validRangeSpy = spyOnCalendarCallback('validRange', () => ({ start: parseUtcDate('2017-06-06') })) + + initCalendar() + + expect(validRangeSpy).toHaveBeenCalled() + expectRenderRange('2017-06-04', '2017-06-11') + expectActiveRange('2017-06-06', '2017-06-11') + }) + }) + }) + + describe('when a three-day view', () => { // a view with no alignment + pushOptions({ + initialView: 'timeGrid', + duration: { days: 3 }, + }) + + describe('when default range is completely before of validRange', () => { + pushOptions({ + validRange: { start: '2017-06-14' }, + }) + it('renders earliest three valid days', () => { + initCalendar() + expectRenderRange('2017-06-14', '2017-06-17') + expectActiveRange('2017-06-14', '2017-06-17') + }) + }) + + describe('when default range is completely after validRange', () => { + pushOptions({ + validRange: { end: '2017-05-31' }, + }) + it('renders latest possible valid day and two invalid days', () => { + initCalendar() + expectRenderRange('2017-05-30', '2017-06-02') + expectActiveRange('2017-05-30', '2017-05-31') + }) + }) + }) + + describe('when hiddenDays causes no days to be active', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-10-04', + hiddenDays: [6], // Sunday, last day within natural week range + validRange: { + start: '2036-05-03', + end: '2036-06-01', + }, + }) + + it('pushes view to nearest valid range', () => { + initCalendar() + expectRenderRange('2036-05-04', '2036-05-10') + expectActiveRange('2036-05-04', '2036-05-10') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/view-duration.ts b/fullcalendar-main/tests/src/view-dates/view-duration.ts new file mode 100644 index 0000000..0cfe33c --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/view-duration.ts @@ -0,0 +1,28 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' + +describe('view duration', () => { + pushOptions({ + initialView: 'timeGrid', + initialDate: '2017-03-15', + }) + + describe('when specified as a week integer', () => { + pushOptions({ + duration: { weeks: 1 }, + }) + it('aligns with start of week', () => { + initCalendar() + expectActiveRange('2017-03-12', '2017-03-19') + }) + }) + + describe('when specified as 7 days', () => { + pushOptions({ + duration: { days: 7 }, + }) + it('aligns with start of week', () => { + initCalendar() + expectActiveRange('2017-03-15', '2017-03-22') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-dates/visibleRange.ts b/fullcalendar-main/tests/src/view-dates/visibleRange.ts new file mode 100644 index 0000000..e1fbd92 --- /dev/null +++ b/fullcalendar-main/tests/src/view-dates/visibleRange.ts @@ -0,0 +1,251 @@ +import { addLocalDays, startOfLocalDay, startOfUtcDay, addUtcDays } from '../lib/date-math.js' +import { expectActiveRange } from '../lib/ViewDateUtils.js' +import { parseUtcDate, parseLocalDate } from '../lib/date-parsing.js' + +describe('visibleRange', () => { + describe('when custom view with a flexible range', () => { + pushOptions({ + initialView: 'timeGrid', + }) + + describe('when given a valid date range', () => { + let startInput = '2017-06-26' + let endInput = '2017-06-29' + + describeOptions('visibleRange', { + 'of Date objects': { + start: new Date(startInput), + end: new Date(endInput), + }, + 'of strings': { + start: startInput, + end: endInput, + }, + 'of a function that returns date objects': () => ({ + start: new Date(startInput), + end: new Date(endInput), + }), + 'of a function that returns strings': () => ({ + start: startInput, + end: endInput, + }), + }, () => { + it('gets set to the given range', () => { + initCalendar() + expectActiveRange(startInput, endInput) + }) + }) + + it('works as a custom view', () => { + initCalendar({ + views: { + myCustomView: { + type: 'timeGrid', + visibleRange: { + start: startInput, + end: endInput, + }, + }, + }, + initialView: 'myCustomView', + }) + expectActiveRange(startInput, endInput) + }) + + it('ignores dateAlignment', () => { + initCalendar({ + dateAlignment: 'dayGridMonth', + visibleRange: { + start: startInput, + end: endInput, + }, + }) + expectActiveRange(startInput, endInput) + }) + + it('works as a dynamic option', () => { + initCalendar({ + initialView: 'dayGrid', + }) + currentCalendar.setOption('visibleRange', { + start: startInput, + end: endInput, + }) + expectActiveRange(startInput, endInput) + }) + }) + + describe('when a function', () => { + let initialDateInput = '2017-06-08T12:30:00' + + it('receives the calendar\'s initialDate, with local timezone, and emits local range', () => { + let matched = false + + initCalendar({ + timeZone: 'local', + initialDate: initialDateInput, + visibleRange(date) { + // this function will receive the date for prev/next, + // which should be ignored. make sure just one call matches. + if (date.valueOf() === parseLocalDate(initialDateInput).valueOf()) { + matched = true + } + + let dayStart = startOfLocalDay(date) + return { + start: addLocalDays(dayStart, -1), + end: addLocalDays(dayStart, 2), + } + }, + }) + + expect(matched).toBe(true) + expectActiveRange(parseLocalDate('2017-06-07'), parseLocalDate('2017-06-10')) + }) + + it('receives the calendar\'s initialDate, with UTC timezone, and emits UTC range', () => { + let matched = false + + initCalendar({ + timeZone: 'UTC', + initialDate: initialDateInput, + visibleRange(date) { + // this function will receive the date for prev/next, + // which should be ignored. make sure just one call matches. + if (date.valueOf() === parseUtcDate(initialDateInput).valueOf()) { + matched = true + } + + let dayStart = startOfUtcDay(date) + return { + start: addUtcDays(dayStart, -1), + end: addUtcDays(dayStart, 2), + } + }, + }) + + expect(matched).toBe(true) + expectActiveRange('2017-06-07', '2017-06-10') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4517 + it('can emit and timed UTC range that will be rounded', () => { + initCalendar({ + dateIncrement: { days: 3 }, + timeZone: 'UTC', + initialDate: initialDateInput, + visibleRange(date) { + return { + start: addUtcDays(date, -1), // 2017-06-07T12:30:00 -> 2017-06-07 + end: addUtcDays(date, 2), // 2017-06-10T12:30:00 -> 2017-06-11 + } + }, + }) + expectActiveRange('2017-06-07', '2017-06-11') + currentCalendar.prev() + expectActiveRange('2017-06-04', '2017-06-07') // second computation will round down the end + }) + }) + + describe('when given an invalid range', () => { + describeOptions('visibleRange', { + 'with end before start': { + start: '2017-06-18', + end: '2017-06-15', + }, + 'with no end': { + start: '2017-06-18', + }, + 'with no start': { + end: '2017-06-15', + }, + }, () => { + it('defaults to the initialDate', () => { // TODO: have it report an warning + initCalendar({ + initialDate: '2017-08-01', + }) + expectActiveRange('2017-08-01', '2017-08-02') + }) + }) + }) + + describe('when later switching to a one-day view', () => { + it('constrains an earlier current date to the start of visibleRange', () => { + initCalendar({ + initialDate: '2017-06-25', + visibleRange: { + start: '2017-06-26', + end: '2017-06-29', + }, + }) + currentCalendar.changeView('timeGridDay') + expectActiveRange('2017-06-26', '2017-06-27') + }) + + it('constrains a later current date to the start of visibleRange', () => { + initCalendar({ + initialDate: '2017-07-01', + visibleRange: { + start: '2017-06-26', + end: '2017-06-29', + }, + }) + currentCalendar.changeView('timeGridDay') + expectActiveRange('2017-06-26', '2017-06-27') + }) + }) + }) + + describe('when a list view', () => { + pushOptions({ + initialView: 'list', + visibleRange: { + start: '2017-06-07', + end: '2017-06-10', + }, + events: [ + { start: '2017-06-08' }, + ], + }) + + it('respects the given range', () => { + initCalendar() + expectActiveRange('2017-06-07', '2017-06-10') + }) + }) + + describe('when custom view with fixed duration', () => { + pushOptions({ + initialDate: '2015-06-08', + initialView: 'timeGrid', + duration: { days: 3 }, + }) + + it('ignores the given visibleRange', () => { + initCalendar({ + visibleRange: { + start: '2017-06-29', + end: '2017-07-04', + }, + }) + expectActiveRange('2015-06-08', '2015-06-11') + }) + }) + + describe('when standard view', () => { + pushOptions({ + initialDate: '2015-06-08', + initialView: 'timeGridWeek', + }) + + it('ignores the given visibleRange', () => { + initCalendar({ + visibleRange: { + start: '2017-06-29', + end: '2017-07-04', + }, + }) + expectActiveRange('2015-06-07', '2015-06-14') + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/columnHeader.ts b/fullcalendar-main/tests/src/view-render/columnHeader.ts new file mode 100644 index 0000000..f7d01ac --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/columnHeader.ts @@ -0,0 +1,40 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dayHeaders', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-05-11', + }) + + describeOptions('initialView', { + 'when month view': 'dayGridMonth', + 'when timeGrid view': 'timeGridDay', + 'when dayGrid view': 'dayGridDay', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + describe('when on', () => { + pushOptions({ + dayHeaders: true, + }) + + it('should show header', () => { + let calendar = initCalendar() + let viewWrapper = new ViewWrapper(calendar) + expect(viewWrapper.header).toBeTruthy() + }) + }) + + describe('when off', () => { + pushOptions({ + dayHeaders: false, + }) + + it('should not show header', () => { + let calendar = initCalendar() + let viewWrapper = new ViewWrapper(calendar) + expect(viewWrapper.header).toBeFalsy() + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/columnHeaderHtml.ts b/fullcalendar-main/tests/src/view-render/columnHeaderHtml.ts new file mode 100644 index 0000000..1f6d2b9 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/columnHeaderHtml.ts @@ -0,0 +1,45 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dayHeaderContent as html', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-05-11', + }) + + describeOptions('initialView', { + 'when month view': 'dayGridMonth', + 'when timeGrid view': 'timeGridDay', + 'when dayGrid view': 'dayGridDay', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('should contain custom HTML', () => { + let calendar = initCalendar({ + dayHeaderContent(arg) { + return { html: '<div class="test">' + currentCalendar.formatDate(arg.date, { weekday: 'long' }) + '</div>' } + }, + }) + let headerWrapper = new ViewWrapper(calendar).header + + let $firstCellEl = $(headerWrapper.getCellEls()[0]) + expect($firstCellEl.find('.test').length).toBe(1) + expect($firstCellEl.text()).toBe('Sunday') + }) + }) + + describeTimeZones((tz) => { + it('receives correct date', () => { + let dates = [] + + initCalendar({ + initialView: 'timeGridDay', + dayHeaderContent(arg) { + dates.push(arg.date) + }, + }) + + expect(dates.length).toBe(1) + expect(dates[0]).toEqualDate(tz.parseDate('2014-05-11')) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/columnHeaderText.ts b/fullcalendar-main/tests/src/view-render/columnHeaderText.ts new file mode 100644 index 0000000..e6456ec --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/columnHeaderText.ts @@ -0,0 +1,43 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('dayHeaderContent as text', () => { // TODO: rename file + pushOptions({ + initialDate: '2014-05-11', + }) + + describeOptions('initialView', { + 'when month view': 'dayGridMonth', + 'when timeGrid view': 'timeGridDay', + 'when dayGrid view': 'dayGridDay', + }, (viewName) => { + let ViewWrapper = viewName.match(/^dayGrid/) ? DayGridViewWrapper : TimeGridViewWrapper + + it('should contain custom HTML escaped text', () => { + let calendar = initCalendar({ + dayHeaderContent(arg) { + return '<div>Custom ' + currentCalendar.formatDate(arg.date, { weekday: 'long' }) + '</div>' + }, + }) + let headerWrapper = new ViewWrapper(calendar).header + let $firstCell = $(headerWrapper.getCellEls()[0]) + expect($firstCell.text()).toBe('<div>Custom Sunday</div>') + }) + }) + + describeTimeZones((tz) => { + it('receives correct date', () => { + let dates = [] + + initCalendar({ + initialView: 'timeGridDay', + dayHeaderContent(arg) { + dates.push(arg.date) + }, + }) + + expect(dates.length).toBe(1) + expect(dates[0]).toEqualDate(tz.parseDate('2014-05-11')) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/daygrid-dirty-hit.ts b/fullcalendar-main/tests/src/view-render/daygrid-dirty-hit.ts new file mode 100644 index 0000000..bcff590 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/daygrid-dirty-hit.ts @@ -0,0 +1,26 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('daygrid view with updated dimensions', () => { + it('reports correct dateClick after resize', (done) => { + let $wrapper = $( + '<div><div style="width:auto"></div></div>', // reset width b/c test css hardcodes it + ).appendTo('body') + $wrapper.width(200) + + let calendar = initCalendar({ + initialDate: '2019-04-01', + initialView: 'dayGridMonth', + dateClick(arg) { + expect(arg.date).toEqualDate('2019-04-02') // a Tues + $wrapper.remove() + done() + }, + }, $wrapper.children().get(0)) + + $wrapper.width(400) + calendar.updateSize() + + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + $(dayGridWrapper.getDayEl('2019-04-02')).simulate('drag') // a click + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/daygrid-multi.ts b/fullcalendar-main/tests/src/view-render/daygrid-multi.ts new file mode 100644 index 0000000..28944d0 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/daygrid-multi.ts @@ -0,0 +1,103 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' +import '../lib/dom-misc.js' + +describe('DayGrid w/ multiple weeks/days', () => { + it('dayGridYear has correct start/end dates', () => { + const calendar = initCalendar({ + initialDate: '2023-06-25', + initialView: 'dayGridYear', + }) + + expect(calendar.view.currentStart).toEqualDate('2023-01-01') + expect(calendar.view.currentEnd).toEqualDate('2024-01-01') + }) + + it('renders scrollbars when 7 weeks', () => { + const calendar = initCalendar({ + initialDate: '2023-01-25', + initialView: 'dayGrid', + duration: { weeks: 7 }, + }) + + const dayGridView = new DayGridViewWrapper(calendar) + expect(dayGridView.getScrollerEl()).toHaveScrollbars() + }) + + it('does NOT render scrollbars when 6 weeks', () => { + const calendar = initCalendar({ + initialDate: '2023-01-25', + initialView: 'dayGrid', + duration: { weeks: 6 }, + }) + + const dayGridView = new DayGridViewWrapper(calendar) + expect(dayGridView.getScrollerEl()).not.toHaveScrollbars() + }) + + it('displays month-start text at two points when month break', () => { + const calendar = initCalendar({ + initialDate: '2023-01-30', + initialView: 'dayGrid', + duration: { weeks: 2 }, + }) + + const dayGrid = new DayGridViewWrapper(calendar).dayGrid + const monthStartEls = dayGrid.getMonthStartEls() + + expect(monthStartEls.length).toBe(2) + expect(monthStartEls[0].innerText).toBe('January 29') + expect(monthStartEls[1].innerText).toBe('February 1') + }) + + it('scrolls to initialDate', () => { + const calendar = initCalendar({ + initialDate: '2023-06-25', + initialView: 'dayGridYear', + }) + + const viewWrapper = new DayGridViewWrapper(calendar) + const scrollerEl = viewWrapper.getScrollerEl() + const dayGridWrapper = viewWrapper.dayGrid + const initialDayEl = dayGridWrapper.getDayEl('2023-06-25') + + expect( + Math.abs( + initialDayEl.getBoundingClientRect().top - + scrollerEl.getBoundingClientRect().top, + ) < 1, + ) + }) + + it('has customizable monthStartFormat', () => { + const calendar = initCalendar({ + initialDate: '2023-01-30', + initialView: 'dayGrid', + duration: { weeks: 2 }, + monthStartFormat: { year: 'numeric', month: 'short', day: '2-digit' }, + }) + + const dayGrid = new DayGridViewWrapper(calendar).dayGrid + const monthStartEls = dayGrid.getMonthStartEls() + + expect(monthStartEls.length).toBe(2) + expect(monthStartEls[0].innerText).toBe('Jan 29, 2023') + expect(monthStartEls[1].innerText).toBe('Feb 01, 2023') + }) + + // https://github.com/fullcalendar/fullcalendar/issues/7197 + it('has month-titles for each month in custom 6-month calendar', () => { + const calendar = initCalendar({ + initialDate: '2023-01-30', + initialView: 'dayGrid', + duration: { months: 6 }, + monthStartFormat: { year: 'numeric', month: 'short', day: '2-digit' }, + }) + + const dayGrid = new DayGridViewWrapper(calendar).dayGrid + const monthStartEls = dayGrid.getMonthStartEls() + + expect(monthStartEls.length).toBe(6) + expect(monthStartEls[0].innerText).toBe('Jan 01, 2023') + expect(monthStartEls[1].innerText).toBe('Feb 01, 2023') + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/height.ts b/fullcalendar-main/tests/src/view-render/height.ts new file mode 100644 index 0000000..4c0e937 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/height.ts @@ -0,0 +1,20 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('view height', () => { + // https://github.com/fullcalendar/fullcalendar/issues/6034 + xit('does not squish view-specific height:auto in timegrid view', () => { + let calendar = initCalendar({ + initialView: 'timeGridWeek', + aspectRatio: 1.8, + views: { + timeGrid: { + height: 'auto', + }, + }, + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + let scrollerEl = viewWrapper.getScrollerEl() + + expect(scrollerEl.getBoundingClientRect().height).toBeGreaterThan(10) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/multimonth.ts b/fullcalendar-main/tests/src/view-render/multimonth.ts new file mode 100644 index 0000000..0d2f9ca --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/multimonth.ts @@ -0,0 +1,129 @@ +import { MultiMonthViewWrapper } from '../lib/wrappers/MultiMonthViewWrapper.js' +import '../lib/dom-geom.js' + +describe('multimonth view', () => { + it('computes start/end for multiMonthYear', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + }) + + expect(calendar.view.currentStart).toEqualDate('2023-01-01') + expect(calendar.view.currentEnd).toEqualDate('2024-01-01') + }) + + it('can have custom duration', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonth', + duration: { months: 2 }, + }) + + expect(calendar.view.currentStart).toEqualDate('2023-06-01') + expect(calendar.view.currentEnd).toEqualDate('2023-08-01') + }) + + it('having small multiMonthMinWidth results in side-by-side months', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthMinWidth: 100, + }) + + const monthWrappers = new MultiMonthViewWrapper(calendar).getMonths() + expect(monthWrappers.length).toBe(12) + expect(monthWrappers[0].el).toBeLeftOf(monthWrappers[1].el) + }) + + it('having large multiMonthMinWidth results in stacking months', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthMinWidth: 600, + }) + + const monthWrappers = new MultiMonthViewWrapper(calendar).getMonths() + expect(monthWrappers.length).toBe(12) + expect(monthWrappers[0].el).not.toBeLeftOf(monthWrappers[1].el) + expect(monthWrappers[0].el).toBeAbove(monthWrappers[1].el) + }) + + it('can have forced single column with multiMonthMaxColumns', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthMaxColumns: 1, + }) + + const monthWrappers = new MultiMonthViewWrapper(calendar).getMonths() + expect(monthWrappers[0].el).not.toBeLeftOf(monthWrappers[1].el) + expect(monthWrappers[0].el).toBeAbove(monthWrappers[1].el) + }) + + it('is scrolled to current date initially', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthMaxColumns: 1, + }) + + const viewWrapper = new MultiMonthViewWrapper(calendar) + const monthWrappers = viewWrapper.getMonths() + const scrollerEl = viewWrapper.getScrollerEl() + + expect( + Math.abs( + scrollerEl.getBoundingClientRect().top - + monthWrappers[5].el.getBoundingClientRect().top, + ), + ).toBeLessThan(2) + + expect(scrollerEl.scrollTop).not.toBe(0) + calendar.next() + calendar.prev() + expect(scrollerEl.scrollTop).toBe(0) + }) + + it('renders events when weekends: false', () => { + const calendar = initCalendar({ + initialDate: '2023-01-25', + initialView: 'multiMonthYear', + weekends: false, + events: [ + { + title: 'Conference', + start: '2023-01-11', + end: '2023-01-13', + }, + ], + }) + + const viewWrapper = new MultiMonthViewWrapper(calendar) + const monthWrappers = viewWrapper.getMonths() + expect(monthWrappers.length).toBe(12) + expect(monthWrappers[0].columnCnt).toBe(5) + expect(viewWrapper.getEventEls().length).toBe(1) + }) + + it('has customizable multiMonthTitleFormat', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthTitleFormat: { month: 'short', year: 'numeric' }, + }) + + const monthWrappers = new MultiMonthViewWrapper(calendar).getMonths() + expect(monthWrappers[0].title).toBe('Jan 2023') + }) + + it('does not accidentally render month-start within cells', () => { + const calendar = initCalendar({ + initialDate: '2023-06-01', + initialView: 'multiMonthYear', + multiMonthTitleFormat: { month: 'short', year: 'numeric' }, + }) + + const viewWrapper = new MultiMonthViewWrapper(calendar) + expect(viewWrapper.el.querySelectorAll('.fc-daygrid-month-start').length).toBe(0) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/rerender.ts b/fullcalendar-main/tests/src/view-render/rerender.ts new file mode 100644 index 0000000..63bb622 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/rerender.ts @@ -0,0 +1,23 @@ +import { DayGridViewWrapper } from '../lib/wrappers/DayGridViewWrapper.js' + +describe('rerendering a calendar', () => { + it('keeps sizing', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + initialDate: '2019-08-08', + dayMaxEventRows: 3, + events: [ + { date: '2019-08-08', title: 'event' }, + { date: '2019-08-08', title: 'event' }, + { date: '2019-08-08', title: 'event' }, + { date: '2019-08-08', title: 'event' }, + ], + }) + let dayGridWrapper = new DayGridViewWrapper(calendar).dayGrid + + expect(dayGridWrapper.getMoreEls().length).toBe(1) + + calendar.render() + expect(dayGridWrapper.getMoreEls().length).toBe(1) // good way to test that sizing is maintained + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/scrollToTime.ts b/fullcalendar-main/tests/src/view-render/scrollToTime.ts new file mode 100644 index 0000000..e6fc63e --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/scrollToTime.ts @@ -0,0 +1,23 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('scrollToTime method', () => { + it('accepts a object duration input', () => { + let calendar = initCalendar({ + scrollTime: 0, + initialView: 'timeGridWeek', + }) + let viewWrapper = new TimeGridViewWrapper(calendar) + + calendar.scrollToTime({ hours: 2 }) + + // NOTE: c&p'd from scrollTime tests + let slotTop = viewWrapper.timeGrid.getTimeTop('02:00:00') - viewWrapper.timeGrid.el.getBoundingClientRect().top + let scrollEl = viewWrapper.getScrollerEl() + let scrollTop = scrollEl.scrollTop + let diff = Math.abs(slotTop - scrollTop) + + expect(slotTop).toBeGreaterThan(0) + expect(scrollTop).toBeGreaterThan(0) + expect(diff).toBeLessThan(3) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/showNonCurrentDates.ts b/fullcalendar-main/tests/src/view-render/showNonCurrentDates.ts new file mode 100644 index 0000000..beaf72a --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/showNonCurrentDates.ts @@ -0,0 +1,51 @@ +import { expectDayRange } from '../lib/ViewRenderUtils.js' + +describe('showNonCurrentDates', () => { + pushOptions({ + showNonCurrentDates: false, + }) + + describe('when in month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + }) + + it('does not render other months\' dates', () => { + initCalendar() + expectDayRange('2017-06-01', '2017-07-01') + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-06-01', + }) + + it('has no effect', () => { + initCalendar() + expectDayRange('2017-05-28', '2017-06-04') + }) + }) + + it('works when disabling weekends and switching views', () => { + initCalendar({ + weekends: false, + initialView: 'dayGridMonth', + initialDate: '2019-06-07', // only shows problem when start date is a weekend! + }) + currentCalendar.next() + currentCalendar.setOption('weekends', true) + // no errors thrown, yay + }) + + it('works when switching views with same formal duration but different rendered duration', () => { + initCalendar({ + initialView: 'listMonth', // something other than than dayGridMonth + initialDate: '2019-01-01', + }) + currentCalendar.changeView('dayGridMonth') + expectDayRange('2019-01-01', '2019-02-01') + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/slotDuration.ts b/fullcalendar-main/tests/src/view-render/slotDuration.ts new file mode 100644 index 0000000..1c8ab7e --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/slotDuration.ts @@ -0,0 +1,99 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('slotDuration', () => { + pushOptions({ + initialDate: '2017-07-17', + initialView: 'timeGridDay', + scrollTime: 0, + locale: 'en-GB', // for 00:00 instead of 24:00 + slotLabelFormat: { hour: '2-digit', minute: '2-digit', hour12: false }, + }) + + describe('when only major slots', () => { + pushOptions({ + slotDuration: '01:00', + slotLabelInterval: '01:00', + }) + + describe('when in alignment with slotMinTime', () => { + pushOptions({ + slotMinTime: '00:00', + slotMaxTime: '03:00', + }) + it('render slots correctly', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getTimeAxisInfo()).toEqual([ + { text: '00:00', isMajor: true }, + { text: '01:00', isMajor: true }, + { text: '02:00', isMajor: true }, + ]) + }) + }) + + describe('when out of alignment with slotMinTime', () => { + pushOptions({ + slotMinTime: '00:20', + slotMaxTime: '03:20', + }) + it('render slots correctly', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getTimeAxisInfo()).toEqual([ + { text: '00:20', isMajor: true }, + { text: '01:20', isMajor: true }, + { text: '02:20', isMajor: true }, + ]) + }) + }) + }) + + describe('when major and minor slots', () => { + pushOptions({ + slotDuration: '00:30', + slotLabelInterval: '01:00', + }) + + describe('when in alignment with slotMinTime', () => { + pushOptions({ + slotMinTime: '00:00', + slotMaxTime: '03:00', + }) + it('render slots correctly', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getTimeAxisInfo()).toEqual([ + { text: '00:00', isMajor: true }, + { text: '', isMajor: false }, + { text: '01:00', isMajor: true }, + { text: '', isMajor: false }, + { text: '02:00', isMajor: true }, + { text: '', isMajor: false }, + ]) + }) + }) + + describe('when out of alignment with slotMinTime', () => { + pushOptions({ + slotMinTime: '00:20', + slotMaxTime: '03:20', + }) + it('render slots correctly', () => { + let calendar = initCalendar() + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + + expect(timeGridWrapper.getTimeAxisInfo()).toEqual([ + { text: '00:20', isMajor: true }, + { text: '', isMajor: false }, + { text: '01:20', isMajor: true }, + { text: '', isMajor: false }, + { text: '02:20', isMajor: true }, + { text: '', isMajor: false }, + ]) + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/timegrid-allday-slot.ts b/fullcalendar-main/tests/src/view-render/timegrid-allday-slot.ts new file mode 100644 index 0000000..9c51655 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/timegrid-allday-slot.ts @@ -0,0 +1,34 @@ +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' + +describe('timegrid all-day slot', () => { + pushOptions({ + initialDate: '2019-04-23', + initialView: 'timeGridWeek', + editable: true, + }) + + // https://github.com/fullcalendar/fullcalendar/issues/4616 + it('allows dragging after dynamic event adding', (done) => { + let calendar = initCalendar({ + eventDrop(arg) { + expect(arg.event.start).toEqualDate('2019-04-24') + done() + }, + }) + + calendar.batchRendering(() => { + calendar.addEvent({ start: '2019-04-23' }) + calendar.addEvent({ start: '2019-04-23' }) + calendar.addEvent({ start: '2019-04-23' }) + }) + + let dayGridWrapper = new TimeGridViewWrapper(calendar).dayGrid + let dayWidth = $(dayGridWrapper.getDayEls('2019-04-23')).width() + let lastEventEl = dayGridWrapper.getEventEls()[2] + + $(lastEventEl).simulate('drag', { + localPoint: { left: '50%', top: '99%' }, + dx: dayWidth, + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/timegrid-slots.ts b/fullcalendar-main/tests/src/view-render/timegrid-slots.ts new file mode 100644 index 0000000..aeaba98 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/timegrid-slots.ts @@ -0,0 +1,9 @@ +describe('timegrid slots', () => { + // https://github.com/fullcalendar/fullcalendar/issues/5952 + it('can render a single big slot without error', () => { + initCalendar({ + initialView: 'timeGridDay', + slotDuration: '24:00', + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/updateSize.ts b/fullcalendar-main/tests/src/view-render/updateSize.ts new file mode 100644 index 0000000..91f6c57 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/updateSize.ts @@ -0,0 +1,18 @@ +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('updateSize method', () => { + it('updates size of a previously hidden element', () => { + let $el = $('<div style="display:none" />').appendTo('body') + let calendar = initCalendar({ + initialView: 'dayGridMonth', + contentHeight: 600, + }, $el) + let calendarWrapper = new CalendarWrapper(calendar) + + $el.show() + calendar.updateSize() + expect(calendarWrapper.getViewContainerEl().offsetHeight).toBeCloseTo(600, 0) + + $el.remove() + }) +}) diff --git a/fullcalendar-main/tests/src/view-render/validRange.ts b/fullcalendar-main/tests/src/view-render/validRange.ts new file mode 100644 index 0000000..9a421f9 --- /dev/null +++ b/fullcalendar-main/tests/src/view-render/validRange.ts @@ -0,0 +1,59 @@ +import { expectDayRange } from '../lib/ViewRenderUtils.js' + +describe('validRange rendering', () => { + describe('with hardcoded start constraint', () => { + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { start: '2017-06-07' }, + }) + + it('does not render days before', () => { + initCalendar() + expectDayRange('2017-06-07', '2017-07-09') + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + validRange: { start: '2017-06-06' }, + }) + + it('does not render days before', () => { + initCalendar() + expectDayRange('2017-06-06', '2017-06-11') + }) + }) + }) + + describe('with hardcoded end constraint', () => { + describe('when month view', () => { + pushOptions({ + initialView: 'dayGridMonth', + initialDate: '2017-06-01', + validRange: { end: '2017-06-07' }, + }) + + it('does not render days on or after', () => { + initCalendar() + expectDayRange('2017-05-28', '2017-06-07') + }) + }) + + describe('when in week view', () => { + pushOptions({ + initialView: 'timeGridWeek', + initialDate: '2017-06-08', + validRange: { end: '2017-06-06' }, + }) + + it('does not render days on or after', () => { + initCalendar() + expectDayRange('2017-06-04', '2017-06-06') + }) + }) + }) +}) diff --git a/fullcalendar-main/tests/src/view-type/changeView.ts b/fullcalendar-main/tests/src/view-type/changeView.ts new file mode 100644 index 0000000..023dca1 --- /dev/null +++ b/fullcalendar-main/tests/src/view-type/changeView.ts @@ -0,0 +1,99 @@ +import { expectActiveRange } from '../lib/ViewDateUtils.js' +import { TimeGridViewWrapper } from '../lib/wrappers/TimeGridViewWrapper.js' +import { CalendarWrapper } from '../lib/wrappers/CalendarWrapper.js' + +describe('changeView', () => { + pushOptions({ + initialDate: '2017-06-08', + initialView: 'dayGridMonth', + }) + + it('can change views', () => { + let calendar = initCalendar() + calendar.changeView('timeGridWeek') + expectActiveRange('2017-06-04', '2017-06-11') + }) + + it('can change views and navigate date', () => { + let calendar = initCalendar() + calendar.changeView('timeGridDay', '2017-06-26') + expectActiveRange('2017-06-26', '2017-06-27') + }) + + it('can change views and change activeRange', () => { + let calendar = initCalendar() + calendar.changeView('timeGrid', { + start: '2017-07-04', + end: '2017-07-08', + }) + expectActiveRange('2017-07-04', '2017-07-08') + }) + + describe('when switching away from view, then back', () => { + // serves as a smoke test too + it('correctly renders original view again', () => { + let calendar = initCalendar({ + initialView: 'dayGridMonth', + }) + + expect(calendar.view.type).toBe('dayGridMonth') + checkViewIntegrity(calendar) + calendar.changeView('timeGridWeek') + + expect(calendar.view.type).toBe('timeGridWeek') + checkViewIntegrity(calendar) + + let timeGridWrapper = new TimeGridViewWrapper(calendar).timeGrid + expect(timeGridWrapper.isStructureValid()).toBe(true) + + calendar.changeView('dayGridWeek') + + expect(calendar.view.type).toBe('dayGridWeek') + checkViewIntegrity(calendar) + calendar.changeView('listWeek') + + expect(calendar.view.type).toBe('listWeek') + checkViewIntegrity(calendar) + calendar.changeView('dayGridMonth') + + expect(calendar.view.type).toBe('dayGridMonth') + checkViewIntegrity(calendar) + }) + }) + + // https://github.com/fullcalendar/fullcalendar/issues/3689 + it('can when switching to/from view while loading events', (done) => { + let calendar = initCalendar({ + headerToolbar: { + left: 'title dayGridDay timeGridDay', + }, + initialView: 'timeGridDay', + now: '2017-06-08T01:00:00', + events(fetchInfo, successCallback) { + setTimeout(() => { + successCallback([ // will run after the first view switch but before the second + { start: '2017-06-08T01:00:00' }, // needs to be timed to cause the JS error + ]) + }, 100) + }, + }) + + calendar.changeView('dayGridDay') + checkViewIntegrity(calendar) + expect(calendar.view.type).toBe('dayGridDay') + + setTimeout(() => { + calendar.changeView('timeGridDay') + checkViewIntegrity(calendar) + expect(calendar.view.type).toBe('timeGridDay') + done() + }, 200) + }) + + function checkViewIntegrity(calendar) { + let $el = $(new CalendarWrapper(calendar).getViewEl()) + expect($el).toBeInDOM() + expect($el.children().length).toBeGreaterThan(0) + expect($el.text()).toBeTruthy() + } +}) diff --git a/fullcalendar-main/tests/src/view-type/exposed-classes.ts b/fullcalendar-main/tests/src/view-type/exposed-classes.ts new file mode 100644 index 0000000..6983e82 --- /dev/null +++ b/fullcalendar-main/tests/src/view-type/exposed-classes.ts @@ -0,0 +1,14 @@ +import { DayGridView, DayTable } from '@fullcalendar/daygrid/internal' +import { ListView } from '@fullcalendar/list/internal' +import { DayTimeColsView, DayTimeCols } from '@fullcalendar/timegrid/internal' + +describe('internal View/Grid classes', () => { + it('are exposed', () => { + expect(typeof DayTimeColsView).toBe('function') + expect(typeof DayGridView).toBe('function') + expect(typeof ListView).toBe('function') + + expect(typeof DayTable).toBe('function') + expect(typeof DayTimeCols).toBe('function') + }) +}) diff --git a/fullcalendar-main/turbo.json b/fullcalendar-main/turbo.json new file mode 100644 index 0000000..a21d72d --- /dev/null +++ b/fullcalendar-main/turbo.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "pipeline": { + "lint": { + "inputs": [ + "./*.{ts,js,cjs}", + "./{src,scripts,config,tests}/**" + ] + }, + "build": { + "dependsOn": ["^build"], + "inputs": [ + "./*.{ts,js,cjs,json}", + "./{src,scripts,config}/**" + ], + "outputs": [ + "./dist/**", + "!./dist/.tsout", + "!./dist/tsconfig.tsbuildinfo", + "./tests/dist/**" + ] + }, + "test": { + "dependsOn": ["build"] + }, + "clean": { + "cache": false + } + } +} diff --git a/hleda-se-fotograf-ka/index.html b/hleda-se-fotograf-ka/index.html index 657c0ce..6f4a53c 100644 --- a/hleda-se-fotograf-ka/index.html +++ b/hleda-se-fotograf-ka/index.html @@ -4,7 +4,7 @@ "><meta property="og:url" content="https://jsem.nudista.online/hleda-se-fotograf-ka/"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://jsem.nudista.online/hleda-se-fotograf-ka/"},"headline":"Hledá se fotograf(ka)","datePublished":"2024-02-23T17:27","dateModified":"2024-03-02T13:10","image":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg","height":1208,"width":1920},"description":"Hledám fotografa/fotografku na léto 2024!\n\n","author":{"@type":"Person","name":"SRN 🇩🇪","url":"https://jsem.nudista.online/autor/jan-rippl-de/"},"publisher":{"@type":"Organization","name":"CZ 🇨🇿","logo":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80}}}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -48,7 +48,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><figure class="post__featured-image post__featured-image--attop"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-md.jpg 768w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xl.jpg 1024w" sizes="(min-width: 1460px) 938px, (min-width: 1200px) calc(75.83vw - 154px), (min-width: 1120px) 938px, (min-width: 900px) calc(55vw + 333px), 100vw" loading="eager" height="1208" width="1920" alt="" aria-describedby="image-caption"></figure><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/inzerce/" class="metadata__maintag">🔔 Inzerce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-de/" class="post__author__name">SRN 🇩🇪</a></div></div><div class="post__date"><time datetime="2024-02-23T17:27">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Hledá se fotograf(ka)</h1><p class="post__lead">Osnova mého harmonogramu práce</p></header><div class="post__entry"></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T13:10">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fhleda-se-fotograf-ka%2F&via=NoLogWeb&text=Hled%C3%A1%20se%20fotograf(ka)" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article><div class="post__section post__related"><div class="main__inner"><h3 class="post__section__title">Související příspěvky:</h3><div class="post__related__wrap"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></div></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><figure class="post__featured-image post__featured-image--attop"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-md.jpg 768w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xl.jpg 1024w" sizes="(min-width: 1460px) 938px, (min-width: 1200px) calc(75.83vw - 154px), (min-width: 1120px) 938px, (min-width: 900px) calc(55vw + 333px), 100vw" loading="eager" height="1208" width="1920" alt="" aria-describedby="image-caption"></figure><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/inzerce/" class="metadata__maintag">🔔 Inzerce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-de/" class="post__author__name">SRN 🇩🇪</a></div></div><div class="post__date"><time datetime="2024-02-23T17:27">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Hledá se fotograf(ka)</h1><p class="post__lead">Osnova mého harmonogramu práce</p></header><div class="post__entry"></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T13:10">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fhleda-se-fotograf-ka%2F&via=NoLogWeb&text=Hled%C3%A1%20se%20fotograf(ka)" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article><div class="post__section post__related"><div class="main__inner"><h3 class="post__section__title">Související příspěvky:</h3><div class="post__related__wrap"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></div></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/hleda-se-kameraman-ka-2024/index.html b/hleda-se-kameraman-ka-2024/index.html index 37ad183..15637dc 100644 --- a/hleda-se-kameraman-ka-2024/index.html +++ b/hleda-se-kameraman-ka-2024/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hledá se kameraman(ka) - NoLogWeb</title><meta name="description" content="Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka."><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="canonical" href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Hledá se kameraman(ka)"><meta property="og:image" content="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg"><meta property="og:image:width" content="1920"><meta property="og:image:height" content="1255"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content="Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka."><meta property="og:url" content="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://jsem.nudista.online/hleda-se-kameraman-ka-2024/"},"headline":"Hledá se kameraman(ka)","datePublished":"2024-02-23T13:56","dateModified":"2024-03-02T13:10","image":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg","height":1255,"width":1920},"description":"Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.","author":{"@type":"Person","name":"SRN 🇩🇪","url":"https://jsem.nudista.online/autor/jan-rippl-de/"},"publisher":{"@type":"Organization","name":"CZ 🇨🇿","logo":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80}}}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><figure class="post__featured-image post__featured-image--attop"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-md.jpg 768w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xl.jpg 1024w" sizes="(min-width: 1460px) 938px, (min-width: 1200px) calc(75.83vw - 154px), (min-width: 1120px) 938px, (min-width: 900px) calc(55vw + 333px), 100vw" loading="eager" height="1255" width="1920" alt="" aria-describedby="image-caption"></figure><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/inzerce/" class="metadata__maintag">🔔 Inzerce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-de/" class="post__author__name">SRN 🇩🇪</a></div></div><div class="post__date"><time datetime="2024-02-23T13:56">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Hledá se kameraman(ka)</h1><p class="post__lead">Hledám kameramana/kameramanku na léto 2024!</p></header><div class="post__entry"><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p><p class="msg msg--highlight"><svg class="fi fi-alert-octagon" width="24px" height="24px" fill="none" stroke="#DD3333" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></g></svg> <span style="color: #dd3333;">Pro tento druh spolupráce je nutné mít dosaženo věkové hranice alespoň osmnácti let (18+), a být způsobilým k právním úkonům.</span></p><h1><i class="bi bi-1-square-fill"></i> Forma spolupráce</h1><fieldset><legend><svg class="fi fi-info" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></g></svg> Informace:</legend><p>Možné formy spolupráce:</p><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - TFP/TFCD</small></li><li><small><svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg> - Barter</small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Honorovaná</small></li></ul><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><small><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - <span style="text-decoration: line-through;">PS</span>, <span style="text-decoration: line-through;">DPP</span>, <span style="text-decoration: line-through;">DPČ</span></small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Smlouva o dílo</small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - tzv. Model Release</small></li></ul><div style="clear: both;"> </div><fieldset><legend><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nejedná se o pracovně-právní vztah...</legend>Z hlediska platné legislativy jsem fyzickou nepodnikající osobou, a tudíž by bylo velice složité (a nákladné) uzavírat s Vámi pracovně-právní vztah jakožto "zaměstnavatel", ať již formou pracovní smlouvy, dohodou o provedení práce, či formou dohody o pracovní činnosti.</fieldset><fieldset><legend><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Jedná se o vztah dodavatele s odběratelem...</legend>Ve své podstatě se jedná o zakázku, konkrétně tedy o zhotovení díla (video), jenž lze ošetřit dle občanského zákoníku "<a href="https://www.podnikatel.cz/smlouvy/smlouva-o-dilo/" target="_blank" rel="noopener noreferrer">smlouvou o dílo</a>".</fieldset></fieldset><h1><i class="bi bi-2-square-fill"></i> Účel spolupráce</h1><fieldset><legend><svg class="fi fi-film" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></g></svg> nudista.online</legend>Cílem (účelem) budoucí spolupráce, je pořízení audio-vizuálního záznamu v digitální podobě, prostřednictvím digitální videokamery, jenž je v mém vlastnictví, a jenž Vám bude pro tento účel zapůjčena v průběhu této spolupráce, a to pro účely vzniku dokumentárního filmu (zpracování videa dokumentární formou) o nudistických aktivitách ČR/SRN.<p class="msg msg--info"><span style="color: #02192b;"><i class="bi bi-badge-cc"></i> <strong>text</strong></span></p></fieldset><fieldset><legend>On-Line redakce (<i class="bi bi-wordpress"></i> Wordpress.com)</legend>Pro možnost on-line spolupráce při psaní tématicky zaměřených článků pro můj magazín nudist@nline, jsem se rozhodl zřídit si na službě wordpress.com placený blog na subdoméně redaktor.nudista.online, jenž mi poslouží pro vytváření konceptů článků nejen o nudismu a lidské nahotě, a to za pomoci jak umělé inteligence (Gemini), jakož i skutečných redaktorů a editorů.</fieldset></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T13:10">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fhleda-se-kameraman-ka-2024%2F&via=NoLogWeb&text=Hled%C3%A1%20se%20kameraman(ka)" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article><div class="post__section post__related"><div class="main__inner"><h3 class="post__section__title">Související příspěvky:</h3><div class="post__related__wrap"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article></div></div></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><figure class="post__featured-image post__featured-image--attop"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-md.jpg 768w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xl.jpg 1024w" sizes="(min-width: 1460px) 938px, (min-width: 1200px) calc(75.83vw - 154px), (min-width: 1120px) 938px, (min-width: 900px) calc(55vw + 333px), 100vw" loading="eager" height="1255" width="1920" alt="" aria-describedby="image-caption"></figure><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/inzerce/" class="metadata__maintag">🔔 Inzerce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-de/" class="post__author__name">SRN 🇩🇪</a></div></div><div class="post__date"><time datetime="2024-02-23T13:56">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Hledá se kameraman(ka)</h1><p class="post__lead">Hledám kameramana/kameramanku na léto 2024!</p></header><div class="post__entry"><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p><p class="msg msg--highlight"><svg class="fi fi-alert-octagon" width="24px" height="24px" fill="none" stroke="#DD3333" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></g></svg> <span style="color: #dd3333;">Pro tento druh spolupráce je nutné mít dosaženo věkové hranice alespoň osmnácti let (18+), a být způsobilým k právním úkonům.</span></p><h1><i class="bi bi-1-square-fill"></i> Forma spolupráce</h1><fieldset><legend><svg class="fi fi-info" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></g></svg> Informace:</legend><p>Možné formy spolupráce:</p><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - TFP/TFCD</small></li><li><small><svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg> - Barter</small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Honorovaná</small></li></ul><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><small><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - <span style="text-decoration: line-through;">PS</span>, <span style="text-decoration: line-through;">DPP</span>, <span style="text-decoration: line-through;">DPČ</span></small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Smlouva o dílo</small></li><li><small><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - tzv. Model Release</small></li></ul><div style="clear: both;"> </div><fieldset><legend><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nejedná se o pracovně-právní vztah...</legend>Z hlediska platné legislativy jsem fyzickou nepodnikající osobou, a tudíž by bylo velice složité (a nákladné) uzavírat s Vámi pracovně-právní vztah jakožto "zaměstnavatel", ať již formou pracovní smlouvy, dohodou o provedení práce, či formou dohody o pracovní činnosti.</fieldset><fieldset><legend><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Jedná se o vztah dodavatele s odběratelem...</legend>Ve své podstatě se jedná o zakázku, konkrétně tedy o zhotovení díla (video), jenž lze ošetřit dle občanského zákoníku "<a href="https://www.podnikatel.cz/smlouvy/smlouva-o-dilo/" target="_blank" rel="noopener noreferrer">smlouvou o dílo</a>".</fieldset></fieldset><h1><i class="bi bi-2-square-fill"></i> Účel spolupráce</h1><fieldset><legend><svg class="fi fi-film" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></g></svg> nudista.online</legend>Cílem (účelem) budoucí spolupráce, je pořízení audio-vizuálního záznamu v digitální podobě, prostřednictvím digitální videokamery, jenž je v mém vlastnictví, a jenž Vám bude pro tento účel zapůjčena v průběhu této spolupráce, a to pro účely vzniku dokumentárního filmu (zpracování videa dokumentární formou) o nudistických aktivitách ČR/SRN.<p class="msg msg--info"><span style="color: #02192b;"><i class="bi bi-badge-cc"></i> <strong>text</strong></span></p></fieldset><fieldset><legend>On-Line redakce (<i class="bi bi-wordpress"></i> Wordpress.com)</legend>Pro možnost on-line spolupráce při psaní tématicky zaměřených článků pro můj magazín nudist@nline, jsem se rozhodl zřídit si na službě wordpress.com placený blog na subdoméně redaktor.nudista.online, jenž mi poslouží pro vytváření konceptů článků nejen o nudismu a lidské nahotě, a to za pomoci jak umělé inteligence (Gemini), jakož i skutečných redaktorů a editorů.</fieldset></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T13:10">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fhleda-se-kameraman-ka-2024%2F&via=NoLogWeb&text=Hled%C3%A1%20se%20kameraman(ka)" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article><div class="post__section post__related"><div class="main__inner"><h3 class="post__section__title">Související příspěvky:</h3><div class="post__related__wrap"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article></div></div></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/index.html b/index.html index 6461200..38e38e8 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Jsem · Nudista · Online</title><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="canonical" href="https://jsem.nudista.online/"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Jsem · Nudista · Online"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1><svg class="fi fi-bell" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></g></svg> Jsem · Nudista · Online</h1><p>Jsem Jan Rippl - nudista online! Vítejte! Tato část mého webu slouží jako moje pracovní nástěnka a zároveň jako má vlatní "sociální sít", jenž je umístěna na službě NoLog (Gitea) co by statické HTML stránky, spravované za pomoci publikačního systému Publii.</p><fieldset><legend><svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewbox="0 0 24 24"><path fill="currentColor" d="M16.279 11.506c.132-.016.257-.018.373 0c.066-.154.078-.419.019-.708c-.09-.429-.211-.688-.461-.646c-.251.04-.261.35-.17.779c.05.24.14.446.239.575m-2.149.339c.18.078.29.129.331.086c.029-.028.021-.084-.022-.154a1.05 1.05 0 0 0-.464-.371a1.26 1.26 0 0 0-1.228.146c-.119.088-.232.209-.218.283c.007.023.023.042.065.05c.099.011.444-.164.843-.188c.282-.02.513.068.693.148m-.361.205c-.232.037-.361.113-.443.187c-.071.062-.113.128-.113.177l.018.042l.037.014c.053 0 .171-.046.171-.046c.324-.115.539-.102.752-.078c.117.014.172.02.198-.02c.007-.012.018-.035-.007-.074c-.056-.091-.291-.24-.613-.202m1.784.756c.159.078.333.046.39-.069c.059-.115-.024-.272-.183-.349c-.158-.079-.333-.049-.39.066c-.057.115.026.274.183.352m1.018-.891c-.128-.002-.234.138-.238.316c-.003.177.1.321.229.322c.129.002.235-.139.238-.315s-.099-.32-.229-.323m-8.644 3.183c-.032-.04-.085-.029-.136-.015c-.036.007-.076.017-.119.016a.265.265 0 0 1-.221-.111c-.059-.09-.056-.225.01-.378l.03-.069c.104-.231.275-.619.082-.988a.88.88 0 0 0-.671-.488a.861.861 0 0 0-.739.267c-.284.313-.327.741-.273.893c.021.056.053.071.075.074c.048.007.119-.029.164-.15l.014-.038c.02-.064.057-.184.118-.278a.518.518 0 0 1 .717-.15c.2.131.275.375.19.608c-.044.121-.115.351-.1.54c.032.383.27.537.48.556c.206.007.35-.108.387-.193c.021-.053.003-.084-.008-.096"></path><path fill="currentColor" d="M19.821 14.397c-.009-.029-.061-.216-.13-.44l-.144-.384c.281-.423.286-.799.249-1.013a1.284 1.284 0 0 0-.372-.724c-.222-.232-.677-.472-1.315-.651l-.335-.093c-.002-.015-.018-.79-.031-1.123c-.011-.24-.031-.616-.148-.986c-.14-.502-.381-.938-.684-1.221c.835-.864 1.355-1.817 1.354-2.634c-.003-1.571-1.933-2.049-4.312-1.063l-.503.214c-.002-.002-.911-.894-.924-.905c-2.714-2.366-11.192 7.06-8.48 9.349l.593.501a2.916 2.916 0 0 0-.166 1.345c.065.631.389 1.234.915 1.701c.5.442 1.159.724 1.796.723c1.055 2.432 3.465 3.922 6.291 4.007c3.032.09 5.576-1.333 6.644-3.889c.069-.179.365-.987.365-1.7c-.001-.718-.406-1.015-.663-1.014M7.416 16.309a1.38 1.38 0 0 1-.28.021c-.916-.026-1.905-.85-2.003-1.827c-.109-1.08.443-1.912 1.421-2.108c.116-.025.258-.038.41-.031c.548.032 1.354.452 1.539 1.645c.164 1.055-.096 2.132-1.087 2.3m-1.021-4.562a2.325 2.325 0 0 0-1.473.94c-.197-.164-.562-.48-.626-.604c-.524-.994.571-2.928 1.337-4.02c1.889-2.698 4.851-4.739 6.223-4.371c.222.064.96.921.96.921s-1.37.759-2.642 1.819c-1.711 1.32-3.006 3.236-3.779 5.315m9.611 4.158a.05.05 0 0 0 .03-.054a.05.05 0 0 0-.056-.045s-1.434.212-2.789-.283c.147-.479.541-.308 1.134-.259a8.287 8.287 0 0 0 2.735-.296c.613-.177 1.419-.524 2.045-1.018c.212.465.286.975.286.975s.163-.029.3.055c.13.08.224.245.16.671c-.133.798-.471 1.445-1.042 2.041a4.259 4.259 0 0 1-1.249.934a5.337 5.337 0 0 1-.814.346c-2.149.701-4.349-.07-5.058-1.727a2.761 2.761 0 0 1-.142-.392c-.302-1.092-.046-2.4.755-3.226v-.001c.051-.052.102-.113.102-.191c0-.064-.042-.133-.077-.183c-.28-.406-1.253-1.099-1.057-2.44c.139-.964.982-1.642 1.768-1.602l.2.012c.34.02.637.063.917.076c.47.019.891-.049 1.391-.465c.169-.142.304-.263.532-.301c.024-.006.084-.025.203-.021a.681.681 0 0 1 .343.109c.4.266.457.912.479 1.385c.012.269.045.922.055 1.108c.026.428.139.489.365.563c.129.044.248.074.423.125c.529.147.845.3 1.043.493a.637.637 0 0 1 .188.372c.065.457-.353 1.021-1.455 1.533c-1.206.559-2.669.701-3.679.588l-.354-.04c-.81-.108-1.269.936-.784 1.651c.313.461 1.164.761 2.017.761c1.953.002 3.455-.832 4.015-1.554l.044-.063c.026-.042.005-.063-.03-.041c-.455.312-2.483 1.552-4.651 1.18c0 0-.264-.044-.504-.138c-.19-.072-.591-.258-.639-.668c1.747.543 2.85.031 2.85.03m-6.12-7.852c.672-.776 1.499-1.452 2.241-1.83c.025-.014.052.015.038.038a2.125 2.125 0 0 0-.208.508c-.006.027.023.049.046.032c.462-.314 1.264-.651 1.968-.693a.03.03 0 0 1 .021.055a1.66 1.66 0 0 0-.31.311c-.014.02-.001.049.024.049c.494.003 1.191.175 1.644.43c.03.018.008.077-.025.069c-.688-.157-1.811-.277-2.979.008c-1.044.254-1.84.646-2.419 1.069c-.03.02-.065-.019-.041-.046"></path></svg> Služba Mailchimp</legend><form id="form" class="validate" action="https://online.us2.list-manage.com/subscribe/post?u=2ac5382353059a705b15ea6ab&id=9189628091&f_id=00924ae0f0" method="post" target="_self"><label>E-mail pro zasílání novinek:</label><br><input id="mce-EMAIL" name="EMAIL" required="" size="40%" type="email" placeholder="Váš e-mail"><div style="position: absolute; left: -5000px;" aria-hidden="true"><input tabindex="-1" name="b_2ac5382353059a705b15ea6ab_9189628091" type="text"></div> <input id="mc-embedded-subscribe" name="subscribe" type="submit" value="Přihlásit"></form><p><br><a href="/ochrana-soukromi/">· Soukromí</a> <a href="#">· Kontakt</a> <a href="/rubriky/politiky/">· Politiky</a></p></fieldset><fieldset><legend><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Veřejné kalendáře (Google)</legend><small>Všechny mé kalendáře se dělí na veřejné / neveřejné a externí / interní.</small><div id="calendar"></div></fieldset></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-29T12:32">úno 29, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/projekt/">Projekt</a></h2><p>Nějaký perex</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/akce/" class="c-card-tag">🗓️ Akce</a></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1><svg class="fi fi-bell" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></g></svg> Jsem · Nudista · Online</h1><p>Jsem Jan Rippl - nudista online! Vítejte! Tato část mého webu slouží jako moje pracovní nástěnka a zároveň jako má vlatní "sociální sít", jenž je umístěna na službě NoLog (Gitea) co by statické HTML stránky, spravované za pomoci publikačního systému Publii.</p><fieldset><legend><svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewbox="0 0 24 24"><path fill="currentColor" d="M16.279 11.506c.132-.016.257-.018.373 0c.066-.154.078-.419.019-.708c-.09-.429-.211-.688-.461-.646c-.251.04-.261.35-.17.779c.05.24.14.446.239.575m-2.149.339c.18.078.29.129.331.086c.029-.028.021-.084-.022-.154a1.05 1.05 0 0 0-.464-.371a1.26 1.26 0 0 0-1.228.146c-.119.088-.232.209-.218.283c.007.023.023.042.065.05c.099.011.444-.164.843-.188c.282-.02.513.068.693.148m-.361.205c-.232.037-.361.113-.443.187c-.071.062-.113.128-.113.177l.018.042l.037.014c.053 0 .171-.046.171-.046c.324-.115.539-.102.752-.078c.117.014.172.02.198-.02c.007-.012.018-.035-.007-.074c-.056-.091-.291-.24-.613-.202m1.784.756c.159.078.333.046.39-.069c.059-.115-.024-.272-.183-.349c-.158-.079-.333-.049-.39.066c-.057.115.026.274.183.352m1.018-.891c-.128-.002-.234.138-.238.316c-.003.177.1.321.229.322c.129.002.235-.139.238-.315s-.099-.32-.229-.323m-8.644 3.183c-.032-.04-.085-.029-.136-.015c-.036.007-.076.017-.119.016a.265.265 0 0 1-.221-.111c-.059-.09-.056-.225.01-.378l.03-.069c.104-.231.275-.619.082-.988a.88.88 0 0 0-.671-.488a.861.861 0 0 0-.739.267c-.284.313-.327.741-.273.893c.021.056.053.071.075.074c.048.007.119-.029.164-.15l.014-.038c.02-.064.057-.184.118-.278a.518.518 0 0 1 .717-.15c.2.131.275.375.19.608c-.044.121-.115.351-.1.54c.032.383.27.537.48.556c.206.007.35-.108.387-.193c.021-.053.003-.084-.008-.096"></path><path fill="currentColor" d="M19.821 14.397c-.009-.029-.061-.216-.13-.44l-.144-.384c.281-.423.286-.799.249-1.013a1.284 1.284 0 0 0-.372-.724c-.222-.232-.677-.472-1.315-.651l-.335-.093c-.002-.015-.018-.79-.031-1.123c-.011-.24-.031-.616-.148-.986c-.14-.502-.381-.938-.684-1.221c.835-.864 1.355-1.817 1.354-2.634c-.003-1.571-1.933-2.049-4.312-1.063l-.503.214c-.002-.002-.911-.894-.924-.905c-2.714-2.366-11.192 7.06-8.48 9.349l.593.501a2.916 2.916 0 0 0-.166 1.345c.065.631.389 1.234.915 1.701c.5.442 1.159.724 1.796.723c1.055 2.432 3.465 3.922 6.291 4.007c3.032.09 5.576-1.333 6.644-3.889c.069-.179.365-.987.365-1.7c-.001-.718-.406-1.015-.663-1.014M7.416 16.309a1.38 1.38 0 0 1-.28.021c-.916-.026-1.905-.85-2.003-1.827c-.109-1.08.443-1.912 1.421-2.108c.116-.025.258-.038.41-.031c.548.032 1.354.452 1.539 1.645c.164 1.055-.096 2.132-1.087 2.3m-1.021-4.562a2.325 2.325 0 0 0-1.473.94c-.197-.164-.562-.48-.626-.604c-.524-.994.571-2.928 1.337-4.02c1.889-2.698 4.851-4.739 6.223-4.371c.222.064.96.921.96.921s-1.37.759-2.642 1.819c-1.711 1.32-3.006 3.236-3.779 5.315m9.611 4.158a.05.05 0 0 0 .03-.054a.05.05 0 0 0-.056-.045s-1.434.212-2.789-.283c.147-.479.541-.308 1.134-.259a8.287 8.287 0 0 0 2.735-.296c.613-.177 1.419-.524 2.045-1.018c.212.465.286.975.286.975s.163-.029.3.055c.13.08.224.245.16.671c-.133.798-.471 1.445-1.042 2.041a4.259 4.259 0 0 1-1.249.934a5.337 5.337 0 0 1-.814.346c-2.149.701-4.349-.07-5.058-1.727a2.761 2.761 0 0 1-.142-.392c-.302-1.092-.046-2.4.755-3.226v-.001c.051-.052.102-.113.102-.191c0-.064-.042-.133-.077-.183c-.28-.406-1.253-1.099-1.057-2.44c.139-.964.982-1.642 1.768-1.602l.2.012c.34.02.637.063.917.076c.47.019.891-.049 1.391-.465c.169-.142.304-.263.532-.301c.024-.006.084-.025.203-.021a.681.681 0 0 1 .343.109c.4.266.457.912.479 1.385c.012.269.045.922.055 1.108c.026.428.139.489.365.563c.129.044.248.074.423.125c.529.147.845.3 1.043.493a.637.637 0 0 1 .188.372c.065.457-.353 1.021-1.455 1.533c-1.206.559-2.669.701-3.679.588l-.354-.04c-.81-.108-1.269.936-.784 1.651c.313.461 1.164.761 2.017.761c1.953.002 3.455-.832 4.015-1.554l.044-.063c.026-.042.005-.063-.03-.041c-.455.312-2.483 1.552-4.651 1.18c0 0-.264-.044-.504-.138c-.19-.072-.591-.258-.639-.668c1.747.543 2.85.031 2.85.03m-6.12-7.852c.672-.776 1.499-1.452 2.241-1.83c.025-.014.052.015.038.038a2.125 2.125 0 0 0-.208.508c-.006.027.023.049.046.032c.462-.314 1.264-.651 1.968-.693a.03.03 0 0 1 .021.055a1.66 1.66 0 0 0-.31.311c-.014.02-.001.049.024.049c.494.003 1.191.175 1.644.43c.03.018.008.077-.025.069c-.688-.157-1.811-.277-2.979.008c-1.044.254-1.84.646-2.419 1.069c-.03.02-.065-.019-.041-.046"></path></svg> Služba Mailchimp</legend><form id="form" class="validate" action="https://online.us2.list-manage.com/subscribe/post?u=2ac5382353059a705b15ea6ab&id=9189628091&f_id=00924ae0f0" method="post" target="_self"><label>E-mail pro zasílání novinek:</label><br><input id="mce-EMAIL" name="EMAIL" required="" size="40%" type="email" placeholder="Váš e-mail"><div style="position: absolute; left: -5000px;" aria-hidden="true"><input tabindex="-1" name="b_2ac5382353059a705b15ea6ab_9189628091" type="text"></div> <input id="mc-embedded-subscribe" name="subscribe" type="submit" value="Přihlásit"></form><p><br><a href="/ochrana-soukromi/">· Soukromí</a> <a href="#">· Kontakt</a> <a href="/rubriky/politiky/">· Politiky</a></p></fieldset><fieldset><legend><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Veřejné kalendáře (Google)</legend><small>Všechny mé kalendáře se dělí na veřejné / neveřejné a externí / interní.</small><div id="calendar"></div></fieldset></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-03-05T22:13">bře 5, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/pixelfedcz/">Pixelfed.cz</a></h2><p>Registrace dnes 5. března 2024 ve 20:59...</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-29T12:32">úno 29, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/projekt/">Projekt</a></h2><p>Nějaký perex</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/akce/" class="c-card-tag">🗓️ Akce</a></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024 </time><a href="https://jsem.nudista.online/kategorie/inzerce/" class="c-card-tag">🔔 Inzerce</a></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/agenda/index.html b/kategorie/agenda/index.html index ac70f2d..0f8c694 100644 --- a/kategorie/agenda/index.html +++ b/kategorie/agenda/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 📋 Agenda - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="📋 Agenda"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/agenda/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📋 Agenda <sup>(0)</sup></h1><p>Souhrn toho, co je třeba udělat, vyřídit, zařídit. Agenda je například program schůze, porady nebo konference, tedy souhrn témat, o nichž se má jednat. Agendou nějakého člověka nebo organizace se rozumí souhrn jeho či jejích praktických cílů a úkolů...</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📋 Agenda <sup>(0)</sup></h1><p>Souhrn toho, co je třeba udělat, vyřídit, zařídit. Agenda je například program schůze, porady nebo konference, tedy souhrn témat, o nichž se má jednat. Agendou nějakého člověka nebo organizace se rozumí souhrn jeho či jejích praktických cílů a úkolů...</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/akce/index.html b/kategorie/akce/index.html index b53cb2e..08c537e 100644 --- a/kategorie/akce/index.html +++ b/kategorie/akce/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 🗓️ Akce - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="🗓️ Akce"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/akce/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🗓️ Akce <sup>(1)</sup></h1></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🗓️ Akce <sup>(1)</sup></h1></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿"> <a href="https://jsem.nudista.online/autor/jan-rippl-cz/">CZ 🇨🇿</a></div><time datetime="2024-02-23T21:36">úno 23, 2024</time></div><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/synchronizace-kalendaru/">Synchronizace kalendářů</a></h2><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/foto/index.html b/kategorie/foto/index.html index 3966efb..5dec4c1 100644 --- a/kategorie/foto/index.html +++ b/kategorie/foto/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 📷 Foto - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="📷 Foto"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/foto/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📷 Foto <sup>(0)</sup></h1><p>Věnuji se digitální umělecké a dokumentární tvorbě <svg class="fi fi-video" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></g></svg> <svg class="fi fi-film" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></g></svg>, jako je tvorba v programech <i class="bi bi-badge-3d"></i> Blender & MakeHuman, <i class="bi bi-badge-4k"></i> video & fotografie</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📷 Foto <sup>(0)</sup></h1><p>Věnuji se digitální umělecké a dokumentární tvorbě <svg class="fi fi-video" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></g></svg> <svg class="fi fi-film" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></g></svg>, jako je tvorba v programech <i class="bi bi-badge-3d"></i> Blender & MakeHuman, <i class="bi bi-badge-4k"></i> video & fotografie</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/index.html b/kategorie/index.html index e71ff38..e9bdc8b 100644 --- a/kategorie/index.html +++ b/kategorie/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>All tags - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Jsem · Nudista · Online"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main page page--tags"><div class="hero"><div class="main__inner"><h1>Kategorie</h1><p>Seznam všech 10 kategorií...</p></div></div><div class="main__inner"><ul class="tags-list"><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/video/"><span class="tags-list__title">🎥 Video <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/agenda/"><span class="tags-list__title">📋 Agenda <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/redakce/"><span class="tags-list__title">📝 Redakce <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/internet/"><span class="tags-list__title">📡 Internet <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/foto/"><figure class="tags-list__image"><img src="https://jsem.nudista.online/media/tags/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/tags/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/tags/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 760px) 311px, (min-width: 480px) calc(44.23vw - 16px), calc(100vw - 63px)" loading="lazy" height="1208" width="1920" alt=""></figure><span class="tags-list__title">📷 Foto <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/inzerce/"><span class="tags-list__title">🔔 Inzerce <sup>(2)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/it/"><span class="tags-list__title">🖥️ IT <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/akce/"><span class="tags-list__title">🗓️ Akce <sup>(1)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/nahota/"><figure class="tags-list__image"><img src="https://jsem.nudista.online/media/tags/10/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/tags/10/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/tags/10/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 760px) 311px, (min-width: 480px) calc(44.23vw - 16px), calc(100vw - 63px)" loading="lazy" height="1255" width="1920" alt=""></figure><span class="tags-list__title">😳 Nahota <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/technologie/"><span class="tags-list__title">🚀 Technologie <sup>(0)</sup></span></a></li></ul></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main page page--tags"><div class="hero"><div class="main__inner"><h1>Kategorie</h1><p>Seznam všech 10 kategorií...</p></div></div><div class="main__inner"><ul class="tags-list"><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/video/"><span class="tags-list__title">🎥 Video <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/agenda/"><span class="tags-list__title">📋 Agenda <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/redakce/"><span class="tags-list__title">📝 Redakce <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/internet/"><span class="tags-list__title">📡 Internet <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/foto/"><figure class="tags-list__image"><img src="https://jsem.nudista.online/media/tags/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/tags/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/tags/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 760px) 311px, (min-width: 480px) calc(44.23vw - 16px), calc(100vw - 63px)" loading="lazy" height="1208" width="1920" alt=""></figure><span class="tags-list__title">📷 Foto <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/inzerce/"><span class="tags-list__title">🔔 Inzerce <sup>(2)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/it/"><span class="tags-list__title">🖥️ IT <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/akce/"><span class="tags-list__title">🗓️ Akce <sup>(1)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/nahota/"><figure class="tags-list__image"><img src="https://jsem.nudista.online/media/tags/10/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/tags/10/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/tags/10/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 760px) 311px, (min-width: 480px) calc(44.23vw - 16px), calc(100vw - 63px)" loading="lazy" height="1255" width="1920" alt=""></figure><span class="tags-list__title">😳 Nahota <sup>(0)</sup></span></a></li><li class="tags-list__item"><a href="https://jsem.nudista.online/kategorie/technologie/"><span class="tags-list__title">🚀 Technologie <sup>(0)</sup></span></a></li></ul></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/internet/index.html b/kategorie/internet/index.html index c19ac4f..2ff359c 100644 --- a/kategorie/internet/index.html +++ b/kategorie/internet/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 📡 Internet - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="📡 Internet"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/internet/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📡 Internet <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📡 Internet <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/inzerce/index.html b/kategorie/inzerce/index.html index 7736710..4a939ba 100644 --- a/kategorie/inzerce/index.html +++ b/kategorie/inzerce/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 🔔 Inzerce - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="🔔 Inzerce"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/inzerce/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🔔 Inzerce <sup>(2)</sup></h1></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024</time></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024</time></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🔔 Inzerce <sup>(2)</sup></h1></div></div><div class="main__inner"><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T17:27">úno 23, 2024</time></div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/2/young-woman-1488904_1920.jpg" srcset="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1208" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/">Hledá se fotograf(ka)</a></h2><p></p></header></article><article class="c-card"><div class="c-card__meta"><div class="c-card__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="SRN 🇩🇪"> <a href="https://jsem.nudista.online/autor/jan-rippl-de/">SRN 🇩🇪</a></div><time datetime="2024-02-23T13:56">úno 23, 2024</time></div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="c-card__image"><img src="https://jsem.nudista.online/media/posts/1/nude-6221682_1920.jpg" srcset="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg 320w, https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-sm.jpg 480w" sizes="(min-width: 700px) 200px, (min-width: 480px) 160px, calc(100vw - 50px)" loading="lazy" height="1255" width="1920" alt=""></a><header class="c-card__header"><h2 class="c-card__title"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/">Hledá se kameraman(ka)</a></h2><p>Za účelem pořizování videozáznamů mých aktivit z oblasti nudismu za pomoci digitální kamery s umístěním zejména ve Spolkové republice Německo, jakož i v České republice, hledám spolupracovníka na pozici kameraman - kameramanka.</p></header></article></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/it/index.html b/kategorie/it/index.html index 556d80c..e465cdf 100644 --- a/kategorie/it/index.html +++ b/kategorie/it/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 🖥️ IT - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="🖥️ IT"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/it/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🖥️ IT <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🖥️ IT <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/nahota/index.html b/kategorie/nahota/index.html index 829adb3..d476e2c 100644 --- a/kategorie/nahota/index.html +++ b/kategorie/nahota/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 😳 Nahota - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="😳 Nahota"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/nahota/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>😳 Nahota <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>😳 Nahota <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/redakce/index.html b/kategorie/redakce/index.html index 279ac42..39951b3 100644 --- a/kategorie/redakce/index.html +++ b/kategorie/redakce/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 📝 Redakce - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="📝 Redakce"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/redakce/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📝 Redakce <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>📝 Redakce <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/technologie/index.html b/kategorie/technologie/index.html index e7121d0..71d7abe 100644 --- a/kategorie/technologie/index.html +++ b/kategorie/technologie/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 🚀 Technologie - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="🚀 Technologie"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/technologie/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🚀 Technologie <sup>(0)</sup></h1><p>HTML, CSS, JS, PHP, SQLite, JSON, XML, Python</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🚀 Technologie <sup>(0)</sup></h1><p>HTML, CSS, JS, PHP, SQLite, JSON, XML, Python</p></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/kategorie/video/index.html b/kategorie/video/index.html index 0101c66..5a3c1fe 100644 --- a/kategorie/video/index.html +++ b/kategorie/video/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tag: 🎥 Video - NoLogWeb</title><meta name="robots" content="noindex, follow"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="🎥 Video"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content=""><meta property="og:url" content="https://jsem.nudista.online/kategorie/video/"><meta property="og:type" content="website"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"NoLogWeb","logo":"https://jsem.nudista.online/media/website/logo.svg","url":"https://jsem.nudista.online/","sameAs":[]}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🎥 Video <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><div class="hero"><div class="main__inner"><h1>🎥 Video <sup>(0)</sup></h1></div></div><div class="main__inner"></div></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/pixelfedcz/index.html b/pixelfedcz/index.html new file mode 100644 index 0000000..9755094 --- /dev/null +++ b/pixelfedcz/index.html @@ -0,0 +1,145 @@ +<!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pixelfed.cz - NoLogWeb</title><meta name="description" content="Registrace dnes 5. března 2024 ve 20:59..."><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="canonical" href="https://jsem.nudista.online/pixelfedcz/"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Pixelfed.cz"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content="Registrace dnes 5. března 2024 ve 20:59..."><meta property="og:url" content="https://jsem.nudista.online/pixelfedcz/"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://jsem.nudista.online/pixelfedcz/"},"headline":"Pixelfed.cz","datePublished":"2024-03-05T22:13","dateModified":"2024-03-05T22:13","image":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80},"description":"Registrace dnes 5. března 2024 ve 20:59...","author":{"@type":"Person","name":"CZ 🇨🇿","url":"https://jsem.nudista.online/autor/jan-rippl-cz/"},"publisher":{"@type":"Organization","name":"CZ 🇨🇿","logo":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80}}}</script><noscript><style>img[loading] { + opacity: 1; + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + var calendarEl = document.getElementById('calendar'); + + var calendar = new FullCalendar.Calendar(calendarEl, { + headerToolbar: { + left: 'title', + center: '', + right: '' + }, + footerToolbar: { + start: '', + center: '', + end: 'today prevYear,prev,next,nextYear' + }, + initialView: 'listDay', + locale: 'cs', + timeZone: 'Europe/Prague', + displayEventTime: true, // don't show the time column in list view + + googleCalendarApiKey: 'AIzaSyD0IpA16TFEGbQNqUaD5MNmVm364DpHlqU', + eventSources: [ + { + googleCalendarId: 'b4742c190c0f07274f2d3b8d0646db4b8dd98289c8159113bdda029514286bf3@group.calendar.google.com' + }, + { + googleCalendarId: 'o8sn1cursjbacb729ulb4c50og@group.calendar.google.com', + className: 'nice-event' + }, + { + googleCalendarId: 'uqu50taoqg4gi4jrl7pu7kknfs@group.calendar.google.com', + className: 'nice-event' + }, + { + googleCalendarId: 'cs.czech#holiday@group.v.calendar.google.com', + className: 'nice-event' + }, + { + googleCalendarId: 'janrippl@gmail.com', + className: 'nice-event' + } + ] + }); + + calendar.render(); + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-cz/" class="post__author__name">CZ 🇨🇿</a></div></div><div class="post__date"><time datetime="2024-03-05T22:13">bře 5, 2024</time></div></div><header class="post__header"><h1 class="post__title">Pixelfed.cz</h1></header><div class="post__entry"><p>Registrace dnes 5. března 2024 ve 20:59...</p><p> </p></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-05T22:13">bře 5, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fpixelfedcz%2F&via=NoLogWeb&text=Pixelfed.cz" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to +//var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); +var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); + +// Update the count down every 1 second +var x = setInterval(function() { + + // Get today's date and time + var now = new Date().getTime(); + + // Find the distance between now and the count down date + var distance = countDownDate - now; + + // Time calculations for days, hours, minutes and seconds + var days = Math.floor(distance / (1000 * 60 * 60 * 24)); + var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((distance % (1000 * 60)) / 1000); + + // Display the result in the element with id="demo" + document.getElementById("demo").innerHTML = "<button title='Dny'>" + days + "</button> <button title='Hodiny'>" + hours + "</button> <button title='Minuty'>" + + minutes + "</button> <button title='Vteřiny'>" + seconds + "</button>"; + + // If the count down is finished, write some text + if (distance < 0) { + clearInterval(x); + document.getElementById("demo").innerHTML = "<button title='Dny'>00</button>:<button title='Hodiny'>00</button>:<button title='Minuty'>00</button>:<button title='Vteřiny'>00</button>"; + } +}, 1000);</script></div><p><small>Vyprší dne: 26. června 2026</small></p></section><div class="follow"><div class="flb-container"><a href="https://witter.cz/@janrippl" target="_blank" class="tltp tltp--top p-mastodon"><svg width="24" height="24" aria-hidden="false"><use xlink:href="https://jsem.nudista.online/media/plugins/followButtons/svg-map.svg#mastodon"/></svg> </a><a href="https://twitter.com/JanRippl" target="_blank" class="tltp tltp--top p-twitter"><svg width="24" height="24" aria-hidden="false"><use xlink:href="https://jsem.nudista.online/media/plugins/followButtons/svg-map.svg#twitter"/></svg> </a><a href="https://www.deviantart.com/janrippl" target="_blank" class="tltp tltp--top p-deviantart"><svg width="24" height="24" aria-hidden="false"><use xlink:href="https://jsem.nudista.online/media/plugins/followButtons/svg-map.svg#deviantart"/></svg></a></div></div></div></div></div></div><script defer="defer" src="https://jsem.nudista.online/assets/js/scripts.min.js?v=12d8fcd46db8fdc7af6797ec26849875"></script><script>var images = document.querySelectorAll('img[loading]'); + for (var i = 0; i < images.length; i++) { + if (images[i].complete) { + images[i].classList.add('is-loaded'); + } else { + images[i].addEventListener('load', function () { + this.classList.add('is-loaded'); + }, false); + } + }</script><script>(function() { + let collapsibleTOCItems = document.querySelectorAll(".post__toc h3"); + + collapsibleTOCItems.forEach(function(item, index) { + let subList = item.nextElementSibling; + let parentItem = item.parentElement; + let toggleButton; + + if (subList && subList.tagName === "OL") { + let defaultState = 'false'; + parentItem.setAttribute('aria-expanded', defaultState); + subList.setAttribute('aria-hidden', defaultState === 'true' ? 'false' : 'true'); + item.setAttribute('id', 'sublist-' + index); + item.setAttribute('tabindex', '0'); + + const toggleElement = "both"; + + if (toggleElement === "header" || toggleElement === "both") { + item.addEventListener('click', function(event) { + event.preventDefault(); + toggleSubList(this, toggleButton); + }); + + item.addEventListener('keydown', function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + toggleSubList(this, toggleButton); + } + }); + } + + if (toggleElement === "button" || toggleElement === "both") { + toggleButton = item.querySelector('.post__toc-toggle'); + toggleButton.textContent = defaultState === 'true' ? 'sbalit' : 'rozbalit'; + toggleButton.addEventListener('click', function(event) { + event.stopPropagation(); + event.preventDefault(); + toggleSubList(item, toggleButton); + }); + } + } + }); + + function toggleSubList(element, button) { + let parentItem = element.parentElement; + let subList = parentItem.querySelector('ol'); + + if (subList) { + if (parentItem.getAttribute('aria-expanded') === 'true') { + parentItem.setAttribute('aria-expanded', 'false'); + subList.setAttribute('aria-hidden', 'true'); + } else { + parentItem.setAttribute('aria-expanded', 'true'); + subList.setAttribute('aria-hidden', 'false'); + } + } + + if (button) { + button.textContent = parentItem.getAttribute('aria-expanded') === 'true' ? 'sbalit' : 'rozbalit'; + } + } + })();</script></body></html> \ No newline at end of file diff --git a/projekt/index.html b/projekt/index.html index 3344aa9..3ce2505 100644 --- a/projekt/index.html +++ b/projekt/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Projekt - NoLogWeb</title><meta name="description" content="Nějaký perex"><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="canonical" href="https://jsem.nudista.online/projekt/"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Projekt"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content="Nějaký perex"><meta property="og:url" content="https://jsem.nudista.online/projekt/"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://jsem.nudista.online/projekt/"},"headline":"Projekt","datePublished":"2024-02-29T12:32","dateModified":"2024-03-01T16:05","image":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80},"description":"Nějaký perex","author":{"@type":"Person","name":"CZ 🇨🇿","url":"https://jsem.nudista.online/autor/jan-rippl-cz/"},"publisher":{"@type":"Organization","name":"CZ 🇨🇿","logo":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80}}}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-cz/" class="post__author__name">CZ 🇨🇿</a></div></div><div class="post__date"><time datetime="2024-02-29T12:32">úno 29, 2024</time></div></div><header class="post__header"><h1 class="post__title">Projekt</h1></header><div class="post__entry"><p>Nějaký perex</p><nav class="post__toc"><h3><span>Seznam ke čtení:</span><button class="post__toc-toggle">rozbalit</button></h3><ol><li><a href="#mcetoc_1hnrd8tpjh">Úvod do projektu</a></li><li><a href="#mcetoc_1hnqa8p3u1">Můj osobní web</a></li><li><a href="#mcetoc_1hnqab7r23">Magazín o nudismu</a></li><li><a href="#mcetoc_1hnr07ums8">Můj osobní blog</a></li></ol></nav><h1 id="mcetoc_1hnrd8tpjh"><i class="bi bi-0-square-fill"></i> Úvod do projektu</h1><fieldset><legend><svg class="fi fi-info" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></g></svg> Vysvětlivky:</legend><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nevyžaduji</li><li><svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg> - Bude</li><li><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Je</li></ul><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nesmí být</li><li><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Musí být</li></ul><div style="clear: both;"> </div><fieldset><legend><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nesmí být...</legend>To proto, že danou službu potřebuji obvykle z technických důvodů provozovat mimo mojí jurisdikci ().</fieldset><fieldset><legend><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Musí být...</legend>To proto, že</fieldset></fieldset><h1 id="mcetoc_1hnqa8p3u1"><i class="bi bi-1-square-fill"></i> Můj osobní web</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> janrippl.cz</legend>Můj osobní web, tvořený HTML/Bootstrap dostupnou zdarma, jenž bude výhradně sloužit k prezentaci mé osoby a mých aktivit, kterým se věnuji.<p class="msg msg--info"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset><h1 id="mcetoc_1hnqab7r23"><i class="bi bi-2-square-fill"></i> Magazín o nudismu</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> nudista.online</legend><p class="msg msg--success"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset><h1 id="mcetoc_1hnr07ums8"><i class="bi bi-3-square-fill"></i> Můj osobní blog</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> jsem.nudista.online</legend><p class="msg msg--info"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-01T16:05">bře 1, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fprojekt%2F&via=NoLogWeb&text=Projekt" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li class="active"><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-cz/" class="post__author__name">CZ 🇨🇿</a></div></div><div class="post__date"><time datetime="2024-02-29T12:32">úno 29, 2024</time></div></div><header class="post__header"><h1 class="post__title">Projekt</h1></header><div class="post__entry"><p>Nějaký perex</p><nav class="post__toc"><h3><span>Seznam ke čtení:</span><button class="post__toc-toggle">rozbalit</button></h3><ol><li><a href="#mcetoc_1hnrd8tpjh">Úvod do projektu</a></li><li><a href="#mcetoc_1hnqa8p3u1">Můj osobní web</a></li><li><a href="#mcetoc_1hnqab7r23">Magazín o nudismu</a></li><li><a href="#mcetoc_1hnr07ums8">Můj osobní blog</a></li></ol></nav><h1 id="mcetoc_1hnrd8tpjh"><i class="bi bi-0-square-fill"></i> Úvod do projektu</h1><fieldset><legend><svg class="fi fi-info" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></g></svg> Vysvětlivky:</legend><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nevyžaduji</li><li><svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg> - Bude</li><li><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Je</li></ul><ul style="float: left; padding-right: 10px; list-style-type: none;"><li><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nesmí být</li><li><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Musí být</li></ul><div style="clear: both;"> </div><fieldset><legend><svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg> - Nesmí být...</legend>To proto, že danou službu potřebuji obvykle z technických důvodů provozovat mimo mojí jurisdikci ().</fieldset><fieldset><legend><svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="green" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg> - Musí být...</legend>To proto, že</fieldset></fieldset><h1 id="mcetoc_1hnqa8p3u1"><i class="bi bi-1-square-fill"></i> Můj osobní web</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> janrippl.cz</legend>Můj osobní web, tvořený HTML/Bootstrap dostupnou zdarma, jenž bude výhradně sloužit k prezentaci mé osoby a mých aktivit, kterým se věnuji.<p class="msg msg--info"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset><h1 id="mcetoc_1hnqab7r23"><i class="bi bi-2-square-fill"></i> Magazín o nudismu</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> nudista.online</legend><p class="msg msg--success"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset><h1 id="mcetoc_1hnr07ums8"><i class="bi bi-3-square-fill"></i> Můj osobní blog</h1><fieldset><legend><svg class="fi fi-link" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></g></svg> jsem.nudista.online</legend><p class="msg msg--info"><strong><svg class="fi fi-trello" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></g></svg>Stav:</strong> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg>nápad · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>testování · <svg class="fi fi-x-square" width="24px" height="24px" fill="none" stroke="red" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></g></svg>realizace · <svg class="fi fi-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></g></svg>produkce</p></fieldset></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-01T16:05">bře 1, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fprojekt%2F&via=NoLogWeb&text=Projekt" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime(); diff --git a/sitemap.xml b/sitemap.xml index 5cea25d..211a578 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -21,6 +21,10 @@ </image:image> </url> <url> +<loc>https://jsem.nudista.online/pixelfedcz/</loc> +<lastmod>2024-03-05T22:13:11+01:00</lastmod> +</url> +<url> <loc>https://jsem.nudista.online/projekt/</loc> <lastmod>2024-03-01T16:05:18+01:00</lastmod> </url> diff --git a/synchronizace-kalendaru/index.html b/synchronizace-kalendaru/index.html index e7f9f2f..6ef6215 100644 --- a/synchronizace-kalendaru/index.html +++ b/synchronizace-kalendaru/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html><html lang="cs"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Synchronizace kalendářů - NoLogWeb</title><meta name="description" content="Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite."><meta name="generator" content="Publii Open-Source CMS for Static Site"><link rel="canonical" href="https://jsem.nudista.online/synchronizace-kalendaru/"><link rel="alternate" type="application/atom+xml" href="https://jsem.nudista.online/feed.xml"><link rel="alternate" type="application/json" href="https://jsem.nudista.online/feed.json"><meta property="og:title" content="Synchronizace kalendářů"><meta property="og:image" content="https://jsem.nudista.online/media/website/logo.svg"><meta property="og:image:width" content="80"><meta property="og:image:height" content="84"><meta property="og:site_name" content="Jsem · Nudista · Online"><meta property="og:description" content="Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite."><meta property="og:url" content="https://jsem.nudista.online/synchronizace-kalendaru/"><meta property="og:type" content="article"><link rel="shortcut icon" href="https://jsem.nudista.online/media/website/favicon.ico" type="image/x-icon"><link rel="preload" href="https://jsem.nudista.online/assets/dynamic/fonts/publicsans/publicsans.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="https://jsem.nudista.online/assets/css/style.css?v=e074b0391e95f6546d012c5297aa5bfb"><script type="application/ld+json">{"@context":"http://schema.org","@type":"Article","mainEntityOfPage":{"@type":"WebPage","@id":"https://jsem.nudista.online/synchronizace-kalendaru/"},"headline":"Synchronizace kalendářů","datePublished":"2024-02-23T21:36","dateModified":"2024-03-02T15:25","image":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80},"description":"Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.","author":{"@type":"Person","name":"CZ 🇨🇿","url":"https://jsem.nudista.online/autor/jan-rippl-cz/"},"publisher":{"@type":"Organization","name":"CZ 🇨🇿","logo":{"@type":"ImageObject","url":"https://jsem.nudista.online/media/website/logo.svg","height":84,"width":80}}}</script><noscript><style>img[loading] { opacity: 1; - }</style></noscript><script src="https://obec-mokriny.cz/kalendar-novy/dist/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/list/index.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/core/locales/cs.global.js"></script><script src="https://obec-mokriny.cz/kalendar-novy/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { + }</style></noscript><script src="/fullcalendar-main/dist/index.global.js"></script><script src="/fullcalendar-main/packages/list/index.global.js"></script><script src="/fullcalendar-main/packages/core/locales/cs.global.js"></script><script src="/fullcalendar-main/packages/google-calendar/index.global.js"></script><script type="text/javascript">document.addEventListener('DOMContentLoaded', function() { var calendarEl = document.getElementById('calendar'); var calendar = new FullCalendar.Calendar(calendarEl, { @@ -44,7 +44,7 @@ }); calendar.render(); - });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-map-pin" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-map" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/akce/" class="metadata__maintag">🗓️ Akce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-cz/" class="post__author__name">CZ 🇨🇿</a></div></div><div class="post__date"><time datetime="2024-02-23T21:36">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Synchronizace kalendářů</h1><p class="post__lead">Synchronizace Google kalendářů s interní databází</p></header><div class="post__entry"><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p><p>Další text</p><p class="msg msg--warning"><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Kalendář <a href="mailto:nudist@nline">janrippl</a> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg></p><p class="msg msg--warning"><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Kalendář <a href="mailto:nudist@nline">nudist@nline</a> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg></p><p>Kalendář janrippl</p></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T15:25">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fsynchronizace-kalendaru%2F&via=NoLogWeb&text=Synchronizace%20kalend%C3%A1%C5%99%C5%AF" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to + });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"><style>:root {--toc-max-height: auto;}html {scroll-padding-top: 2rem;}.post__toc h3{align-items: center;border: none;cursor: pointer;display: inline-flex;gap: .5em;margin: 0;padding: 0;user-select: none;}.post__toc > ol {margin-left: 0;max-height: 0;overflow: auto;opacity: 0;transition: opacity 0.3s ease-out, max-height 0.5s ease-out;}.post__toc > ol[aria-hidden="false"] {max-height: var(--toc-max-height);opacity: 1;transition: opacity 0.3s ease-in, max-height 0.5s ease-in;}.post__toc ol {counter-reset: item;list-style: none;}.post__toc li {padding-left: 0;padding-bottom: 0;}.post__toc ol ol {margin-left: 1.5rem;margin-top: 0;}.post__toc ol ol li::before {margin-left: 0;}.post__toc a {align-items: stretch;color: var(--toc-link-color, var(--link-color));display: inline-flex;flex-wrap: nowrap;}.post__toc a:hover, .post__toc a:active, .post__toc a:focus {color: var(--toc-link-color-hover, var(--link-color-hover));}.post__toc a::before {content: counters(item, ".", decimal) ". ";counter-increment: item;color: var(--toc-number-color, var(--text-color));display: inline-block;flex-grow: 0;flex-shrink: 0;margin-right: 0.2em;}.post__toc-toggle {background: none;border: none;border-radius: 0;box-shadow: none;color: var(--toc-toggle-link-color, var(--color));cursor: pointer;display: inline;font-weight: normal;overflow: visible;padding: 0;text-align: left;text-decoration: none;text-transform: none;vertical-align: baseline;will-change: unset;}.post__toc-toggle:hover, .post__toc-toggle:active, .post__toc-toggle:focus {background: inherit;border: inherit;box-shadow: inherit;color: var(--toc-toggle-link-color-hover, var(--color, inherit));text-decoration: none;transform: inherit;}.post__toc-toggle::before{content: "[";color: var(--toc-toggle-color, var(--text-color));}.post__toc-toggle::after {content: "]";color: var(--toc-toggle-color, var(--text-color));}</style><noscript><style>.post__toc > ol {max-height: var(--toc-max-height);opacity: 1;transition: none;}.post__toc h3{cursor: default;}</style></noscript><style>.fi{fill:none;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}</style><style>:root { --flb-container-gap: 0.8rem; --flb-container-direction: row; --flb-btn-hover-effect: translate3d(0, -2px, 0); --flb-btn-gap: 0.4rem; --flb-btn-transition: all 0.24s ease-out; --flb-btn-border-radius: 6px; --flb-btn-font-family: var(--body-font); --flb-btn-font-size: 0.875rem; --flb-btn-line-height: inherit; --flb-icon-size: 24px; --flb-link-color: #404258; --flb-link-color-hover: #EC1037; --flb-link-icon-color: #404258; --flb-link-icon-color-hover: #EC1037; }.flb-container { display: inline-flex; flex-direction: var(--flb-container-direction); flex-wrap: wrap; gap: var(--flb-container-gap); justify-content: center; }a.tltp tltp--top { all: unset; align-items: center; color: var(--flb-link-color); cursor: pointer; display: inline-flex; font-family: var(--flb-btn-font-family); font-size: var(--flb-btn-font-size); gap: var(--flb-btn-gap); line-height: var(--flb-btn-line-height); transition: var(--flb-btn-transition); text-decodration: none; will-change: transform; } a.tltp tltp--top:hover { color: var(--flb-link-color-hover); transform: var(--flb-btn-hover-effect); } a.tltp tltp--top svg { fill: var(--flb-link-icon-color); height: var(--flb-icon-size); margin: unset; opacity: unset; pointer-events: none; transition: var(--flb-btn-transition); width: var(--flb-icon-size); } a.tltp tltp--top:hover svg { fill: var(--flb-link-icon-color-hover); }a.tltp tltp--top svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top:hover svg { fill: hsla(var(--s-platform-h), var(--s-platform-s), var(--s-platform-l), var(--s-platform-a)); }a.tltp tltp--top.p-mastodon{--s-platform-h:252;--s-platform-s:59%;--s-platform-l:51%;--s-platform-a:1;}a.tltp tltp--top.p-twitter{--s-platform-h:0;--s-platform-s:0%;--s-platform-l:0%;--s-platform-a:1;}a.tltp tltp--top.p-deviantart{--s-platform-h:161;--s-platform-s:100%;--s-platform-l:45%;--s-platform-a:1;}</style></head><body><div class="content"><div class="left-bar"><div class="left-bar__inner"><header class="header"><a class="logo" href="https://jsem.nudista.online/"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a><nav class="navbar"><button class="navbar__toggle" aria-label="Nabídka" aria-haspopup="true" aria-expanded="false"><span class="navbar__toggle__box"><span class="navbar__toggle__inner">Nabídka</span></span></button><ul class="navbar__menu"><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-cz/" target="_self" aria-label="Jan Rippl 🇨🇿"><svg class="fi fi-user-plus" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></g></svg> <span>Jan Rippl 🇨🇿</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/autor/jan-rippl-de/" target="_self" aria-label="Jan Rippl 🇩🇪"><svg class="fi fi-user-check" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></g></svg> <span>Jan Rippl 🇩🇪</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/projekt/" target="_self" aria-label="Projekt 🇪🇺"><svg class="fi fi-users" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></g></svg> <span>Projekt 🇪🇺</span></a></li><li><a class="tltp" href="https://jsem.nudista.online/kategorie/" target="_self" aria-label="Kategorie 🏷️"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> <span>Kategorie 🏷️</span></a></li></ul></nav><a class="logo logo--atbottom" href="./"><img src="https://jsem.nudista.online/media/website/logo.svg" alt="NoLogWeb" width="80" height="84"></a></header></div></div><main class="main"><article class="post"><div class="post__meta post__meta--attop"><div class="post__meta--attop__inner"><div class="post__maintag"><svg width="20" height="20" aria-hidden="true" focusable="false"><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#tag"/></svg> Publikováno v kategorii: <a href="https://jsem.nudista.online/kategorie/akce/" class="metadata__maintag">🗓️ Akce</a></div></div></div><div class="main__inner"><div class="post__meta"><div class="post__author"><img src="https://www.gravatar.com/avatar/153f1792a2575477faab9e0cc15eec39?s=240" loading="lazy" height="240" width="240" alt="CZ 🇨🇿" class="post__author__avatar"><div><a href="https://jsem.nudista.online/autor/jan-rippl-cz/" class="post__author__name">CZ 🇨🇿</a></div></div><div class="post__date"><time datetime="2024-02-23T21:36">úno 23, 2024</time></div></div><header class="post__header"><h1 class="post__title">Synchronizace kalendářů</h1><p class="post__lead">Synchronizace Google kalendářů s interní databází</p></header><div class="post__entry"><p>Nakonec jsem dospěl k závěru, že nejlepší bude používat co by datové zdroje kalendáře Google, zatímco pro můj interní kalendář databázi SQLite.</p><p>Další text</p><p class="msg msg--warning"><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Kalendář <a href="mailto:nudist@nline">janrippl</a> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg></p><p class="msg msg--warning"><svg class="fi fi-calendar" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></g></svg> Kalendář <a href="mailto:nudist@nline">nudist@nline</a> <svg class="fi fi-check-square" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></g></svg></p><p>Kalendář janrippl</p></div><footer class="post__footer"><div class="post__last-updated">Tento příspěvek byl aktualizován dne: <time datetime="2024-03-02T15:25">bře 2, 2024</time></div><div class="post__share"><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fjsem.nudista.online%2Fsynchronizace-kalendaru%2F&via=NoLogWeb&text=Synchronizace%20kalend%C3%A1%C5%99%C5%AF" class="js-share twitter tltp tltp--top" aria-label="Twitter" rel="nofollow noopener noreferrer"><svg><use xlink:href="https://jsem.nudista.online/assets/svg/svg-map.svg#twitter"/></svg> <span>Twitter</span></a></div></footer></div></article></main><div class="right-bar"><div class="right-bar__inner"><div class="sidebar"><section class="box featured"><h3 class="box__title"><svg class="fi fi-award" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></g></svg> Inzerce:</h3><ul class="featured__container"><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/2/responsive/young-woman-1488904_1920-xs.jpg" loading="lazy" alt="" height="1208" width="1208"></a><div><a href="https://jsem.nudista.online/hleda-se-fotograf-ka/" class="featured__title">Hledá se fotograf(ka)</a> <time class="featured__meta" datetime="2024-02-23T17:27">úno 23, 2024</time></div></li><li class="featured__item"><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__image"><img src="https://jsem.nudista.online/media/posts/1/responsive/nude-6221682_1920-xs.jpg" loading="lazy" alt="" height="1255" width="1255"></a><div><a href="https://jsem.nudista.online/hleda-se-kameraman-ka-2024/" class="featured__title">Hledá se kameraman(ka)</a> <time class="featured__meta" datetime="2024-02-23T13:56">úno 23, 2024</time></div></li></ul></section><section class="box tags"><h3 class="box__title"><svg class="fi fi-tag" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></g></svg> Kategorie:</h3><ul class="tags__list"><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/video/" class="btn btn--gray">🎥 Video <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/agenda/" class="btn btn--gray">📋 Agenda <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/redakce/" class="btn btn--gray">📝 Redakce <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/internet/" class="btn btn--gray">📡 Internet <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/foto/" class="btn btn--gray">📷 Foto <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/inzerce/" class="btn btn--gray">🔔 Inzerce <sup>(2)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/it/" class="btn btn--gray">🖥️ IT <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/akce/" class="btn btn--gray">🗓️ Akce <sup>(1)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/nahota/" class="btn btn--gray">😳 Nahota <sup>(0)</sup></a></li><li class="tags__item"><a href="https://jsem.nudista.online/kategorie/technologie/" class="btn btn--gray">🚀 Technologie <sup>(0)</sup></a></li></ul></section><section class="box newsletter"><h3 class="box__title"><svg class="fi fi-clock" width="24px" height="24px" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></g></svg> Čas zbývající k dokončení:</h3><p class="newsletter__desc">Usilovně na tom pracuji...</p><div id="demo" style="width:100%;"><script>// Set the date we're counting down to //var countDownDate = new Date("<?php echo($row['mesic'] . " " . $row['den'] . ", " . $row['rok'] . " " . $row['hodina'] . ":" . $row['minuta'] . ":" . $row['sekunda']); ?>").getTime(); var countDownDate = new Date("June 9, 2026 00:00:00").getTime();