diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 412eeda..0000000 --- a/.gitattributes +++ /dev/null @@ -1,22 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto - -# Custom for Visual Studio -*.cs diff=csharp -*.sln merge=union -*.csproj merge=union -*.vbproj merge=union -*.fsproj merge=union -*.dbproj merge=union - -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7e7c863 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,34 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [master] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build-and-deploy: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@3.6.2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: public \ No newline at end of file diff --git a/.gitignore b/.gitignore index bb1b644..e7a327a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,218 +1,5 @@ -################# -## Eclipse -################# +/node_modules/ +/public/build/ -*.pydevproject -.project -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.publishproj - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap .DS_Store - - -############# -## Python -############# - -*.py[cod] - -# Packages -*.egg -*.egg-info -dist/ -build/ -eggs/ -parts/ -var/ -sdist/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg - -.idea \ No newline at end of file +.eslintcache diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 73fe088..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 lhswebdev - -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. \ No newline at end of file diff --git a/Mockups/Adhiv/about.png b/Mockups/Adhiv/about.png deleted file mode 100644 index 92b9626..0000000 Binary files a/Mockups/Adhiv/about.png and /dev/null differ diff --git a/Mockups/Adhiv/about.psd b/Mockups/Adhiv/about.psd deleted file mode 100644 index 734cf41..0000000 Binary files a/Mockups/Adhiv/about.psd and /dev/null differ diff --git a/Mockups/Adhiv/flyer.png b/Mockups/Adhiv/flyer.png deleted file mode 100644 index cbe1e09..0000000 Binary files a/Mockups/Adhiv/flyer.png and /dev/null differ diff --git a/Mockups/Adhiv/flyer.psd b/Mockups/Adhiv/flyer.psd deleted file mode 100644 index 6e52ffc..0000000 Binary files a/Mockups/Adhiv/flyer.psd and /dev/null differ diff --git a/Mockups/Adhiv/landingpage-update.png b/Mockups/Adhiv/landingpage-update.png deleted file mode 100644 index 4713642..0000000 Binary files a/Mockups/Adhiv/landingpage-update.png and /dev/null differ diff --git a/Mockups/Adhiv/landingpage-update.psd b/Mockups/Adhiv/landingpage-update.psd deleted file mode 100644 index d7ac960..0000000 Binary files a/Mockups/Adhiv/landingpage-update.psd and /dev/null differ diff --git a/Mockups/Adhiv/lwd.png b/Mockups/Adhiv/lwd.png deleted file mode 100644 index 3b202ea..0000000 Binary files a/Mockups/Adhiv/lwd.png and /dev/null differ diff --git a/Mockups/Adhiv/lwd.psd b/Mockups/Adhiv/lwd.psd deleted file mode 100644 index e04ec09..0000000 Binary files a/Mockups/Adhiv/lwd.psd and /dev/null differ diff --git a/Mockups/Adhiv/miniflyer-double.jpg b/Mockups/Adhiv/miniflyer-double.jpg deleted file mode 100644 index c83fb6e..0000000 Binary files a/Mockups/Adhiv/miniflyer-double.jpg and /dev/null differ diff --git a/Mockups/Adhiv/miniflyer.jpg b/Mockups/Adhiv/miniflyer.jpg deleted file mode 100644 index b4d1302..0000000 Binary files a/Mockups/Adhiv/miniflyer.jpg and /dev/null differ diff --git a/Mockups/Adhiv/miniflyer.psd b/Mockups/Adhiv/miniflyer.psd deleted file mode 100644 index 2d94d19..0000000 Binary files a/Mockups/Adhiv/miniflyer.psd and /dev/null differ diff --git a/Mockups/Adhiv/presentationslide1 b/Mockups/Adhiv/presentationslide1 deleted file mode 100644 index ff485c1..0000000 Binary files a/Mockups/Adhiv/presentationslide1 and /dev/null differ diff --git a/Mockups/Adhiv/presentationslide1.png b/Mockups/Adhiv/presentationslide1.png deleted file mode 100644 index 10aeb28..0000000 Binary files a/Mockups/Adhiv/presentationslide1.png and /dev/null differ diff --git a/Mockups/Adhiv/presentationslide2.png b/Mockups/Adhiv/presentationslide2.png deleted file mode 100644 index 937da0c..0000000 Binary files a/Mockups/Adhiv/presentationslide2.png and /dev/null differ diff --git a/Mockups/Adhiv/presentationslide2.psd b/Mockups/Adhiv/presentationslide2.psd deleted file mode 100644 index f97dab9..0000000 Binary files a/Mockups/Adhiv/presentationslide2.psd and /dev/null differ diff --git a/Mockups/Adhiv/prof-pic.png b/Mockups/Adhiv/prof-pic.png deleted file mode 100644 index 29e41c6..0000000 Binary files a/Mockups/Adhiv/prof-pic.png and /dev/null differ diff --git a/Mockups/Adhiv/prof-pic.psd b/Mockups/Adhiv/prof-pic.psd deleted file mode 100644 index ed84423..0000000 Binary files a/Mockups/Adhiv/prof-pic.psd and /dev/null differ diff --git a/Mockups/Adhiv/updates.png b/Mockups/Adhiv/updates.png deleted file mode 100644 index 55e30b8..0000000 Binary files a/Mockups/Adhiv/updates.png and /dev/null differ diff --git a/Mockups/Adhiv/updates.psd b/Mockups/Adhiv/updates.psd deleted file mode 100644 index d2e4b82..0000000 Binary files a/Mockups/Adhiv/updates.psd and /dev/null differ diff --git a/Mockups/Matthew/Landing Page 8-12-14/Landing Page.png b/Mockups/Matthew/Landing Page 8-12-14/Landing Page.png deleted file mode 100644 index dbaff21..0000000 Binary files a/Mockups/Matthew/Landing Page 8-12-14/Landing Page.png and /dev/null differ diff --git a/Mockups/Matthew/Landing Page 8-12-14/Landing Page.psd b/Mockups/Matthew/Landing Page 8-12-14/Landing Page.psd deleted file mode 100644 index c642fe4..0000000 Binary files a/Mockups/Matthew/Landing Page 8-12-14/Landing Page.psd and /dev/null differ diff --git a/Mockups/Matthew/Landing Page Editor Hook/landingpage-update.psd b/Mockups/Matthew/Landing Page Editor Hook/landingpage-update.psd deleted file mode 100644 index e11883e..0000000 Binary files a/Mockups/Matthew/Landing Page Editor Hook/landingpage-update.psd and /dev/null differ diff --git a/Mockups/Matthew/Landing Page w smaller header/landingpage-update.psd b/Mockups/Matthew/Landing Page w smaller header/landingpage-update.psd deleted file mode 100644 index 8b6e601..0000000 Binary files a/Mockups/Matthew/Landing Page w smaller header/landingpage-update.psd and /dev/null differ diff --git a/Mockups/Matthew/Thumbs.db b/Mockups/Matthew/Thumbs.db deleted file mode 100644 index 6f24f59..0000000 Binary files a/Mockups/Matthew/Thumbs.db and /dev/null differ diff --git a/Mockups/presentation mocks/divider-slide.png b/Mockups/presentation mocks/divider-slide.png deleted file mode 100644 index b17ae94..0000000 Binary files a/Mockups/presentation mocks/divider-slide.png and /dev/null differ diff --git a/Mockups/presentation mocks/divider-slide.psd b/Mockups/presentation mocks/divider-slide.psd deleted file mode 100644 index a86501c..0000000 Binary files a/Mockups/presentation mocks/divider-slide.psd and /dev/null differ diff --git a/Mockups/presentation mocks/general-slide.png b/Mockups/presentation mocks/general-slide.png deleted file mode 100644 index 271fa15..0000000 Binary files a/Mockups/presentation mocks/general-slide.png and /dev/null differ diff --git a/Mockups/presentation mocks/general-slide.psd b/Mockups/presentation mocks/general-slide.psd deleted file mode 100644 index e814e56..0000000 Binary files a/Mockups/presentation mocks/general-slide.psd and /dev/null differ diff --git a/Mockups/presentation mocks/title.png b/Mockups/presentation mocks/title.png deleted file mode 100644 index 9a12c3f..0000000 Binary files a/Mockups/presentation mocks/title.png and /dev/null differ diff --git a/Mockups/presentation mocks/title.psd b/Mockups/presentation mocks/title.psd deleted file mode 100644 index 0a8a3a8..0000000 Binary files a/Mockups/presentation mocks/title.psd and /dev/null differ diff --git a/README.md b/README.md index 258f708..bd1774e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -lhswebdev.github.io -=================== +# LHS Webstie + +made by anish lakkapragada (hence spaper) \ No newline at end of file diff --git a/about.html b/about.html deleted file mode 100644 index 955d8f8..0000000 --- a/about.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - About - - - - - - - - - - - - -
-
- - -

Lynbrook Web Dev

-
- -
-

ABOUT THE TEAM

- -
- Aanand -
-

AANAND K.

-

Co-president

-

akainth015@gmail.com

-
- -

- Hey there! I'm one of your co-presidents, and I've been coding for about five years. I have experience with some cool technologies like Node.js, React.js, Vue.js, and other really cool stacks (not related to WebDev). I hope to be able to teach you guys some advanced web development tools, and how to create advanced projects with persistent, real-time data. -

-
- -
- Nirmit -
-

NIRMIT A.

-

Co-President

-

nirmitashar@outlook.com

-
- -

- Hi there! I'm the other co-president, and have been learning coding since 5th grade, starting with web dev. I have much experience with the standard languages: HTML/CSS/Javascript, and with many of JavaScript libraries and frameworks such as Vue, JQuery, and Angular. I have built websites for multiple volunteering organizations, for hackathons, and for myself and friends. If you join Web Dev, you'll learn the skills to do the same! We'll be working on cool projects throughout the year and possibly do some competitions, so make sure you and your friends join! -

-
- -
- Kunal -
-

KUNAL S.

-

vice president

-

kunal@kunalsheth.info

-
- -

- Hello! I'm your Vice President for 2018/19. I primarily work with JVM languages but also have experience with functional programming, real-time control systems, and server-side web development. This summer, I built a robotics software framework, researched type-systems, and programmed microcontrollers. Currently, I'm managing web development for two organizations. WebDev this year is going to be great. We make all sorts of cool and interesting projects - don’t miss out! -

-
- -
- Kento Nishi -
-

KENTO N.

-

Lead Developer

-

kento24gs@outlook.com

-

GitHub: @KentoNishi

-
- -

- Hello World! - I'm the lead developer for the 2019-2020 school year. - I'll be working with both front-end and back-end development. - I have many years of experience with languages such as JavaScript, - Python, C++, and more! - Recently, I've been working on many machine learning projects - with libraries such as Keras and TensorFlow. - I'm a very strong believer in the future of Web 2.0, and I'm - also a huge open source enthusiast. - I'm really excited to be a part of the Web-Dev team! - -

-
- -
- Sofia -
-

SOFIA T.

-

secretary/treasurer

-

tkasof@gmail.com

-
- -

- Hey! I'm Sofia, your 2019/20 secretary/treasurer. Having only coded for about a year, I definitely have much left to learn on my journey through computer science. I initially realized my interest for web development just last year, which later persuaded me to begin studying CS myself. So don't worry if you don't have any experience! Passion stems from interest, and we'd be happy to help you find yours. -

-
-
-
- - diff --git a/bin/prerender.js b/bin/prerender.js new file mode 100644 index 0000000..3a2f528 --- /dev/null +++ b/bin/prerender.js @@ -0,0 +1,23 @@ +import { existsSync, promises as fs } from 'fs' +import { join } from 'path' + +import App from '../src/App.svelte' + +async function main() { + const templatePath = join(process.cwd(), 'src', 'index.template') + const publicPath = join(process.cwd(), 'public') + + const template = await fs.readFile(templatePath) + const app = App.render() + + if (!existsSync(publicPath)) { + await fs.mkdir(publicPath) + } + + await fs.writeFile( + join(publicPath, 'index.html'), + template.toString().replace('%svelte.head%', app.head).replace('%svelte.html%', app.html) + ) +} + +main() \ No newline at end of file diff --git a/calendar.html b/calendar.html deleted file mode 100644 index 54c57ec..0000000 --- a/calendar.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - Calendar - - - - - - - - - - - - - - - - - - -
-
- - -

Lynbrook Web Dev

-
- -

CALENDAR

- - -
- -
- - - diff --git a/content/images/cityscape.jpg b/content/images/cityscape.jpg deleted file mode 100644 index fd35e5f..0000000 Binary files a/content/images/cityscape.jpg and /dev/null differ diff --git a/content/images/codebrowser-cropped.png b/content/images/codebrowser-cropped.png deleted file mode 100644 index dec0f3b..0000000 Binary files a/content/images/codebrowser-cropped.png and /dev/null differ diff --git a/content/images/codebrowser-cropped.psd b/content/images/codebrowser-cropped.psd deleted file mode 100644 index adfef25..0000000 Binary files a/content/images/codebrowser-cropped.psd and /dev/null differ diff --git a/content/images/codebrowser.png b/content/images/codebrowser.png deleted file mode 100644 index 9cdbcb6..0000000 Binary files a/content/images/codebrowser.png and /dev/null differ diff --git a/content/images/codebrowser.psd b/content/images/codebrowser.psd deleted file mode 100644 index b1e0afe..0000000 Binary files a/content/images/codebrowser.psd and /dev/null differ diff --git a/content/images/css-icon.png b/content/images/css-icon.png deleted file mode 100644 index ca59005..0000000 Binary files a/content/images/css-icon.png and /dev/null differ diff --git a/content/images/flatphone.png b/content/images/flatphone.png deleted file mode 100644 index 56e80e7..0000000 Binary files a/content/images/flatphone.png and /dev/null differ diff --git a/content/images/graypattern.jpg b/content/images/graypattern.jpg deleted file mode 100644 index 3b856bb..0000000 Binary files a/content/images/graypattern.jpg and /dev/null differ diff --git a/content/images/hamburger.black.png b/content/images/hamburger.black.png deleted file mode 100644 index 964bf52..0000000 Binary files a/content/images/hamburger.black.png and /dev/null differ diff --git a/content/images/hamburger.white.png b/content/images/hamburger.white.png deleted file mode 100644 index c49f89f..0000000 Binary files a/content/images/hamburger.white.png and /dev/null differ diff --git a/content/images/html-icon.png b/content/images/html-icon.png deleted file mode 100755 index b816da9..0000000 Binary files a/content/images/html-icon.png and /dev/null differ diff --git a/content/images/js-icon.png b/content/images/js-icon.png deleted file mode 100755 index c4e3ec2..0000000 Binary files a/content/images/js-icon.png and /dev/null differ diff --git a/content/images/mock.flipped.png b/content/images/mock.flipped.png deleted file mode 100644 index a81cbca..0000000 Binary files a/content/images/mock.flipped.png and /dev/null differ diff --git a/content/images/mock.png b/content/images/mock.png deleted file mode 100644 index 413e257..0000000 Binary files a/content/images/mock.png and /dev/null differ diff --git a/content/images/team/Arthur.JPG b/content/images/team/Arthur.JPG deleted file mode 100644 index b88acd0..0000000 Binary files a/content/images/team/Arthur.JPG and /dev/null differ diff --git a/content/images/team/Kento.jpg b/content/images/team/Kento.jpg deleted file mode 100644 index 0b189d9..0000000 Binary files a/content/images/team/Kento.jpg and /dev/null differ diff --git a/content/images/team/Matthew.jpg b/content/images/team/Matthew.jpg deleted file mode 100644 index 3a279fa..0000000 Binary files a/content/images/team/Matthew.jpg and /dev/null differ diff --git a/content/images/team/Talha.jpg b/content/images/team/Talha.jpg deleted file mode 100644 index cc569e3..0000000 Binary files a/content/images/team/Talha.jpg and /dev/null differ diff --git a/content/images/team/aanand.jpg b/content/images/team/aanand.jpg deleted file mode 100644 index 7c2e24c..0000000 Binary files a/content/images/team/aanand.jpg and /dev/null differ diff --git a/content/images/team/avinash.jpg b/content/images/team/avinash.jpg deleted file mode 100644 index 07996f1..0000000 Binary files a/content/images/team/avinash.jpg and /dev/null differ diff --git a/content/images/team/kunal.jpg b/content/images/team/kunal.jpg deleted file mode 100644 index 0baa9bc..0000000 Binary files a/content/images/team/kunal.jpg and /dev/null differ diff --git a/content/images/team/nirmit.jpg b/content/images/team/nirmit.jpg deleted file mode 100644 index 04df633..0000000 Binary files a/content/images/team/nirmit.jpg and /dev/null differ diff --git a/content/images/team/shannon.jpg b/content/images/team/shannon.jpg deleted file mode 100644 index 2748264..0000000 Binary files a/content/images/team/shannon.jpg and /dev/null differ diff --git a/content/images/team/stkach.jpg b/content/images/team/stkach.jpg deleted file mode 100644 index 37bb87d..0000000 Binary files a/content/images/team/stkach.jpg and /dev/null differ diff --git a/content/scripts/calendar.js b/content/scripts/calendar.js deleted file mode 100644 index eff75b2..0000000 --- a/content/scripts/calendar.js +++ /dev/null @@ -1,9 +0,0 @@ -$(document).ready(function(){ - $('#calendar').fullCalendar({ - googleCalendarApiKey: 'AIzaSyDTGJh5n6dMIC5sREBfk8DVMRhXW4xt2cY', - events: { - googleCalendarId: 'tc87q7o58cjcirq5edlhg8p8i8@group.calendar.google.com' - }, - eventColor: '#E74B3C' - }); -}); diff --git a/content/scripts/nav.js b/content/scripts/nav.js deleted file mode 100644 index 695f936..0000000 --- a/content/scripts/nav.js +++ /dev/null @@ -1,15 +0,0 @@ -function toggleNav() { - var hamburger = document.getElementById("menu-button"), current_class = hamburger.className; - hamburger.className = (current_class == "open") ? "" : "open"; - - var nav = document.getElementById("nav-container"); - var content = document.getElementById("content-container"); - - if (nav.className != "nav-opened") { - nav.className = "nav-opened"; - content.className = "nav-opened"; - } else { - nav.className = ""; - content.className = ""; - } -} diff --git a/content/scripts/updates.js b/content/scripts/updates.js deleted file mode 100644 index d84582e..0000000 --- a/content/scripts/updates.js +++ /dev/null @@ -1,62 +0,0 @@ -var updates = [ - { - "title":"Passed by Leg Council!", - "date": "November 4th, 2014", - "content": "Aw yeah. Today, our club proposal was passed by the legislative council. Lynbrook Web Dev is officially in business (as soon as next semester arrives)! Thank you to the 70+ people who filled out our interest form earlier in the month. Together, you played a huge role in getting this club started, and we are so excited to get all of you started on some code. Organization is currently underway and more information and details will be posted soon. Do not fear. All shall soon become clear." - }, - { - "title":"Updated Website", - "date": "December 8th, 2014", - "content": "'She gone shake it. Like a red nose, like a, like a, like a red nose...' The original one-page Lynbrok Web Dev website has been updated into the full-feature, responsive site, which includes a calendar of (tentative) meeting times, this updates page, officer biographies, and a link to the soon-to-be-filled Github Repo of lesson presentations and resources. Lesson planning is underway, and - as soon as the website is debugged and finalized - a massive club launch will utterly shatter Facebook. Nah. Just be on the look out for a few posts about the start of the club." - }, - { - "title":"First Meeting", - "date": "December 18th, 2014", - "content": "AT LAST, the time has come for this club to commence. The first Lynbrook Web Dev meeting will take place on Tuesday, January 13 at lunch in room 608. This first meeting will be quick and purely informational, so no need to bring anything. We'll be giving an overview of our plans for the semester as well as instructions for each of you as members. After we get this informational meeting out of the way, we will begin fun stuff the following week. For now, make sure to come out to our first meeting!" - }, - { - "title":"CAML Takeover", - "date": "February 4, 2015", - "content": "Due to the CAML taking place in room 608 on Tuesday, February 10, we will not be having a member meeting. See you all after the break! If you ever need to look at any of the resources, just look through our presentations in the Resources section of our website." - }, - { - "title": "New Year, New Update!", - "date": "August 27, 2016", - "content": "It's finally the new school year, and we're pumped to start the third year of Web Dev Club! Check out our Club Info Day booth on Thursday, 9/1 for more info." - }, - { - "title": "Never 2 Late for an Update", - "date": "September 5, 2018", - "content": "It's Club Info Day once again...two years later! Thanks for coming by our website. If you want to stay updated for our first meeting and haven't signed up at our booth today, sign up on our interest list linked on the 'Home' page. You'll then receive an exciting survey after." - }, - { - title: "New Meeting Room", - date: "September 17, 2019", - content: "Club Info Day just happened, and your officer team is excited for a new year! You can sign up to receive emails about the club! We also have a new advisor, Mrs. Levin! That means we'll be meeting in a new room: room 204. Make sure to stop by and learn more! We'll see you then!" - } -]; - - -var section, article, info, updatetitle, updatetitlecontent, date, datecontent, text, textcontent; -section = document.createElement("section"); -section.setAttribute("id","updates"); -document.getElementById("content-container").appendChild(section); -for(var i = updates.length-1; i>=0; i--) { - article = document.createElement("article"); - info = document.createElement("section"); - info.setAttribute("class","update-info"); - updatetitle = document.createElement("h1"); - updatetitlecontent = document.createTextNode(updates[i].title); - updatetitle.appendChild(updatetitlecontent); - date = document.createElement("h2"); - datecontent = document.createTextNode(updates[i].date); - date.appendChild(datecontent); - info.appendChild(updatetitle); - info.appendChild(date); - text = document.createElement("p"); - textcontent = document.createTextNode(updates[i].content); - text.appendChild(textcontent); - article.appendChild(info); - article.appendChild(text); - section.appendChild(article); -} diff --git a/content/styles/about.css b/content/styles/about.css deleted file mode 100644 index c11b7fb..0000000 --- a/content/styles/about.css +++ /dev/null @@ -1,99 +0,0 @@ -body { - background-color: #F0F0F0; -} - -#team { - margin-bottom: 50px; -} - -#team article { - position: relative; - margin: 50px auto 0; - - height: 200px; - width: 80%; - - background-color: #fff; - text-align: left; -} - #team > article > img { - float: left; - margin-top: 25px; - margin-left: 50px; - - height: 150px; - width: 150px; - border-radius: 150px; - } - #team > article > .quick-info { - float: left; - margin-left: 25px; - } - #team .quick-info > h1 { - color: #E74B3C; - margin-top: 50px; - font-size: 30px; - } - #team .quick-info > h2 { - font-size: 25px; - font-weight: 500; - } - #team .quick-info > h3 { - font-size: 20px; - font-weight: 300; - } - #team > article > p { - font-size: 16px; - width: 47%; - margin: 0; - margin-right: 50px; - - /*Fix for making the p vertically centered and floated right. It was set to a block level element: didn't want to mess with that*/ - position: absolute; - top: 50%; - right: 0; - transform: translateY(-52%); - } - -/* Collapse in the image and quick-info so the paragraph doesn't run over the email. */ -@media all and (max-width: 1210px) and (min-width:1100px) { - #team article img { - margin-left: 25px; - } - #team .quick-info > h1 { - margin-top:60px; - } - - #team .quick-info > h1 { font-size:25px; } - #team .quick-info > h2 { font-size:20px; } - #team .quick-info > h3 { font-size:18px; } -} -@media all and (max-width:1100px) { - #team article { - height: auto; - text-align:center; - padding-top:50px; - padding-bottom: 50px; - } - #team article img { - float:none; - margin: 0 auto; - height: 200px; - width: 200px; - border-radius: 200px; - } - #team article .quick-info { - float: none; - margin-bottom:25px; - margin-left:0; - } - #team article p { - /*Undo the vertical centering*/ - transform: none; - position: static; - - float:none; - width:70%; - margin:0 auto; - } -} diff --git a/content/styles/calendar.css b/content/styles/calendar.css deleted file mode 100644 index ec79a6b..0000000 --- a/content/styles/calendar.css +++ /dev/null @@ -1,10 +0,0 @@ -body { - background-color: rgb(240, 240, 240); -} - -#calendar { - margin: 50px auto; - padding: 2% 3%; - background-color: white; - width: 80%; -} diff --git a/content/styles/index.css b/content/styles/index.css deleted file mode 100644 index 9f64858..0000000 --- a/content/styles/index.css +++ /dev/null @@ -1,288 +0,0 @@ -.spins { - -webkit-transform: rotate(180deg); /* Chrome, Opera 15+, Safari 3.1+ */ - -ms-transform: rotate(180deg); /* IE 9 */ - transform: rotate(180deg); /* Firefox 16+, IE 10+, Opera */ -} - -h1, h2, h3 { - font-weight: 300; - text-align: center; - margin: 0; -} - h1 { - font-size: 30px; - } -h3, code, button { - margin: 0 auto; - margin-top: 25px; - margin-bottom: 25px; - font-size: 25px; -} - -/* These are styles for the big welcome row at the top. */ -#welcome { - background-color: #e74b3c; - color: #fff; - text-align: center; - overflow-y: hidden; /* This hides the lower box shadow on the brackets screenshot so it doesn't go past the orange. */ - height:600px; -} - #welcome > h1 { - font-size: 40px; - padding-top: 40px; - } - #welcome > h2 { - font-size: 20px; - margin-top: 10px; - font-weight: 400; - } - #welcome > h3 { - width:50%; - } - - #brackets-screenshot { - width: 60%; - box-shadow: 0 10px 48px -3px #000; - overflow-y: hidden; - display: block; /* Thanks! http://www.htmlforums.com/css/t-margin-under-image-in-ie-73651.html */ - margin: 0 auto; - margin-top: 50px; - } - -/* These are styles for the "hook", that lets the user play around with CSS styles. */ -.hook code { - font-size: 25px; - display: block; -} - .hook code input { - font-size: 20px; - } - -/* These are styles for the language boxes. */ -#lang-boxes { - text-align: center; /* This centers the middle box (the other two are floated). */ - margin-bottom: 50px; - margin-top: 50px; - color: #2c3e50; -} - #lang-boxes > article { - width: 25%; - background-color: #f4f4f4; - height: 300px; - padding: 10px; - - border: 1px solid; - border-top-width:15px; - } - /* These position the boxes. */ - #lang-boxes #html-box { - float: left; - margin-left: 50px; - border-color: #2ecc71; - } - #lang-boxes #css-box { - /* This is basically a replacement for float:center; it'll cause it to position based on the text-align:center in #lang-boxes. */ - display: inline-block; - border-color: #8e44ad; - } - #lang-boxes #js-box { - float: right; - margin-right: 50px; - border-color: #3498db; - } - - /* This styles the inside of the boxes. */ - #lang-boxes > article > h1 { - font-weight: 600; - margin-top: 25px; - } - #lang-boxes > article > img { - width: 75px; - margin-top: 15px; - margin-bottom: 15px; - } - #lang-boxes > article > p { - font-size: 20px; - } - -/* These are styles for the events section. */ -.event { - padding-top: 50px; - padding-bottom: 50px; -} - .event > h1 { - width: 300px; - padding-bottom: 50px; - margin: 0 auto; - border-bottom: 1px solid currentColor; - } - .event > p { - font-size: 18px; - line-height: 35px; - - width: 50%; - - margin: 0 auto; - margin-top: 50px; - } - -#meeting { - color: white; - - background: url('../images/graypattern.jpg'); /* Thanks! http://www.thewallpapers.org/download/26391/gray-patterns-wallpaper */ - background-size:cover; - background-attachment: fixed; -} - #track-boxes { /* These are styles for the front-end / back-end description boxes */ - width:70%; - margin:25px auto; - overflow:hidden; - } - #track-boxes > article { - margin:10px; - width:350px; - height:240px; - border:1px solid white; - border-top-width:15px; - padding:15px; - padding-bottom:20px; - } - #front-box { - float:left; - } - #back-box { - float:right; - } - #track-boxes > article > h1 { - margin-top:25px; - } - -/* See http://stackoverflow.com/questions/20268962/fixed-attachment-background-image-flicker-disappear-in-chrome-when-coupled-with */ -@media screen and (-webkit-min-device-pixel-ratio:0) { - #meeting { - background-attachment: scroll; - } -} -#workshop { - color: black; - - background: white; - position: relative; -} - #workshop > img { - width: 300px; - position: absolute; - top: 80px; - } - #phone { left: 30px; } - #mock { right: 30px; } - - -/* These are styles for the footer. */ -#goodbye { - padding-top: 50px; - background-color: #e74b3c; - color: white; - position: relative; - width: 100%; - margin: 0; - padding-bottom: 50px; -} - #goodbye > .button { - margin-top: 100px; - } - -/* Assorted media queries that change the page depending on it's size. */ -@media all and (max-width:1200px) { - /* This keeps the images out of the way of the text for hackathons */ - #workshop > img { - width: 200px; - } - - #track-boxes { - width:85%; - } - #track-boxes > article { - width:315px; - } -} - -@media all and (max-width:890px) { - #lang-boxes > article > p { - font-size: 18px; - } - #track-boxes { /* Removes the floats and centers boxes regularly */ - text-align: center; - } - #track-boxes > article { - width:90%; - height:initial; - margin:10px 0; - float:none; - display:inline-block; - } -} - -@media all and (max-width:770px) { - .event > p { - width: 80%; - } - - /* Hides the mock image and centers the phone image. */ - #workshop > img { - position: static; - width: 40%; - margin-top: 50px; - } - #mock { display:none; } -} - -@media all and (max-width:700px) { - #welcome h3 { - font-size: 20px; - width: 90%; - } - - #welcome #brackets-screenshot { - width: 90%; - } - - #lang-boxes { - margin-top: 0; - position: relative; - height: 1100px; - } - /* - I know what you're thinking - what is all of this crappy code just to center some divs?!? - You dummy, just float:none all of them and margin: 0 auto! - WELL IT'S NOT THAT EASY -.- Chrome is kind of mean about floats. - https://stackoverflow.com/questions/26540998/chrome-not-re-applying-floats-after-media-queries - */ - #lang-boxes #html-box, - #lang-boxes #css-box, - #lang-boxes #js-box { - position: absolute; - margin: 0; - width: 70%; - left: 15%; - margin-left: -10px; - display: block; - } -<<<<<<< Updated upstream - - #lang-boxes #html-box { margin-top: 0; } - #lang-boxes #css-box { margin-top: 360px; } - #lang-boxes #js-box { margin-top: 720px; } -} -======= - .bubbles #html-bubble { - margin-top: 0; - } - .bubbles #css-bubble { - margin-top: 360px; - } - .bubbles #js-bubble { - margin-top: 720px; - } -} ->>>>>>> Stashed changes diff --git a/content/styles/nav.css b/content/styles/nav.css deleted file mode 100644 index ac8ac37..0000000 --- a/content/styles/nav.css +++ /dev/null @@ -1,121 +0,0 @@ -/* Modeled after http://purecss.io/layouts/side-menu/ */ -body, html { - overflow-x: hidden; -} - -#nav-container { - position: fixed; - margin-left: -200px; - padding-top: 100px; - - width: 200px; - height: 100%; - - background-color: #272727; - box-shadow: inset -5px -5px 15px #000; - - transition: margin-left 0.5s ease; -} - #nav-container.nav-opened { - margin-left: 0; - } - #nav-container a { - display: block; - - width: 100%; - height: 50px; - line-height: 50px; - - text-decoration: none; - - font-size: 25px; - color: #fff; - } - #nav-container a.current, - #nav-container a:hover { - background-color: #fff; - color: #000; - } - -#content-container { - position: relative; - width: 100%; - - transition: margin-left 0.5s ease; -} -#content-container.nav-opened { - margin-left: 200px; -} - -/* Modeled after http://codepen.io/designcouch/pen/Atyop */ - -#menu-button { - width: 60px; - height: 45px; - position: absolute; - margin: 25px; - transform: rotate(0deg); - transition: .5s ease-in-out; - cursor: pointer; - z-index: 1; -} - #menu-button.open { - position: fixed; - margin-left:-125px; - } - -#menu-button span { - display: block; - position: absolute; - height: 6px; - width: 100%; - background-color: #fff; - border-radius: 6px; - opacity: 1; - left: 0; - transform-origin: left center; - transform: rotate(0deg); - transition: .25s ease-in-out; -} - -#menu-button span:nth-child(1) { - top: 0px; -} - -#menu-button span:nth-child(2) { - top: 18px; -} - -#menu-button span:nth-child(3) { - top: 36px; -} - -#menu-button.open span:nth-child(1) { - transform: rotate(45deg); - top: -3px; - left: 8px; -} - -#menu-button.open span:nth-child(2) { - width: 0%; - opacity: 0; -} - -#menu-button.open span:nth-child(3) { - transform: rotate(-45deg); - top: 39px; - left: 8px; -} - -#header-bar { - width: 100%; - height:95px; - background-color: #E74B3C; -} - #header-bar h1 { - color: #fff; - position: absolute; - left: 150px; - line-height: 95px; - font-size: 35px; - } \ No newline at end of file diff --git a/content/styles/shared.css b/content/styles/shared.css deleted file mode 100644 index 39c8f3d..0000000 --- a/content/styles/shared.css +++ /dev/null @@ -1,55 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Lato:300,400,600,700,800); - -html, body { - margin: 0; - width: 100%; - height: 100%; - color: #2c3e50; - text-align: center; - font-family: 'Lato'; -} - -h1, h2, h3 { - margin: 0; -} -h1 { - font-weight: 400; -} - -button { - margin: 0 auto; - margin-top: 25px; - margin-bottom: 25px; - font-size: 25px; -} - -a { - color: currentColor; -} - -.button { - width: 150px; - line-height: 55px; - - font-size: 20px; - text-decoration: none; - - display: block; - margin: 0 auto; -} - .standard { - background-color: #fff; - color: #e74b3c; - border: 1px solid #fff; - } - .standard:hover { - background-color: #e74b3c; - color: #fff; - } - -.title { - margin-top: 75px; - color: #343333; - font-weight: 700; - font-size:35px; -} diff --git a/content/styles/updates.css b/content/styles/updates.css deleted file mode 100644 index 3258ee9..0000000 --- a/content/styles/updates.css +++ /dev/null @@ -1,35 +0,0 @@ -body { - background-color: #F0F0F0; -} - -#updates { - margin-bottom: 50px; -} - #updates > article { - text-align: left; - background-color: #fff; - width: 60%; - margin: 50px auto 0 auto; - padding: 20px 40px; - border-radius: 20px; - border-left: 10px solid #e74b3c; - } - .update-info > h1 { - text-transform: uppercase; - font-size: 30px; - color: #e74b3c; - font-weight: 800; - padding: 5px 0; - } - .update-info > h2 { - text-transform: lowercase; - font-size: 25px; - color: #bababa; - font-weight: 500; - padding-bottom: 5px; - } - #updates > article > p { - margin: 0; - font-size: 18px; - line-height: 135%; - } diff --git a/encryption-demo/encryption.html b/encryption-demo/encryption.html deleted file mode 100644 index 9d2d210..0000000 --- a/encryption-demo/encryption.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/encryption-demo/encryption.js b/encryption-demo/encryption.js deleted file mode 100644 index 7999a84..0000000 --- a/encryption-demo/encryption.js +++ /dev/null @@ -1,56 +0,0 @@ -function shiftChar(ch, sh) { - sh = sh % 26; - var i = ch.charCodeAt() + sh; - if (i > 122) { // 'z' - i = i - 26; - } - else if (i < 97) { // 'a' - i = i + 26; - } - return String.fromCharCode(i); -} - -function caesar(text, shift) { - text = text.toLowerCase(); - var ciphertext = ""; - for (var i=0; i < text.length; i++) { - var c = shiftChar(text.charAt(i), shift); - ciphertext += c; - } - console.log(ciphertext); -} - -function vigenere(text, shift) { - text = text.toLowerCase(); - var ciphertext = ""; - for (var i=0; i < text.length; i++) { - var s = shift.charAt(i % shift.length).charCodeAt() - 96; - var c = shiftChar(text.charAt(i), s); - ciphertext += c; - } - console.log(ciphertext); -} - -function freqCount(text) { - text = text.toLowerCase(); - var frequencies = {}; - for (var i=0; i < text.length; i++) { - var c = text.charAt(i); - if (!frequencies.hasOwnProperty(c)) { - frequencies[c] = 0; - } - frequencies[c]++; - } - console.log(frequencies); - printFreqChart(frequencies); -} - -function printFreqChart(freq) { - Object.keys(freq).forEach(function(key) { - var row = key + " | "; - for (var j = 0; j < freq[key]; j++) { - row += "x"; - } - console.log(row); - }); -} diff --git a/index.html b/index.html deleted file mode 100644 index b391f7f..0000000 --- a/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - LHS Web Dev - - - - - - - - - - - - - -
- - -
-

Lynbrook Web Dev

-

LEARN, CREATE, AND PLAY

- -

In the Web Dev club, we'll teach you everything you need to know to get up and running with web development.

- - < SIGN UP > - -

Scroll down for more

- - web dev -
- - - -
-
-

HTML

- html - -

- HTML is the meat of a web page - it describes the content and general layout of a page. -

-
- -
-

CSS

- css - -

- CSS makes sites pretty - it describes the presentation and design of the underlying HTML. -

-
- -
-

Javascript

- javascript - -

- Javascript is responsible for making things happen - it makes your website as interactive as you want. -

-
-
- -
-
-

MEETINGS

- -

- Our weekly meetings discuss computer networking and the Internet as well as current advances in frontend and backend web development. At the beginning of the year, we'll look at the inner workings of the Internet and demo interesting APIs and browser features. We have two learning tracks for coding: front-end and back-end. -

- -
-
-

Front-end

-

- Front-end is the part of the website you see, the content and the design. This track is a great fit for beginner coders and will cover HTML and CSS. Through our lessons and hands-on projects, you'll be able to code your own personal website (in addition to several smaller side projects) by the end of the year! -

-
-
-

Back-end

-

- Back-end holds the logic and interactivity of your website. If you have basic experience with languages like Java, Python, or C++, back-end is for you! Some back-end projects include an interactive blog, Firebase database, browser games, and an HTTP server. -

-
-
-

- Whether you're looking to get into coding or to advance your existing skills, or if you're just curious about how the Internet works, Web Dev Club is the place for you! -

-
- -
-

WORKSHOPS

- - Phone - Website - -

- We will hold monthly coding workshops for both beginners and veteran coders. These optional workshops will be 1-2 hours during 7th period or afterschool. Interested members with no prior experience have the opportunity to learn the basics of website development (HTML, CSS, and Javascript) through projects created during each workshop, such as a landing page for a personal website. Coders with more experience get to engage in more advanced workshops, which explore a number of topics including, but not limited to, PHP, AJAX, node.js, servers, and databases. -

-
-
- - -
- - - - diff --git a/libraries/fullcalendar-2.2.3/changelog.txt b/libraries/fullcalendar-2.2.3/changelog.txt deleted file mode 100644 index 96aab4e..0000000 --- a/libraries/fullcalendar-2.2.3/changelog.txt +++ /dev/null @@ -1,785 +0,0 @@ - -v2.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 - - -v2.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 - - -v2.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 - - -v2.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 - - -v2.1.1 (2014-08-29) -------------------- - -- removeEventSource not working with array ([2203]) -- mouseout not triggered after mouseover+updateEvent ([829]) -- agenda event's render with no 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 - - -v2.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) -- `eventLimitText` -- `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 - - -v2.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 - - -v2.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 - - -v2.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 - - -v2.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/ - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.5.2 (2011-08-21) -------------------- - -- correctly process UTC "Z" ISO8601 date strings ([750]) - -[750]: https://code.google.com/p/fullcalendar/issues/detail?id=750 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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') - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 - - -v1.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 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 - - -v1.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 - - -v1.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 `' - ) - .click(function() { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('
'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } - - - function updateTitle(text) { - el.find('h2').text(text); - } - - - function activateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - - - function deactivateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - - - function disableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .attr('disabled', 'disabled') - .addClass(tm + '-state-disabled'); - } - - - function enableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeAttr('disabled') - .removeClass(tm + '-state-disabled'); - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -fc.sourceNormalizers = []; -fc.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager(options) { // assumed to be a calendar - var t = this; - - - // exports - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.updateEvent = updateEvent; - t.renderEvent = renderEvent; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - - - // imports - var trigger = t.trigger; - var getView = t.getView; - var reportEvents = t.reportEvents; - var getEventEnd = t.getEventEnd; - - - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var currentFetchID = 0; - var pendingSourceCnt = 0; - var loadingLevel = 0; - var cache = []; // holds events that have already been expanded - - - $.each( - (options.events ? [ options.events ] : []).concat(options.eventSources || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); - - - - /* Fetching - -----------------------------------------------------------------------------*/ - - - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - // or, a part of the new range is outside of the old range? (after normalizing) - start.clone().stripZone() < rangeStart.clone().stripZone() || - end.clone().stripZone() > rangeEnd.clone().stripZone(); - } - - - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - cache = []; - var fetchID = ++currentFetchID; - var len = sources.length; - pendingSourceCnt = len; - for (var i=0; i= eventStart && end <= eventEnd; - } - - - // Does the event's date range intersect with the given range? - // start/end already assumed to have stripped zones :( - function eventIntersectsRange(event, start, end) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = t.getEventEnd(event).stripZone(); - - return start < eventEnd && end > eventStart; - } - -} - - -// updates the "backup" properties, which are preserved in order to compute diffs later on. -function backupEventDates(event) { - event._allDay = event.allDay; - event._start = event.start.clone(); - event._end = event.end ? event.end.clone() : null; -} - -;; - -/* FullCalendar-specific DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left -// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. -function compensateScroll(rowEls, scrollbarWidths) { - if (scrollbarWidths.left) { - rowEls.css({ - 'border-left-width': 1, - 'margin-left': scrollbarWidths.left - 1 - }); - } - if (scrollbarWidths.right) { - rowEls.css({ - 'border-right-width': 1, - 'margin-right': scrollbarWidths.right - 1 - }); - } -} - - -// Undoes compensateScroll and restores all borders/margins -function uncompensateScroll(rowEls) { - rowEls.css({ - 'margin-left': '', - 'margin-right': '', - 'border-left-width': '', - 'border-right-width': '' - }); -} - - -// Make the mouse cursor express that an event is not allowed in the current area -function disableCursor() { - $('body').addClass('fc-not-allowed'); -} - - -// Returns the mouse cursor to its original look -function enableCursor() { - $('body').removeClass('fc-not-allowed'); -} - - -// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. -// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering -// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and -// reduces the available height. -function distributeHeight(els, availableHeight, shouldRedistribute) { - - // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, - // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. - - var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element - var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* - var flexEls = []; // elements that are allowed to expand. array of DOM nodes - var flexOffsets = []; // amount of vertical space it takes up - var flexHeights = []; // actual css height - var usedHeight = 0; - - undistributeHeight(els); // give all elements their natural height - - // find elements that are below the recommended height (expandable). - // important to query for heights in a single first pass (to avoid reflow oscillation). - els.each(function(i, el) { - var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = $(el).outerHeight(true); - - if (naturalOffset < minOffset) { - flexEls.push(el); - flexOffsets.push(naturalOffset); - flexHeights.push($(el).height()); - } - else { - // this element stretches past recommended height (non-expandable). mark the space as occupied. - usedHeight += naturalOffset; - } - }); - - // readjust the recommended height to only consider the height available to non-maxed-out rows. - if (shouldRedistribute) { - availableHeight -= usedHeight; - minOffset1 = Math.floor(availableHeight / flexEls.length); - minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* - } - - // assign heights to all expandable elements - $(flexEls).each(function(i, el) { - var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = flexOffsets[i]; - var naturalHeight = flexHeights[i]; - var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding - - if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things - $(el).height(newHeight); - } - }); -} - - -// Undoes distrubuteHeight, restoring all els to their natural height -function undistributeHeight(els) { - els.height(''); -} - - -// Given `els`, a jQuery set of cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Turns a container element into a scroller if its contents is taller than the allotted height. -// Returns true if the element is now a scroller, false otherwise. -// NOTE: this method is best because it takes weird zooming dimensions into account -function setPotentialScroller(containerEl, height) { - containerEl.height(height).addClass('fc-scroller'); - - // are scrollbars needed? - if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( - return true; - } - - unsetScroller(containerEl); // undo - return false; -} - - -// Takes an element that might have been a scroller, and turns it back into a normal element. -function unsetScroller(containerEl) { - containerEl.height('').removeClass('fc-scroller'); -} - - -/* General DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Given a container element, return an object with the pixel values of the left/right scrollbars. -// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. -// PREREQUISITE: container element must have a single child with display:block -function getScrollbarWidths(container) { - var containerLeft = container.offset().left; - var containerRight = containerLeft + container.width(); - var inner = container.children(); - var innerLeft = inner.offset().left; - var innerRight = innerLeft + inner.outerWidth(); - - return { - left: innerLeft - containerLeft, - right: containerRight - innerRight - }; -} - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -/* FullCalendar-specific Misc Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. -// Expects all dates to be normalized to the same timezone beforehand. -function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { - var segStart, segEnd; - var isStart, isEnd; - - if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? - - if (subjectStart >= intervalStart) { - segStart = subjectStart.clone(); - isStart = true; - } - else { - segStart = intervalStart.clone(); - isStart = false; - } - - if (subjectEnd <= intervalEnd) { - segEnd = subjectEnd.clone(); - isEnd = true; - } - else { - segEnd = intervalEnd.clone(); - isEnd = false; - } - - return { - start: segStart, - end: segEnd, - isStart: isStart, - isEnd: isEnd - }; - } -} - - -function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object - obj = obj || {}; - if (obj[name] !== undefined) { - return obj[name]; - } - var parts = name.split(/(?=[A-Z])/), - i = parts.length - 1, res; - for (; i>=0; i--) { - res = obj[parts[i].toLowerCase()]; - if (res !== undefined) { - return res; - } - } - return obj['default']; -} - - -/* Date Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; - - -// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. -// Moments will have their timezones normalized. -function dayishDiff(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), - ms: a.time() - b.time() - }); -} - - -function isNativeDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; -} - - -// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" -function isTimeString(str) { - return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); -} - - -/* General Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -fc.applyAll = applyAll; // export - - -// Create an object that has the given prototype. Just like Object.create -function createObject(proto) { - var f = function() {}; - f.prototype = proto; - return new f(); -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '
'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -function capitaliseFirstLetter(str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -function compareNumbers(a, b) { // for .sort() - return a - b; -} - - -// Returns a function, that, as long as it continues to be invoked, will not -// be triggered. The function will be called after it stops being called for -// N milliseconds. -// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 -function debounce(func, wait) { - var timeoutId; - var args; - var context; - var timestamp; // of most recent call - var later = function() { - var last = +new Date() - timestamp; - if (last < wait && last > 0) { - timeoutId = setTimeout(later, wait - last); - } - else { - timeoutId = null; - func.apply(context, args); - if (!timeoutId) { - context = args = null; - } - } - }; - - return function() { - context = this; - args = arguments; - timestamp = +new Date(); - if (!timeoutId) { - timeoutId = setTimeout(later, wait); - } - }; -} - -;; - -var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; -var ambigTimeOrZoneRegex = - /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; -var newMomentProto = moment.fn; // where we will attach our new methods -var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods -var allowValueOptimization; -var setUTCValues; // function defined below -var setLocalValues; // function defined below - - -// Creating -// ------------------------------------------------------------------------------------------------- - -// Creates a new moment, similar to the vanilla moment(...) constructor, but with -// extra features (ambiguous time, enhanced formatting). When given an existing moment, -// it will function as a clone (and retain the zone of the moment). Anything else will -// result in a moment in the local zone. -fc.moment = function() { - return makeMoment(arguments); -}; - -// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. -fc.moment.utc = function() { - var mom = makeMoment(arguments, true); - - // Force it into UTC because makeMoment doesn't guarantee it - // (if given a pre-existing moment for example) - if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone - mom.utc(); - } - - return mom; -}; - -// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. -// ISO8601 strings with no timezone offset will become ambiguously zoned. -fc.moment.parseZone = function() { - return makeMoment(arguments, true, true); -}; - -// Builds an enhanced moment from args. When given an existing moment, it clones. When given a -// native Date, or called with no arguments (the current time), the resulting moment will be local. -// Anything else needs to be "parsed" (a string or an array), and will be affected by: -// parseAsUTC - if there is no zone information, should we parse the input in UTC? -// parseZone - if there is zone information, should we force the zone of the moment? -function makeMoment(args, parseAsUTC, parseZone) { - var input = args[0]; - var isSingleString = args.length == 1 && typeof input === 'string'; - var isAmbigTime; - var isAmbigZone; - var ambigMatch; - var mom; - - if (moment.isMoment(input)) { - mom = moment.apply(null, args); // clone it - transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone - } - else if (isNativeDate(input) || input === undefined) { - mom = moment.apply(null, args); // will be local - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC) { - mom = moment.utc.apply(moment, args); - } - else { - mom = moment.apply(null, args); - } - - if (isAmbigTime) { - mom._ambigTime = true; - mom._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - mom._ambigZone = true; - } - else if (isSingleString) { - mom.zone(input); // if not a valid zone, will assign UTC - } - } - } - - mom._fullCalendar = true; // flag for extended functionality - - return mom; -} - - -// A clone method that works with the flags related to our enhanced functionality. -// In the future, use moment.momentProperties -newMomentProto.clone = function() { - var mom = oldMomentProto.clone.apply(this, arguments); - - // these flags weren't transfered with the clone - transferAmbigs(this, mom); - if (this._fullCalendar) { - mom._fullCalendar = true; - } - - return mom; -}; - - -// Time-of-day -// ------------------------------------------------------------------------------------------------- - -// GETTER -// Returns a Duration with the hours/minutes/seconds/ms values of the moment. -// If the moment has an ambiguous time, a duration of 00:00 will be returned. -// -// SETTER -// You can supply a Duration, a Moment, or a Duration-like argument. -// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. -newMomentProto.time = function(time) { - - // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. - // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. - if (!this._fullCalendar) { - return oldMomentProto.time.apply(this, arguments); - } - - if (time == null) { // getter - return moment.duration({ - hours: this.hours(), - minutes: this.minutes(), - seconds: this.seconds(), - milliseconds: this.milliseconds() - }); - } - else { // setter - - this._ambigTime = false; // mark that the moment now has a time - - if (!moment.isDuration(time) && !moment.isMoment(time)) { - time = moment.duration(time); - } - - // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). - // Only for Duration times, not Moment times. - var dayHours = 0; - if (moment.isDuration(time)) { - dayHours = Math.floor(time.asDays()) * 24; - } - - // We need to set the individual fields. - // Can't use startOf('day') then add duration. In case of DST at start of day. - return this.hours(dayHours + time.hours()) - .minutes(time.minutes()) - .seconds(time.seconds()) - .milliseconds(time.milliseconds()); - } -}; - -// Converts the moment to UTC, stripping out its time-of-day and timezone offset, -// but preserving its YMD. A moment with a stripped time will display no time -// nor timezone offset when .format() is called. -newMomentProto.stripTime = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - - this.utc(); // set the internal UTC flag (will clear the ambig flags) - setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), - // which clears all ambig flags. Same with setUTCValues with moment-timezone. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -newMomentProto.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -newMomentProto.stripZone = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - var wasAmbigTime = this._ambigTime; - - this.utc(); // set the internal UTC flag (will clear the ambig flags) - setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms - - if (wasAmbigTime) { - // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign - this._ambigTime = true; - } - - // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), - // which clears all ambig flags. Same with setUTCValues with moment-timezone. - this._ambigZone = true; - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -newMomentProto.hasZone = function() { - return !this._ambigZone; -}; - -// this method implicitly marks a zone (will get called upon .utc() and .local()) -newMomentProto.zone = function(tzo) { - - if (tzo != null) { // setter - // these assignments needs to happen before the original zone method is called. - // I forget why, something to do with a browser crash. - this._ambigTime = false; - this._ambigZone = false; - } - - return oldMomentProto.zone.apply(this, arguments); -}; - -// this method implicitly marks a zone -newMomentProto.local = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array - var wasAmbigZone = this._ambigZone; - - oldMomentProto.local.apply(this, arguments); // will clear ambig flags - - if (wasAmbigZone) { - // If the moment was ambiguously zoned, the date fields were stored as UTC. - // We want to preserve these, but in local time. - setLocalValues(this, a); - } - - return this; // for chaining -}; - - -// Formatting -// ------------------------------------------------------------------------------------------------- - -newMomentProto.format = function() { - if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? - return formatDate(this, arguments[0]); // our extended formatting - } - if (this._ambigTime) { - return oldMomentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return oldMomentProto.format.apply(this, arguments); -}; - -newMomentProto.toISOString = function() { - if (this._ambigTime) { - return oldMomentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return oldMomentProto.toISOString.apply(this, arguments); -}; - - -// Querying -// ------------------------------------------------------------------------------------------------- - -// Is the moment within the specified range? `end` is exclusive. -// FYI, this method is not a standard Moment method, so always do our enhanced logic. -newMomentProto.isWithin = function(start, end) { - var a = commonlyAmbiguate([ this, start, end ]); - return a[0] >= a[1] && a[0] < a[2]; -}; - -// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. -// If no units specified, the two moments must be identically the same, with matching ambig flags. -newMomentProto.isSame = function(input, units) { - var a; - - // only do custom logic if this is an enhanced moment - if (!this._fullCalendar) { - return oldMomentProto.isSame.apply(this, arguments); - } - - if (units) { - a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times - return oldMomentProto.isSame.call(a[0], a[1], units); - } - else { - input = fc.moment.parseZone(input); // normalize input - return oldMomentProto.isSame.call(this, input) && - Boolean(this._ambigTime) === Boolean(input._ambigTime) && - Boolean(this._ambigZone) === Boolean(input._ambigZone); - } -}; - -// Make these query methods work with ambiguous moments -$.each([ - 'isBefore', - 'isAfter' -], function(i, methodName) { - newMomentProto[methodName] = function(input, units) { - var a; - - // only do custom logic if this is an enhanced moment - if (!this._fullCalendar) { - return oldMomentProto[methodName].apply(this, arguments); - } - - a = commonlyAmbiguate([ this, input ]); - return oldMomentProto[methodName].call(a[0], a[1], units); - }; -}); - - -// Misc Internals -// ------------------------------------------------------------------------------------------------- - -// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. -// for example, of one moment has ambig time, but not others, all moments will have their time stripped. -// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. -function commonlyAmbiguate(inputs, preserveTime) { - var outputs = []; - var anyAmbigTime = false; - var anyAmbigZone = false; - var i; - - for (i=0; i "MMMM D YYYY" - formatStr = localeData.longDateFormat(formatStr) || formatStr; - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - - separator = separator || ' - '; - - return formatRangeWithChunks( - date1, - date2, - getFormatStringChunks(formatStr), - separator, - isRTL - ); -} -fc.formatRange = formatRange; // expose - - -function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { - var chunkStr; // the rendering of the chunk - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for (leftI=0; leftIleftI; rightI--) { - chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); - if (chunkStr === false) { - break; - } - rightStr = chunkStr + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI=leftI; middleI<=rightI; middleI++) { - middleStr1 += formatDateWithChunk(date1, chunks[middleI]); - middleStr2 += formatDateWithChunk(date2, chunks[middleI]); - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return leftStr + middleStr + rightStr; -} - - -var similarUnitMap = { - Y: 'year', - M: 'month', - D: 'day', // day of month - d: 'day', // day of week - // prevents a separator between anything time-related... - A: 'second', // AM/PM - a: 'second', // am/pm - T: 'second', // A/P - t: 'second', // a/p - H: 'second', // hour (24) - h: 'second', // hour (12) - m: 'second', // minute - s: 'second' // second -}; -// TODO: week maybe? - - -// Given a formatting chunk, and given that both dates are similar in the regard the -// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. -function formatSimilarChunk(date1, date2, chunk) { - var token; - var unit; - - if (typeof chunk === 'string') { // a literal string - return chunk; - } - else if ((token = chunk.token)) { - unit = similarUnitMap[token.charAt(0)]; - // are the dates the same for this unit of measurement? - if (unit && date1.isSame(date2, unit)) { - return oldMomentFormat(date1, token); // would be the same if we used `date2` - // BTW, don't support custom tokens - } - } - - return false; // the chunk is NOT the same for the two dates - // BTW, don't support splitting on non-zero areas -} - - -// Chunking Utils -// ------------------------------------------------------------------------------------------------- - - -var formatStringChunkCache = {}; - - -function getFormatStringChunks(formatStr) { - if (formatStr in formatStringChunkCache) { - return formatStringChunkCache[formatStr]; - } - return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); -} - - -// Break the formatting string into an array of chunks -function chunkFormatString(formatStr) { - var chunks = []; - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination - var match; - - while ((match = chunker.exec(formatStr))) { - if (match[1]) { // a literal string inside [ ... ] - chunks.push(match[1]); - } - else if (match[2]) { // non-zero formatting inside ( ... ) - chunks.push({ maybe: chunkFormatString(match[2]) }); - } - else if (match[3]) { // a formatting token - chunks.push({ token: match[3] }); - } - else if (match[5]) { // an unenclosed literal string - chunks.push(match[5]); - } - } - - return chunks; -} - -;; - -/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------- -Options: - - className (string) - - content (HTML string or jQuery element set) - - parentEl - - top - - left - - right (the x coord of where the right edge should be. not a "CSS" right) - - autoHide (boolean) - - show (callback) - - hide (callback) -*/ - -function Popover(options) { - this.options = options || {}; -} - - -Popover.prototype = { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - documentMousedownProxy: null, // document mousedown handler bound to `this` - margin: 10, // the space required between the popover and the edges of the scroll container - - - // Shows the popover on the specified position. Renders it if not already - show: function() { - if (this.isHidden) { - if (!this.el) { - this.render(); - } - this.el.show(); - this.position(); - this.isHidden = false; - this.trigger('show'); - } - }, - - - // Hides the popover, through CSS, but does not remove it from the DOM - hide: function() { - if (!this.isHidden) { - this.el.hide(); - this.isHidden = true; - this.trigger('hide'); - } - }, - - - // Creates `this.el` and renders content inside of it - render: function() { - var _this = this; - var options = this.options; - - this.el = $('
') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); - } - }, - - - // Triggered when the user clicks *anywhere* in the document, for the autoHide feature - documentMousedown: function(ev) { - // only hide the popover if the click happened outside the popover - if (this.el && !$(ev.target).closest(this.el).length) { - this.hide(); - } - }, - - - // Hides and unregisters any handlers - destroy: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - $(document).off('mousedown', this.documentMousedownProxy); - }, - - - // Positions the popover optimally, using the top/left/right options - position: function() { - var options = this.options; - var origin = this.el.offsetParent().offset(); - var width = this.el.outerWidth(); - var height = this.el.outerHeight(); - var windowEl = $(window); - var viewportEl = getScrollParent(this.el); - var viewportTop; - var viewportLeft; - var viewportOffset; - var top; // the "position" (not "offset") values for the popover - var left; // - - // compute top and left - top = options.top || 0; - if (options.left !== undefined) { - left = options.left; - } - else if (options.right !== undefined) { - left = options.right - width; // derive the left value from the right value - } - else { - left = 0; - } - - if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result - viewportEl = windowEl; - viewportTop = 0; // the window is always at the top left - viewportLeft = 0; // (and .offset() won't work if called here) - } - else { - viewportOffset = viewportEl.offset(); - viewportTop = viewportOffset.top; - viewportLeft = viewportOffset.left; - } - - // if the window is scrolled, it causes the visible area to be further down - viewportTop += windowEl.scrollTop(); - viewportLeft += windowEl.scrollLeft(); - - // constrain to the view port. if constrained by two edges, give precedence to top/left - if (options.viewportConstrain !== false) { - top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); - top = Math.max(top, viewportTop + this.margin); - left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); - left = Math.max(left, viewportLeft + this.margin); - } - - this.el.css({ - top: top - origin.top, - left: left - origin.left - }); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - // TODO: better code reuse for this. Repeat code - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - -}; - -;; - -/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date ------------------------------------------------------------------------------------------------------------------------- -Common interface: - - CoordMap.prototype = { - build: function() {}, - getCell: function(x, y) {} - }; - -*/ - -/* Coordinate map for a grid component -----------------------------------------------------------------------------------------------------------------------*/ - -function GridCoordMap(grid) { - this.grid = grid; -} - - -GridCoordMap.prototype = { - - grid: null, // reference to the Grid - rows: null, // the top-to-bottom y coordinates. including the bottom of the last item - cols: null, // the left-to-right x coordinates. including the right of the last item - - containerEl: null, // container element that all coordinates are constrained to. optionally assigned - minX: null, - maxX: null, // exclusive - minY: null, - maxY: null, // exclusive - - - // Queries the grid for the coordinates of all the cells - build: function() { - this.grid.buildCoords( - this.rows = [], - this.cols = [] - ); - this.computeBounds(); - }, - - - // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null - getCell: function(x, y) { - var cell = null; - var rows = this.rows; - var cols = this.cols; - var r = -1; - var c = -1; - var i; - - if (this.inBounds(x, y)) { - - for (i = 0; i < rows.length; i++) { - if (y >= rows[i][0] && y < rows[i][1]) { - r = i; - break; - } - } - - for (i = 0; i < cols.length; i++) { - if (x >= cols[i][0] && x < cols[i][1]) { - c = i; - break; - } - } - - if (r >= 0 && c >= 0) { - cell = { row: r, col: c }; - cell.grid = this.grid; - cell.date = this.grid.getCellDate(cell); - } - } - - return cell; - }, - - - // If there is a containerEl, compute the bounds into min/max values - computeBounds: function() { - var containerOffset; - - if (this.containerEl) { - containerOffset = this.containerEl.offset(); - this.minX = containerOffset.left; - this.maxX = containerOffset.left + this.containerEl.outerWidth(); - this.minY = containerOffset.top; - this.maxY = containerOffset.top + this.containerEl.outerHeight(); - } - }, - - - // Determines if the given coordinates are in bounds. If no `containerEl`, always true - inBounds: function(x, y) { - if (this.containerEl) { - return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; - } - return true; - } - -}; - - -/* Coordinate map that is a combination of multiple other coordinate maps -----------------------------------------------------------------------------------------------------------------------*/ - -function ComboCoordMap(coordMaps) { - this.coordMaps = coordMaps; -} - - -ComboCoordMap.prototype = { - - coordMaps: null, // an array of CoordMaps - - - // Builds all coordMaps - build: function() { - var coordMaps = this.coordMaps; - var i; - - for (i = 0; i < coordMaps.length; i++) { - coordMaps[i].build(); - } - }, - - - // Queries all coordMaps for the cell underneath the given coordinates, returning the first result - getCell: function(x, y) { - var coordMaps = this.coordMaps; - var cell = null; - var i; - - for (i = 0; i < coordMaps.length && !cell; i++) { - cell = coordMaps[i].getCell(x, y); - } - - return cell; - } - -}; - -;; - -/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup) - -function DragListener(coordMap, options) { - this.coordMap = coordMap; - this.options = options || {}; -} - - -DragListener.prototype = { - - coordMap: null, - options: null, - - isListening: false, - isDragging: false, - - // the cell/date the mouse was over when listening started - origCell: null, - origDate: null, - - // the cell/date the mouse is over - cell: null, - date: null, - - // coordinates of the initial mousedown - mouseX0: null, - mouseY0: null, - - // handler attached to the document, bound to the DragListener's `this` - mousemoveProxy: null, - mouseupProxy: null, - - scrollEl: null, - scrollBounds: null, // { top, bottom, left, right } - scrollTopVel: null, // pixels per second - scrollLeftVel: null, // pixels per second - scrollIntervalId: null, // ID of setTimeout for scrolling animation loop - scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled - - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - // Call this when the user does a mousedown. Will probably lead to startListening - mousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - - ev.preventDefault(); // prevents native selection in most browsers - - this.startListening(ev); - - // start the drag immediately if there is no minimum distance for a drag start - if (!this.options.distance) { - this.startDrag(ev); - } - } - }, - - - // Call this to start tracking mouse movements - startListening: function(ev) { - var scrollParent; - var cell; - - if (!this.isListening) { - - // grab scroll container and attach handler - if (ev && this.options.scroll) { - scrollParent = getScrollParent($(ev.target)); - if (!scrollParent.is(window) && !scrollParent.is(document)) { - this.scrollEl = scrollParent; - - // scope to `this`, and use `debounce` to make sure rapid calls don't happen - this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); - this.scrollEl.on('scroll', this.scrollHandlerProxy); - } - } - - this.computeCoords(); // relies on `scrollEl` - - // get info on the initial cell, date, and coordinates - if (ev) { - cell = this.getCell(ev); - this.origCell = cell; - this.origDate = cell ? cell.date : null; - - this.mouseX0 = ev.pageX; - this.mouseY0 = ev.pageY; - } - - $(document) - .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) - .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) - .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 - - this.isListening = true; - this.trigger('listenStart', ev); - } - }, - - - // Recomputes the drag-critical positions of elements - computeCoords: function() { - this.coordMap.build(); - this.computeScrollBounds(); - }, - - - // Called when the user moves the mouse - mousemove: function(ev) { - var minDistance; - var distanceSq; // current distance from mouseX0/mouseY0, squared - - if (!this.isDragging) { // if not already dragging... - // then start the drag if the minimum distance criteria is met - minDistance = this.options.distance || 1; - distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.startDrag(ev); - } - } - - if (this.isDragging) { - this.drag(ev); // report a drag, even if this mousemove initiated the drag - } - }, - - - // Call this to initiate a legitimate drag. - // This function is called internally from this class, but can also be called explicitly from outside - startDrag: function(ev) { - var cell; - - if (!this.isListening) { // startDrag must have manually initiated - this.startListening(); - } - - if (!this.isDragging) { - this.isDragging = true; - this.trigger('dragStart', ev); - - // report the initial cell the mouse is over - cell = this.getCell(ev); - if (cell) { - this.cellOver(cell, true); - } - } - }, - - - // Called while the mouse is being moved and when we know a legitimate drag is taking place - drag: function(ev) { - var cell; - - if (this.isDragging) { - cell = this.getCell(ev); - - if (!isCellsEqual(cell, this.cell)) { // a different cell than before? - if (this.cell) { - this.cellOut(); - } - if (cell) { - this.cellOver(cell); - } - } - - this.dragScroll(ev); // will possibly cause scrolling - } - }, - - - // Called when a the mouse has just moved over a new cell - cellOver: function(cell) { - this.cell = cell; - this.date = cell.date; - this.trigger('cellOver', cell, cell.date); - }, - - - // Called when the mouse has just moved out of a cell - cellOut: function() { - if (this.cell) { - this.trigger('cellOut', this.cell); - this.cell = null; - this.date = null; - } - }, - - - // Called when the user does a mouseup - mouseup: function(ev) { - this.stopDrag(ev); - this.stopListening(ev); - }, - - - // Called when the drag is over. Will not cause listening to stop however. - // A concluding 'cellOut' event will NOT be triggered. - stopDrag: function(ev) { - if (this.isDragging) { - this.stopScrolling(); - this.trigger('dragStop', ev); - this.isDragging = false; - } - }, - - - // Call this to stop listening to the user's mouse events - stopListening: function(ev) { - if (this.isListening) { - - // remove the scroll handler if there is a scrollEl - if (this.scrollEl) { - this.scrollEl.off('scroll', this.scrollHandlerProxy); - this.scrollHandlerProxy = null; - } - - $(document) - .off('mousemove', this.mousemoveProxy) - .off('mouseup', this.mouseupProxy) - .off('selectstart', this.preventDefault); - - this.mousemoveProxy = null; - this.mouseupProxy = null; - - this.isListening = false; - this.trigger('listenStop', ev); - - this.origCell = this.cell = null; - this.origDate = this.date = null; - } - }, - - - // Gets the cell underneath the coordinates for the given mouse event - getCell: function(ev) { - return this.coordMap.getCell(ev.pageX, ev.pageY); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - }, - - - // Stops a given mouse event from doing it's native browser action. In our case, text selection. - preventDefault: function(ev) { - ev.preventDefault(); - }, - - - /* Scrolling - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - var el = this.scrollEl; - var offset; - - if (el) { - offset = el.offset(); - this.scrollBounds = { - top: offset.top, - left: offset.left, - bottom: offset.top + el.outerHeight(), - right: offset.left + el.outerWidth() - }; - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - dragScroll: function(ev) { - var sensitivity = this.scrollSensitivity; - var bounds = this.scrollBounds; - var topCloseness, bottomCloseness; - var leftCloseness, rightCloseness; - var topVel = 0; - var leftVel = 0; - - if (bounds) { // only scroll if scrollEl exists - - // compute closeness to edges. valid range is from 0.0 - 1.0 - topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; - leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; - - // translate vertical closeness into velocity. - // mouse must be completely in bounds for velocity to happen. - if (topCloseness >= 0 && topCloseness <= 1) { - topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up - } - else if (bottomCloseness >= 0 && bottomCloseness <= 1) { - topVel = bottomCloseness * this.scrollSpeed; - } - - // translate horizontal closeness into velocity - if (leftCloseness >= 0 && leftCloseness <= 1) { - leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left - } - else if (rightCloseness >= 0 && rightCloseness <= 1) { - leftVel = rightCloseness * this.scrollSpeed; - } - } - - this.setScrollVel(topVel, leftVel); - }, - - - // Sets the speed-of-scrolling for the scrollEl - setScrollVel: function(topVel, leftVel) { - - this.scrollTopVel = topVel; - this.scrollLeftVel = leftVel; - - this.constrainScrollVel(); // massages into realistic values - - // if there is non-zero velocity, and an animation loop hasn't already started, then START - if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { - this.scrollIntervalId = setInterval( - $.proxy(this, 'scrollIntervalFunc'), // scope to `this` - this.scrollIntervalMs - ); - } - }, - - - // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way - constrainScrollVel: function() { - var el = this.scrollEl; - - if (this.scrollTopVel < 0) { // scrolling up? - if (el.scrollTop() <= 0) { // already scrolled all the way up? - this.scrollTopVel = 0; - } - } - else if (this.scrollTopVel > 0) { // scrolling down? - if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? - this.scrollTopVel = 0; - } - } - - if (this.scrollLeftVel < 0) { // scrolling left? - if (el.scrollLeft() <= 0) { // already scrolled all the left? - this.scrollLeftVel = 0; - } - } - else if (this.scrollLeftVel > 0) { // scrolling right? - if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? - this.scrollLeftVel = 0; - } - } - }, - - - // This function gets called during every iteration of the scrolling animation loop - scrollIntervalFunc: function() { - var el = this.scrollEl; - var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by - - // change the value of scrollEl's scroll - if (this.scrollTopVel) { - el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); - } - if (this.scrollLeftVel) { - el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); - } - - this.constrainScrollVel(); // since the scroll values changed, recompute the velocities - - // if scrolled all the way, which causes the vels to be zero, stop the animation loop - if (!this.scrollTopVel && !this.scrollLeftVel) { - this.stopScrolling(); - } - }, - - - // Kills any existing scrolling animation loop - stopScrolling: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - // when all done with scrolling, recompute positions since they probably changed - this.computeCoords(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - scrollHandler: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.computeCoords(); - } - } - -}; - - -// Returns `true` if the cells are identically equal. `false` otherwise. -// They must have the same row, col, and be from the same grid. -// Two null values will be considered equal, as two "out of the grid" states are the same. -function isCellsEqual(cell1, cell2) { - - if (!cell1 && !cell2) { - return true; - } - - if (cell1 && cell2) { - return cell1.grid === cell2.grid && - cell1.row === cell2.row && - cell1.col === cell2.col; - } - - return false; -} - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -function MouseFollower(sourceEl, options) { - this.options = options = options || {}; - this.sourceEl = sourceEl; - this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent -} - - -MouseFollower.prototype = { - - options: null, - - sourceEl: null, // the element that will be cloned and made to look like it is dragging - el: null, // the clone of `sourceEl` that will track the mouse - parentEl: null, // the element that `el` (the clone) will be attached to - - // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl - top0: null, - left0: null, - - // the initial position of the mouse - mouseY0: null, - mouseX0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` - - isFollowing: false, - isHidden: false, - isAnimating: false, // doing the revert animation? - - - // Causes the element to start following the mouse - start: function(ev) { - if (!this.isFollowing) { - this.isFollowing = true; - - this.mouseY0 = ev.pageY; - this.mouseX0 = ev.pageX; - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); - } - }, - - - // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. - // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. - stop: function(shouldRevert, callback) { - var _this = this; - var revertDuration = this.options.revertDuration; - - function complete() { - this.isAnimating = false; - _this.destroyEl(); - - this.top0 = this.left0 = null; // reset state for future updatePosition calls - - if (callback) { - callback(); - } - } - - if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time - this.isFollowing = false; - - $(document).off('mousemove', this.mousemoveProxy); - - if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? - this.isAnimating = true; - this.el.animate({ - top: this.top0, - left: this.left0 - }, { - duration: revertDuration, - complete: complete - }); - } - else { - complete(); - } - } - }, - - - // Gets the tracking element. Create it if necessary - getEl: function() { - var el = this.el; - - if (!el) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - el = this.el = this.sourceEl.clone() - .css({ - position: 'absolute', - visibility: '', // in case original element was hidden (commonly through hideEvents()) - display: this.isHidden ? 'none' : '', // for when initially hidden - margin: 0, - right: 'auto', // erase and set width instead - bottom: 'auto', // erase and set height instead - width: this.sourceEl.width(), // explicit height in case there was a 'right' value - height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value - opacity: this.options.opacity || '', - zIndex: this.options.zIndex - }) - .appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - destroyEl: function() { - if (this.el) { - this.el.remove(); - this.el = null; - } - }, - - - // Update the CSS position of the tracking element - updatePosition: function() { - var sourceOffset; - var origin; - - this.getEl(); // ensure this.el - - // make sure origin info was computed - if (this.top0 === null) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - sourceOffset = this.sourceEl.offset(); - origin = this.el.offsetParent().offset(); - this.top0 = sourceOffset.top - origin.top; - this.left0 = sourceOffset.left - origin.left; - } - - this.el.css({ - top: this.top0 + this.topDelta, - left: this.left0 + this.leftDelta - }); - }, - - - // Gets called when the user moves the mouse - mousemove: function(ev) { - this.topDelta = ev.pageY - this.mouseY0; - this.leftDelta = ev.pageX - this.mouseX0; - - if (!this.isHidden) { - this.updatePosition(); - } - }, - - - // Temporarily makes the tracking element invisible. Can be called before following starts - hide: function() { - if (!this.isHidden) { - this.isHidden = true; - if (this.el) { - this.el.hide(); - } - } - }, - - - // Show the tracking element after it has been temporarily hidden - show: function() { - if (this.isHidden) { - this.isHidden = false; - this.updatePosition(); - this.getEl().show(); - } - } - -}; - -;; - -/* A utility class for rendering rows. -----------------------------------------------------------------------------------------------------------------------*/ -// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" -// (such as highlight rows, day rows, helper rows, etc). - -function RowRenderer(view) { - this.view = view; -} - - -RowRenderer.prototype = { - - view: null, // a View object - cellHtml: '', // plain default HTML used for a cell when no other is available - - - // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. - // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. - // `row` is an optional row number. - rowHtml: function(rowType, row) { - var view = this.view; - var renderCell = this.getHtmlRenderer('cell', rowType); - var cellHtml = ''; - var col; - var date; - - row = row || 0; - - for (col = 0; col < view.colCnt; col++) { - date = view.cellToDate(row, col); - cellHtml += renderCell(row, col, date); - } - - cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro - - return '' + cellHtml + ''; - }, - - - // Applies the "intro" and "outro" HTML to the given cells. - // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. - // `cells` can be an HTML string of 's or a jQuery element - // `row` is an optional row number. - bookendCells: function(cells, rowType, row) { - var view = this.view; - var intro = this.getHtmlRenderer('intro', rowType)(row || 0); - var outro = this.getHtmlRenderer('outro', rowType)(row || 0); - var isRTL = view.opt('isRTL'); - var prependHtml = isRTL ? outro : intro; - var appendHtml = isRTL ? intro : outro; - - if (typeof cells === 'string') { - return prependHtml + cells + appendHtml; - } - else { // a jQuery element - return cells.prepend(prependHtml).append(appendHtml); - } - }, - - - // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific - // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. - // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. - // We will query the View object first for any custom rendering functions, then the methods of the subclass. - getHtmlRenderer: function(rendererName, rowType) { - var view = this.view; - var generalName; // like "cellHtml" - var specificName; // like "dayCellHtml". based on rowType - var provider; // either the View or the RowRenderer subclass, whichever provided the method - var renderer; - - generalName = rendererName + 'Html'; - if (rowType) { - specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; - } - - if (specificName && (renderer = view[specificName])) { - provider = view; - } - else if (specificName && (renderer = this[specificName])) { - provider = this; - } - else if ((renderer = view[generalName])) { - provider = view; - } - else if ((renderer = this[generalName])) { - provider = this; - } - - if (typeof renderer === 'function') { - return function() { - return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string - }; - } - - // the rendered can be a plain string as well. if not specified, always an empty string. - return function() { - return renderer || ''; - }; - } - -}; - -;; - -/* An abstract class comprised of a "grid" of cells that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -function Grid(view) { - RowRenderer.call(this, view); // call the super-constructor - this.coordMap = new GridCoordMap(this); - this.elsByFill = {}; -} - - -Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class -$.extend(Grid.prototype, { - - el: null, // the containing element - coordMap: null, // a GridCoordMap that converts pixel values to datetimes - cellDuration: null, // a cell's duration. subclasses must assign this ASAP - elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. - - - // Renders the grid into the `el` element. - // Subclasses should override and call this super-method when done. - render: function() { - this.bindHandlers(); - }, - - - // Called when the grid's resources need to be cleaned up - destroy: function() { - // subclasses can implement - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the given empty arrays with the y and x coordinates of the cells - buildCoords: function(rows, cols) { - // subclasses must implement - }, - - - // Given a cell object, returns the date for that cell - getCellDate: function(cell) { - // subclasses must implement - }, - - - // Given a cell object, returns the element that represents the cell's whole-day - getCellDayEl: function(cell) { - // subclasses must implement - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - // subclasses must implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attach handlers to `this.el`, using bubbling to listen to all ancestors. - // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the - // DOM and jQuery will be smart enough to garbage collect the handlers. - bindHandlers: function() { - var _this = this; - - this.el.on('mousedown', function(ev) { - if ( - !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link - !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) - ) { - _this.dayMousedown(ev); - } - }); - - this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var isSelectable = view.opt('selectable'); - var dates = null; // the inclusive dates of the selection. will be null if no selection - var start; // the inclusive start of the selection - var end; // the *exclusive* end of the selection - var dayEl; - - // this listener tracks a mousedown on a day element, and a subsequent drag. - // if the drag ends on the same day, it is a 'dayClick'. - // if 'selectable' is enabled, this listener also detects selections. - var dragListener = new DragListener(this.coordMap, { - //distance: 5, // needs more work if we want dayClick to fire correctly - scroll: view.opt('dragScroll'), - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - cellOver: function(cell, date) { - if (dragListener.origDate) { // click needs to have started on a cell - - dayEl = _this.getCellDayEl(cell); - - dates = [ date, dragListener.origDate ].sort(compareNumbers); // works with Moments - start = dates[0]; - end = dates[1].clone().add(_this.cellDuration); - - if (isSelectable) { - if (calendar.isSelectionAllowedInRange(start, end)) { // allowed to select within this range? - _this.renderSelection(start, end); - } - else { - dates = null; // flag for an invalid selection - disableCursor(); - } - } - } - }, - cellOut: function(cell, date) { - dates = null; - _this.destroySelection(); - enableCursor(); - }, - listenStop: function(ev) { - if (dates) { // started and ended on a cell? - if (dates[0].isSame(dates[1])) { - view.trigger('dayClick', dayEl[0], start, ev); - } - if (isSelectable) { - // the selection will already have been rendered. just report it - view.reportSelection(start, end, ev); - } - } - enableCursor(); - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a event being dragged over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being dragged - destroyDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized. - // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. - renderResize: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - destroyResize: function() { - // subclasses must implement - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock event over the given date(s). - // `end` can be null, in which case the mock event that is rendered will have a null end time. - // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - renderRangeHelper: function(start, end, sourceSeg) { - var view = this.view; - var fakeEvent; - - // compute the end time if forced to do so (this is what EventManager does) - if (!end && view.opt('forceEventDuration')) { - end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); - } - - fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - fakeEvent.start = start; - fakeEvent.end = end; - fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay - - // this extra className will be useful for differentiating real events from mock events in CSS - fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - - // if something external is being dragged in, don't render a resizer - if (!sourceSeg) { - fakeEvent.editable = false; - } - - this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Renders a mock event - renderHelper: function(event, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - destroyHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - renderSelection: function(start, end) { - this.renderHighlight(start, end); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - destroySelection: function() { - this.destroyHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. - renderHighlight: function(start, end) { - this.renderFill('highlight', this.rangeToSegs(start, end)); - }, - - - // Unrenders the emphasis on a date range - destroyHighlight: function() { - this.destroyFill('highlight'); - }, - - - // Generates an array of classNames for rendering the highlight. Used by the fill system. - highlightSegClasses: function() { - return [ 'fc-highlight' ]; - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a set of rectangles over the given segments of time. - // Returns a subset of segs, the segs that were actually rendered. - // Responsible for populating this.elsByFill - renderFill: function(type, segs) { - // subclasses must implement - }, - - - // Unrenders a specific type of fill that is currently rendered on the grid - destroyFill: function(type) { - var el = this.elsByFill[type]; - - if (el) { - el.remove(); - delete this.elsByFill[type]; - } - }, - - - // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. - // Only returns segments that successfully rendered. - // To be harnessed by renderFill (implemented by subclasses). - // Analagous to renderFgSegEls. - renderFillSegEls: function(type, segs) { - var _this = this; - var segElMethod = this[type + 'SegEl']; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { - - // build a large concatenation of segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fillSegHtml(type, segs[i]); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = $(node); - - // allow custom filter methods per-type - if (segElMethod) { - el = segElMethod.call(_this, seg, el); - } - - if (el) { // custom filters did not cancel the render - el = $(el); // allow custom filter to return raw DOM node - - // correct element type? (would be bad if a non-TD were inserted into a table for example) - if (el.is(_this.fillSegTag)) { - seg.el = el; - renderedSegs.push(seg); - } - } - }); - } - - return renderedSegs; - }, - - - fillSegTag: 'div', // subclasses can override - - - // Builds the HTML needed for one fill segment. Generic enought o work with different types. - fillSegHtml: function(type, seg) { - var classesMethod = this[type + 'SegClasses']; // custom hooks per-type - var stylesMethod = this[type + 'SegStyles']; // - var classes = classesMethod ? classesMethod.call(this, seg) : []; - var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string - - return '<' + this.fillSegTag + - (classes.length ? ' class="' + classes.join(' ') + '"' : '') + - (styles ? ' style="' + styles + '"' : '') + - ' />'; - }, - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a day-of-week header row - headHtml: function() { - return '' + - '
' + - '' + - '' + - this.rowHtml('head') + // leverages RowRenderer - '' + - '
' + - '
'; - }, - - - // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell - headCellHtml: function(row, col, date) { - var view = this.view; - var calendar = view.calendar; - var colFormat = view.opt('columnFormat'); - - return '' + - '' + - htmlEscape(calendar.formatDate(date, colFormat)) + - ''; - }, - - - // Renders the HTML for a single-day background cell - bgCellHtml: function(row, col, date) { - var view = this.view; - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return ''; - }, - - - // Computes HTML classNames for a single-day cell - getDayClasses: function(date) { - var view = this.view; - var today = view.calendar.getNow().stripTime(); - var classes = [ 'fc-' + dayIDs[date.day()] ]; - - if ( - view.name === 'month' && - date.month() != view.intervalStart.month() - ) { - classes.push('fc-other-month'); - } - - if (date.isSame(today, 'day')) { - classes.push( - 'fc-today', - view.highlightStateClass - ); - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - - return classes; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(Grid.prototype, { - - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - segs: null, // the event segments currently rendered in the grid - - - // Renders the given events onto the grid - renderEvents: function(events) { - var segs = this.eventsToSegs(events); - var bgSegs = []; - var fgSegs = []; - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - if (isBgEvent(seg.event)) { - bgSegs.push(seg); - } - else { - fgSegs.push(seg); - } - } - - // Render each different type of segment. - // Each function may return a subset of the segs, segs that were actually rendered. - bgSegs = this.renderBgSegs(bgSegs) || bgSegs; - fgSegs = this.renderFgSegs(fgSegs) || fgSegs; - - this.segs = bgSegs.concat(fgSegs); - }, - - - // Unrenders all events currently rendered on the grid - destroyEvents: function() { - this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - - this.destroyFgSegs(); - this.destroyBgSegs(); - - this.segs = null; - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getSegs: function() { - return this.segs || []; - }, - - - /* Foreground Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. - renderFgSegs: function(segs) { - // subclasses must implement - }, - - - // Unrenders all currently rendered foreground segments - destroyFgSegs: function() { - // subclasses must implement - }, - - - // Renders and assigns an `el` property for each foreground event segment. - // Only returns segments that successfully rendered. - // A utility that subclasses may use. - renderFgSegEls: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { // don't build an empty html string - - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fgSegHtml(segs[i], disableResizing); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - } - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() - fgSegHtml: function(seg, disableResizing) { - // subclasses should implement - }, - - - /* Background Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given background event segments onto the grid. - // Returns a subset of the segs that were actually rendered. - renderBgSegs: function(segs) { - return this.renderFill('bgEvent', segs); - }, - - - // Unrenders all the currently rendered background event segments - destroyBgSegs: function() { - this.destroyFill('bgEvent'); - }, - - - // Renders a background event element, given the default rendering. Called by the fill system. - bgEventSegEl: function(seg, el) { - return this.view.resolveEventEl(seg.event, el); // will filter through eventRender - }, - - - // Generates an array of classNames to be used for the default rendering of a background event. - // Called by the fill system. - bgEventSegClasses: function(seg) { - var event = seg.event; - var source = event.source || {}; - - return [ 'fc-bgevent' ].concat( - event.className, - source.className || [] - ); - }, - - - // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. - // Called by the fill system. - // TODO: consolidate with getEventSkinCss? - bgEventSegStyles: function(seg) { - var view = this.view; - var event = seg.event; - var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - - if (backgroundColor) { - return 'background-color:' + backgroundColor; - } - - return ''; - }, - - - // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. - businessHoursSegClasses: function(seg) { - return [ 'fc-nonbusiness', 'fc-bgevent' ]; - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers to the container element and leverage bubbling - bindSegHandlers: function() { - var _this = this; - var view = this.view; - - $.each( - { - mouseenter: function(seg, ev) { - _this.triggerSegMouseover(seg, ev); - }, - mouseleave: function(seg, ev) { - _this.triggerSegMouseout(seg, ev); - }, - click: function(seg, ev) { - return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel - }, - mousedown: function(seg, ev) { - if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { - _this.segResizeMousedown(seg, ev); - } - else if (view.isEventDraggable(seg.event)) { - _this.segDragMousedown(seg, ev); - } - } - }, - function(name, func) { - // attach the handler to the container element and only listen for real event elements via bubbling - _this.el.on(name, '.fc-event-container > *', function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents - - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return func.call(this, seg, ev); // `this` will be the event element - } - }); - } - ); - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - triggerSegMouseover: function(seg, ev) { - if (!this.mousedOverSeg) { - this.mousedOverSeg = seg; - this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - triggerSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - /* Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event, which might lead to dragging. - // Generic enough to work with any type of Grid. - segDragMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var newStart, newEnd; - - // A clone of the original element that will move with the mouse - var mouseFollower = new MouseFollower(seg.el, { - parentEl: view.el, - opacity: view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = new DragListener(view.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - listenStart: function(ev) { - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isDraggingSeg = true; - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - var origDate = seg.cellDate || dragListener.origDate; - var res = _this.computeDraggedEventDates(seg, origDate, date); - newStart = res.start; - newEnd = res.end; - - if (calendar.isEventAllowedInRange(event, newStart, res.visibleEnd)) { // allowed to drop here? - if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication - mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); - } - } - else { - // have the helper follow the mouse (no snapping) with a warning-style cursor - newStart = null; // mark an invalid drop date - mouseFollower.show(); - disableCursor(); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newStart = null; - view.destroyDrag(); // unrender whatever was done in view.renderDrag - mouseFollower.show(); // show in case we are moving out of all cells - enableCursor(); - }, - dragStop: function(ev) { - var hasChanged = newStart && !newStart.isSame(event.start); - - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!hasChanged, function() { - _this.isDraggingSeg = false; - view.destroyDrag(); - view.showEvent(event); - view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (hasChanged) { - view.eventDrop(el[0], event, newStart, ev); // will rerender all events... - } - }); - - enableCursor(); - }, - listenStop: function() { - mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates. - // Might return a `null` end (even when forceEventDuration is on). - computeDraggedEventDates: function(seg, dragStartDate, dropDate) { - var view = this.view; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var delta; - var newStart; - var newEnd; - var newAllDay; - var visibleEnd; - - if (dropDate.hasTime() === dragStartDate.hasTime()) { - delta = dayishDiff(dropDate, dragStartDate); - newStart = start.clone().add(delta); - if (event.end === null) { // do we need to compute an end? - newEnd = null; - } - else { - newEnd = end.clone().add(delta); - } - newAllDay = event.allDay; // keep it the same - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - newStart = dropDate; - newEnd = null; // end should be cleared - newAllDay = !dropDate.hasTime(); - } - - // compute what the end date will appear to be - visibleEnd = newEnd || view.calendar.getDefaultEventEnd(newAllDay, newStart); - - return { start: newStart, end: newEnd, visibleEnd: visibleEnd }; - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event's resizer, which might lead to resizing. - // Generic enough to work with any type of Grid. - segResizeMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var newEnd = null; - var dragListener; - - function destroy() { // resets the rendering to show the original event - _this.destroyResize(); - view.showEvent(event); - } - - // Tracks mouse movement over the *grid's* coordinate map - dragListener = new DragListener(this.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isResizingSeg = true; - view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - // compute the new end. don't allow it to go before the event's start - if (date.isBefore(start)) { // allows comparing ambig to non-ambig - date = start; - } - newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end - - if (calendar.isEventAllowedInRange(event, start, newEnd)) { // allowed to be resized here? - if (newEnd.isSame(end)) { - newEnd = null; // mark an invalid resize - destroy(); - } - else { - _this.renderResize(start, newEnd, seg); - view.hideEvent(event); - } - } - else { - newEnd = null; // mark an invalid resize - destroy(); - disableCursor(); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newEnd = null; - destroy(); - enableCursor(); - }, - dragStop: function(ev) { - _this.isResizingSeg = false; - destroy(); - enableCursor(); - view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (newEnd) { - view.eventResize(el[0], event, newEnd, ev); // will rerender all events... - } - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var event = seg.event; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat( - event.className, - event.source ? event.source.className : [] - ); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - return classes; - }, - - - // Utility for generating a CSS string with all the event skin-related properties - getEventSkinCss: function(event) { - var view = this.view; - var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - var borderColor = - event.borderColor || - eventColor || - source.borderColor || - sourceColor || - view.opt('eventBorderColor') || - optionColor; - var textColor = - event.textColor || - source.textColor || - view.opt('eventTextColor'); - var statements = []; - if (backgroundColor) { - statements.push('background-color:' + backgroundColor); - } - if (borderColor) { - statements.push('border-color:' + borderColor); - } - if (textColor) { - statements.push('color:' + textColor); - } - return statements.join(';'); - }, - - - /* Converting events -> ranges -> segs - ------------------------------------------------------------------------------------------------------------------*/ - - - // Converts an array of event objects into an array of event segment objects. - // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events. - eventsToSegs: function(events, rangeToSegsFunc) { - var eventRanges = this.eventsToRanges(events); - var segs = []; - var i; - - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply( - segs, - this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc) - ); - } - - return segs; - }, - - - // Converts an array of events into an array of "range" objects. - // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property. - // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events, - // will create an array of ranges that span the time *not* covered by the given event. - eventsToRanges: function(events) { - var _this = this; - var eventsById = groupEventsById(events); - var ranges = []; - - // group by ID so that related inverse-background events can be rendered together - $.each(eventsById, function(id, eventGroup) { - if (eventGroup.length) { - ranges.push.apply( - ranges, - isInverseBgEvent(eventGroup[0]) ? - _this.eventsToInverseRanges(eventGroup) : - _this.eventsToNormalRanges(eventGroup) - ); - } - }); - - return ranges; - }, - - - // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges - eventsToNormalRanges: function(events) { - var calendar = this.view.calendar; - var ranges = []; - var i, event; - var eventStart, eventEnd; - - for (i = 0; i < events.length; i++) { - event = events[i]; - - // make copies and normalize by stripping timezone - eventStart = event.start.clone().stripZone(); - eventEnd = calendar.getEventEnd(event).stripZone(); - - ranges.push({ - event: event, - start: eventStart, - end: eventEnd, - eventStartMS: +eventStart, - eventDurationMS: eventEnd - eventStart - }); - } - - return ranges; - }, - - - // Converts an array of events, with inverse-background rendering, into an array of range objects. - // The range objects will cover all the time NOT covered by the events. - eventsToInverseRanges: function(events) { - var view = this.view; - var viewStart = view.start.clone().stripZone(); // normalize timezone - var viewEnd = view.end.clone().stripZone(); // normalize timezone - var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies - var inverseRanges = []; - var event0 = events[0]; // assign this to each range's `.event` - var start = viewStart; // the end of the previous range. the start of the new range - var i, normalRange; - - // ranges need to be in order. required for our date-walking algorithm - normalRanges.sort(compareNormalRanges); - - for (i = 0; i < normalRanges.length; i++) { - normalRange = normalRanges[i]; - - // add the span of time before the event (if there is any) - if (normalRange.start > start) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - event: event0, - start: start, - end: normalRange.start - }); - } - - start = normalRange.end; - } - - // add the span of time after the last event (if there is any) - if (start < viewEnd) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - event: event0, - start: start, - end: viewEnd - }); - } - - return inverseRanges; - }, - - - // Slices the given event range into one or more segment objects. - // A `rangeToSegsFunc` custom slicing function can be given. - eventRangeToSegs: function(eventRange, rangeToSegsFunc) { - var segs; - var i, seg; - - if (rangeToSegsFunc) { - segs = rangeToSegsFunc(eventRange.start, eventRange.end); - } - else { - segs = this.rangeToSegs(eventRange.start, eventRange.end); // defined by the subclass - } - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.event = eventRange.event; - seg.eventStartMS = eventRange.eventStartMS; - seg.eventDurationMS = eventRange.eventDurationMS; - } - - return segs; - } - -}); - - -/* Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -function isBgEvent(event) { // returns true if background OR inverse-background - var rendering = getEventRendering(event); - return rendering === 'background' || rendering === 'inverse-background'; -} - - -function isInverseBgEvent(event) { - return getEventRendering(event) === 'inverse-background'; -} - - -function getEventRendering(event) { - return firstDefined((event.source || {}).rendering, event.rendering); -} - - -function groupEventsById(events) { - var eventsById = {}; - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - (eventsById[event._id] || (eventsById[event._id] = [])).push(event); - } - - return eventsById; -} - - -// A cmp function for determining which non-inverted "ranges" (see above) happen earlier -function compareNormalRanges(range1, range2) { - return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first -} - - -// A cmp function for determining which segments should take visual priority -// DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS -function compareSegs(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title -} - - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -function DayGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -DayGrid.prototype = createObject(Grid.prototype); // declare the super-class -$.extend(DayGrid.prototype, { - - numbersVisible: false, // should render a row for day/week numbers? manually set by the view - cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - dayEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - render: function(isRigid) { - var view = this.view; - var html = ''; - var row; - - for (row = 0; row < view.rowCnt; row++) { - html += this.dayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.dayEls = this.el.find('.fc-day'); - - // run all the day cells through the dayRender callback - this.dayEls.each(function(i, node) { - var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); - view.trigger('dayRender', null, date, $(node)); - }); - - Grid.prototype.render.call(this); // call the super-method - }, - - - destroy: function() { - this.destroySegPopover(); - }, - - - // Generates the HTML for a single row. `row` is the row number. - dayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - - if (isRigid) { - classes.push('fc-rigid'); - } - - return '' + - '
' + - '
' + - '' + - this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() - '
' + - '
' + - '
' + - '' + - (this.numbersVisible ? - '' + - this.rowHtml('number', row) + // leverages RowRenderer. View will define render method - '' : - '' - ) + - '
' + - '
' + - '
'; - }, - - - // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. - // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering - // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). - dayCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var e, n, p; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements - e = $(_e); - n = e.offset().left; - if (i) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - this.rowEls.each(function(i, _e) { - e = $(_e); - n = e.offset().top; - if (i) { - p[1] = n; - } - p = [ n ]; - rows[i] = p; - }); - p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row - }, - - - // Converts a cell to a date - getCellDate: function(cell) { - return this.view.cellToDate(cell); // leverages the View's cell system - }, - - - // Gets the whole-day element associated with the cell - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - return this.view.rangeToSegments(start, end); // leverages the View's cell system - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - var opacity; - - // always render a highlight underneath - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(true, start) - ); - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && !seg.el.closest(this.el).length) { - - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEls.css('opacity', opacity); - } - - return true; // a helper has been rendered - } - }, - - - // Unrenders any visual indication of a hovering event - destroyDrag: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderHighlight(start, end); - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders a visual indication of an event being resized - destroyResize: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var segs = this.eventsToSegs([ event ]); - var rowStructs; - - segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered - rowStructs = this.renderSegRows(segs); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('
'); // will be absolutely positioned - var skeletonTop; - - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - this.helperEls = $(helperNodes); // array -> jQuery set - }, - - - // Unrenders any visual indication of a mock helper event - destroyHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - fillSegTag: 'td', // override the default tag name - - - // Renders a set of rectangles over the given segments of days. - // Only returns segments that successfully rendered. - renderFill: function(type, segs) { - var nodes = []; - var i, seg; - var skeletonEl; - - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - skeletonEl = this.renderFillRow(type, seg); - this.rowEls.eq(seg.row).append(skeletonEl); - nodes.push(skeletonEl[0]); - } - - this.elsByFill[type] = $(nodes); - - return segs; - }, - - - // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. - renderFillRow: function(type, seg) { - var colCnt = this.view.colCnt; - var startCol = seg.leftCol; - var endCol = seg.rightCol + 1; - var skeletonEl; - var trEl; - - skeletonEl = $( - '
' + - '
' + - '
' - ); - trEl = skeletonEl.find('tr'); - - if (startCol > 0) { - trEl.append(''); - } - - trEl.append( - seg.el.attr('colspan', endCol - startCol) - ); - - if (endCol < colCnt) { - trEl.append(''); - } - - this.bookendCells(trEl, type); - - return skeletonEl; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(DayGrid.prototype, { - - rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering - - - // Unrenders all events currently rendered on the grid - destroyEvents: function() { - this.destroySegPopover(); // removes the "more.." events popover - Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getSegs: function() { - return Grid.prototype.getSegs.call(this) // get the segments from the super-method - .concat(this.popoverSegs || []); // append the segments from the "more..." popover - }, - - - // Renders the given background event segments onto the grid - renderBgSegs: function(segs) { - - // don't render timed background events - var allDaySegs = $.grep(segs, function(seg) { - return seg.event.allDay; - }); - - return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method - }, - - - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - var rowStructs; - - // render an `.el` on each seg - // returns a subset of the segs. segs that were actually rendered - segs = this.renderFgSegEls(segs); - - rowStructs = this.rowStructs = this.renderSegRows(segs); - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - }); - - return segs; // return only the segs that were actually rendered - }, - - - // Unrenders all currently rendered foreground event segments - destroyFgSegs: function() { - var rowStructs = this.rowStructs || []; - var rowStruct; - - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.rowStructs = null; - }, - - - // Uses the given events array to generate elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). - // PRECONDITION: each segment shoud already have a rendered and assigned `.el` - renderSegRows: function(segs) { - var rowStructs = []; - var segRows; - var row; - - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderSegRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeHtml = ''; - var titleHtml; - - classes.unshift('fc-day-grid-event'); - - // Only display a timed events time if it is the starting segment - if (!event.allDay && seg.isStart) { - timeHtml = '' + htmlEscape(view.getEventTimeText(event)) + ''; - } - - titleHtml = - '' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - ''; - - return '
' + - '
' + - (isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - renderSegRow: function(row, rowSegs) { - var view = this.view; - var colCnt = view.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $(''); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all elements of the level+col matrix - var loneCellMatrix = []; // lookup for elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $(''); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $(''); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $('').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr, 'eventSkeleton'); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - segs.sort(compareSegs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var view = this.view; - var segRows = []; - var i; - - for (i = 0; i < view.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } - -}); - - -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; - - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } - - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ -// NOTE: all the segs being passed around in here are foreground segs - -$.extend(DayGrid.prototype, { - - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - destroySegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - - // Reveal one level at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal - if (trEl.position().top + trEl.outerHeight() > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var view = this.view; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" links and DOM nodes - var col = 0; // col # - var cell; - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row - var limitedNodes; // array of temporarily hidden level and segment DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - cell = { row: row, col: col }; - segsBelow = _this.getCellSegs(cell, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(cell, segsBelow); - moreWrap = $('
').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - cell = { row: row, col: col }; - segsBelow = this.getCellSegs(cell, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - cell = { row: row, col: seg.leftCol + j }; - moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too - moreWrap = $('
').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(view.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(cell, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = view.cellToDate(cell); - var moreEl = $(this); - var dayEl = _this.getCellDayEl(cell); - var allSegs = _this.getCellSegs(cell); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.trigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(date, cell, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(date, cell, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the
wrapper around the - var topEl; // the element we want to match the top coordinate of - var options; - - if (view.rowCnt == 1) { - topEl = this.view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(cell.row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(date, segs), - parentEl: this.el, - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // destroy everything when the popover is hidden - _this.segPopover.destroy(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the to avoid border confusion. - if (view.opt('isRTL')) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(date, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = date.format(view.opt('dayPopoverFormat')); - var content = $( - '
' + - '' + - '' + - htmlEscape(title) + - '' + - '
' + - '
' + - '
' + - '
' + - '
' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderFgSegEls(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - segs[i].cellDate = date; - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - - // build an array of the original events - var events = $.map(segs, function(seg) { - return seg.event; - }); - - var dayStart = dayDate.clone().stripTime(); - var dayEnd = dayStart.clone().add(1, 'days'); - - // slice the events with a custom slicing function - return this.eventsToSegs( - events, - function(rangeStart, rangeEnd) { - var seg = intersectionToSeg(rangeStart, rangeEnd, dayStart, dayEnd); // if no intersection, undefined - return seg ? [ seg ] : []; // must return an array of segments - } - ); - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var view = this.view; - var opt = view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(cell, startLevel) { - var segMatrix = this.rowStructs[cell.row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][cell.col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } - -}); - -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ - -function TimeGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -TimeGrid.prototype = createObject(Grid.prototype); // define the super-class -$.extend(TimeGrid.prototype, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - - dayEls: null, // cells elements in the day-row background - slatEls: null, // elements running horizontally across all columns - - slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot - - helperEl: null, // cell skeleton element for rendering the mock event "helper" - - businessHourSegs: null, - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - render: function() { - this.processOptions(); - - this.el.html(this.renderHtml()); - - this.dayEls = this.el.find('.fc-day'); - this.slatEls = this.el.find('.fc-slats tr'); - - this.computeSlatTops(); - - this.renderBusinessHours(); - - Grid.prototype.render.call(this); // call the super-method - }, - - - renderBusinessHours: function() { - var events = this.view.calendar.getBusinessHoursEvents(); - this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent'); - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '
' + - '' + - this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml - '
' + - '
' + - '
' + - '' + - this.slatRowHtml() + - '
' + - '
'; - }, - - - // Renders the HTML for a vertical background cell behind the slots. - // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. - slotBgCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - slatRowHtml: function() { - var view = this.view; - var calendar = view.calendar; - var isRTL = view.opt('isRTL'); - var html = ''; - var slotNormal = this.slotDuration.asMinutes() % 15 === 0; - var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var minutes; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.maxTime) { - slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues - minutes = slotDate.minutes(); - - axisHtml = - '' + - ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time - '' + // for matchCellWidths - htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + - '' : - '' - ) + - ''; - - html += - '' + - (!isRTL ? axisHtml : '') + - '' + - (isRTL ? axisHtml : '') + - ""; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.cellDuration = snapDuration; // important to assign this for Grid.events.js - - this.minTime = moment.duration(view.opt('minTime')); - this.maxTime = moment.duration(view.opt('maxTime')); - }, - - - // Slices up a date range into a segment for each column - rangeToSegs: function(rangeStart, rangeEnd) { - var view = this.view; - var segs = []; - var seg; - var col; - var cellDate; - var colStart, colEnd; - - // normalize - rangeStart = rangeStart.clone().stripZone(); - rangeEnd = rangeEnd.clone().stripZone(); - - for (col = 0; col < view.colCnt; col++) { - cellDate = view.cellToDate(0, col); // use the View's cell system for this - colStart = cellDate.clone().time(this.minTime); - colEnd = cellDate.clone().time(this.maxTime); - seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); - if (seg) { - seg.col = col; - segs.push(seg); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid - resize: function() { - this.computeSlatTops(); - this.updateSegVerticals(); - }, - - - // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. - // "Snap" cells are different the slots because they might have finer granularity. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var originTop = this.el.offset().top; - var snapTime = moment.duration(+this.minTime); - var p = null; - var e, n; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { - e = $(_e); - n = e.offset().left; - if (p) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - p = null; - while (snapTime < this.maxTime) { - n = originTop + this.computeTimeTop(snapTime); - if (p) { - p[1] = n; - } - p = [ n ]; - rows.push(p); - snapTime.add(this.snapDuration); - } - p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end - }, - - - // Gets the datetime for the given slot cell - getCellDate: function(cell) { - var view = this.view; - var calendar = view.calendar; - - return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone - view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column - .time(this.minTime + this.snapDuration * cell.row) - ); - }, - - - // Gets the element that represents the whole-day the cell resides on - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.col); - }, - - - // 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: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date.clone().stripZone() - startOfDayDate.clone().stripTime() - ) - ); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - var slatTop; - var slatBottom; - - // constrain. because minTime/maxTime might be customized - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(this.slatEls.length, slatCoverage); - - slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot - slatRemainder = slatCoverage - slatIndex; - slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot - - if (slatRemainder) { // time spans part-way into the slot - slatBottom = this.slatTops[slatIndex + 1]; - return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots - } - else { - return slatTop; - } - }, - - - // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. - // Includes the the bottom of the last slat as the last item in the array. - computeSlatTops: function() { - var tops = []; - var top; - - this.slatEls.each(function(i, node) { - top = $(node).position().top; - tops.push(top); - }); - - tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat - - this.slatTops = tops; - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the specified date(s). - // `end` and `seg` can be null. See View's documentation on renderDrag for more info. - renderDrag: function(start, end, seg) { - var opacity; - - if (seg) { // if there is event information for this drag, render a helper event - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEl.css('opacity', opacity); - } - - return true; // signal that a helper has been rendered - } - else { - // otherwise, just render a highlight - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(false, start) - ); - } - }, - - - // Unrenders any visual indication of an event being dragged - destroyDrag: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders any visual indication of an event being resized - destroyResize: function() { - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - var segs = this.eventsToSegs([ event ]); - var tableEl; - var i, seg; - var sourceEl; - - segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered - tableEl = this.renderSegTable(segs); - - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - } - - this.helperEl = $('
') - .append(tableEl) - .appendTo(this.el); - }, - - - // Unrenders any mock helper event - destroyHelper: function() { - if (this.helperEl) { - this.helperEl.remove(); - this.helperEl = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(start, end) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - this.renderRangeHelper(start, end); - } - else { - this.renderHighlight(start, end); - } - }, - - - // Unrenders any visual indication of a selection - destroySelection: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a set of rectangles over the given time segments. - // Only returns segments that successfully rendered. - renderFill: function(type, segs, className) { - var view = this.view; - var segCols; - var skeletonEl; - var trEl; - var col, colSegs; - var tdEl; - var containerEl; - var dayDate; - var i, seg; - - if (segs.length) { - - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - className = className || type.toLowerCase(); - skeletonEl = $( - '
' + - '
' + - '
' - ); - trEl = skeletonEl.find('tr'); - - for (col = 0; col < segCols.length; col++) { - colSegs = segCols[col]; - tdEl = $('').appendTo(trEl); - - if (colSegs.length) { - containerEl = $('
').appendTo(tdEl); - dayDate = view.cellToDate(0, col); - - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - containerEl.append( - seg.el.css({ - top: this.computeDateTop(seg.start, dayDate), - bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge - }) - ); - } - } - } - - this.bookendCells(trEl, type); - - this.el.append(skeletonEl); - this.elsByFill[type] = skeletonEl; - } - - return segs; - } - -}); - -;; - -/* Event-rendering methods for the TimeGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(TimeGrid.prototype, { - - eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements - - - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered - - this.el.append( - this.eventSkeletonEl = $('
') - .append(this.renderSegTable(segs)) - ); - - return segs; // return only the segs that were actually rendered - }, - - - // Unrenders all currently rendered foreground event segments - destroyFgSegs: function(segs) { - if (this.eventSkeletonEl) { - this.eventSkeletonEl.remove(); - this.eventSkeletonEl = null; - } - }, - - - // Renders and returns the portion of the event-skeleton. - // Returns an object with properties 'tbodyEl' and 'segs'. - renderSegTable: function(segs) { - var tableEl = $('
'); - var trEl = tableEl.find('tr'); - var segCols; - var i, seg; - var col, colSegs; - var containerEl; - - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - this.computeSegVerticals(segs); // compute and assign top/bottom - - for (col = 0; col < segCols.length; col++) { // iterate each column grouping - colSegs = segCols[col]; - placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array - - containerEl = $('
'); - - // assign positioning CSS and insert into container - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - seg.el.css(this.generateSegPositionCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - - containerEl.append(seg.el); - } - - trEl.append($('').append(containerEl)); - } - - this.bookendCells(trEl, 'eventSkeleton'); - - return tableEl; - }, - - - // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. - // Repositions business hours segs too, so not just for events. Maybe shouldn't be here. - updateSegVerticals: function() { - var allSegs = (this.segs || []).concat(this.businessHourSegs || []); - var i; - - this.computeSegVerticals(allSegs); - - for (i = 0; i < allSegs.length; i++) { - allSegs[i].el.css( - this.generateSegVerticalCss(allSegs[i]) - ); - } - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.top = this.computeDateTop(seg.start, seg.start); - seg.bottom = this.computeDateTop(seg.end, seg.start); - } - }, - - - // Renders the HTML for a single event segment's default rendering - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = view.getEventTimeText(seg.start, seg.end); - fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); - startTimeText = view.getEventTimeText(seg.start, null); - } - } else { - // Display the normal time text for the *event's* times - timeText = view.getEventTimeText(event); - fullTimeText = view.getEventTimeText(event, 'LT'); - startTimeText = view.getEventTimeText(event.start, null); - } - - return '' + - '
' + - (timeText ? - '
' + - '' + htmlEscape(timeText) + '' + - '
' : - '' - ) + - (event.title ? - '
' + - htmlEscape(event.title) + - '
' : - '' - ) + - '
' + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateSegPositionCss: function(seg) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var shouldOverlap = view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var 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) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // 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; - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegCols: function(segs) { - var view = this.view; - var segCols = []; - var i; - - for (i = 0; i < view.colCnt; i++) { - segCols.push([]); - } - - for (i = 0; i < segs.length; i++) { - segCols[segs[i].col].push(segs[i]); - } - - return segCols; - } - -}); - - -// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. -// Also reorders the given array by date! -function placeSlotSegs(segs) { - var levels; - var level0; - var i; - - segs.sort(compareSegs); // order by date - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - computeSlotSegCoords(level0[i], 0, 0); - } - } -} - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i seg2.top && seg1.top < seg2.bottom; -} - - -// A cmp function for determining which forward segment to rely on more when computing coordinates. -function compareForwardSlotSegs(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - compareSegs(seg1, seg2); -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ -// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. - -View.prototype = { - - calendar: null, // owner Calendar object - coordMap: null, // a CoordMap object for converting pixel regions to dates - el: null, // the view's containing element. set by Calendar - - // important Moments - start: null, // the date of the very first cell - end: null, // the date after the very last cell - intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) - intervalEnd: null, // the exclusive end of the interval of time the view represents - - // used for cell-to-date and date-to-cell calculations - rowCnt: null, // # of weeks - colCnt: null, // # of days displayed in a week - - isSelected: false, // boolean whether cells are user-selected or not - - // subclasses can optionally use a scroll container - scrollerEl: null, // the element that will most likely scroll when content is too tall - scrollTop: null, // cached vertical scroll value - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // document handlers, bound to `this` object - documentMousedownProxy: null, - documentDragStartProxy: null, - - - // Serves as a "constructor" to suppliment the monster `View` constructor below - init: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - - // save references to `this`-bound handlers - this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); - this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); - }, - - - // Renders the view inside an already-defined `this.el`. - // Subclasses should override this and then call the super method afterwards. - render: function() { - this.updateSize(); - this.trigger('viewRender', this, this, this.el); - - // attach handlers to document. do it here to allow for destroy/rerender - $(document) - .on('mousedown', this.documentMousedownProxy) - .on('dragstart', this.documentDragStartProxy); // jqui drag - }, - - - // Clears all view rendering, event elements, and unregisters handlers - destroy: function() { - this.unselect(); - this.trigger('viewDestroy', this, this, this.el); - this.destroyEvents(); - this.el.empty(); // removes inner contents but leaves the element intact - - $(document) - .off('mousedown', this.documentMousedownProxy) - .off('dragstart', this.documentDragStartProxy); - }, - - - // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. - // Should apply the delta to `date` (a Moment) and return it. - incrementDate: function(date, delta) { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - if (isResize) { - this.recordScroll(); - } - this.updateHeight(); - this.updateWidth(); - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function() { - // subclasses should implement - }, - - - // Refreshes the vertical dimensions of the calendar - updateHeight: function() { - var calendar = this.calendar; // we poll the calendar for height information - - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, - - - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, - - - // Given the total height of the view, return the number of pixels that should be used for the scroller. - // Utility for subclasses. - computeScrollerHeight: function(totalHeight) { - var both = this.el.add(this.scrollerEl); - var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) - - // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return totalHeight - otherHeight; - }, - - - // Called for remembering the current scroll value of the scroller. - // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently - // change the scroll of the container. - recordScroll: function() { - if (this.scrollerEl) { - this.scrollTop = this.scrollerEl.scrollTop(); - } - }, - - - // Set the scroll value of the scroller to the previously recorded value. - // Should be called after we know the view's dimensions have been restored following some type of destructive - // operation (like temporarily removing DOM elements). - restoreScroll: function() { - if (this.scrollTop !== null) { - this.scrollerEl.scrollTop(this.scrollTop); - } - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the events onto the view. - // Should be overriden by subclasses. Subclasses should call the super-method afterwards. - renderEvents: function(events) { - this.segEach(function(seg) { - this.trigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.trigger('eventAfterAllRender'); - }, - - - // Removes event elements from the view. - // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. - destroyEvents: function() { - this.segEach(function(seg) { - this.trigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, - - - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.trigger('eventRender', event, event, el); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // Iterates through event segments. Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - segEach: function(func, event) { - var segs = this.getSegs(); - var i; - - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - func.call(this, segs[i]); - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getSegs: function() { - // subclasses must implement - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the specified date. - // `end` is a Moment and might be null. - // `seg` might be null. if specified, it is the segment object of the event being dragged. - // otherwise, an external event from outside the calendar is being dragged. - renderDrag: function(start, end, seg) { - // subclasses should implement - }, - - - // Unrenders a visual indication of event hovering - destroyDrag: function() { - // subclasses should implement - }, - - - // Handler for accepting externally dragged events being dropped in the view. - // Gets called when jqui's 'dragstart' is fired. - documentDragStart: function(ev, ui) { - var _this = this; - var calendar = this.calendar; - var eventStart = null; // a null value signals an unsuccessful drag - var eventEnd = null; - var visibleEnd = null; // will be calculated event when no eventEnd - var el; - var accept; - var meta; - var eventProps; // if an object, signals an event should be created upon drop - var dragListener; - - if (this.opt('droppable')) { // only listen if this setting is on - el = $(ev.target); - - // Test that the dragged element passes the dropAccept selector or filter function. - // FYI, the default is "*" (matches all) - accept = this.opt('dropAccept'); - if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { - - meta = getDraggedElMeta(el); // data for possibly creating an event - eventProps = meta.eventProps; - - // listener that tracks mouse movement over date-associated pixel regions - dragListener = new DragListener(this.coordMap, { - cellOver: function(cell, cellDate) { - eventStart = cellDate; - eventEnd = meta.duration ? eventStart.clone().add(meta.duration) : null; - visibleEnd = eventEnd || calendar.getDefaultEventEnd(!eventStart.hasTime(), eventStart); - - // keep the start/end up to date when dragging - if (eventProps) { - $.extend(eventProps, { start: eventStart, end: eventEnd }); - } - - if (calendar.isExternalDragAllowedInRange(eventStart, visibleEnd, eventProps)) { - _this.renderDrag(eventStart, visibleEnd); - } - else { - eventStart = null; // signal unsuccessful - disableCursor(); - } - }, - cellOut: function() { - eventStart = null; - _this.destroyDrag(); - enableCursor(); - } - }); - - // gets called, only once, when jqui drag is finished - $(document).one('dragstop', function(ev, ui) { - var renderedEvents; - - _this.destroyDrag(); - enableCursor(); - - if (eventStart) { // element was dropped on a valid date/time cell - - // if dropped on an all-day cell, and element's metadata specified a time, set it - if (meta.startTime && !eventStart.hasTime()) { - eventStart.time(meta.startTime); - } - - // trigger 'drop' regardless of whether element represents an event - _this.trigger('drop', el[0], eventStart, ev, ui); - - // create an event from the given properties and the latest dates - if (eventProps) { - renderedEvents = calendar.renderEvent(eventProps, meta.stick); - _this.trigger('eventReceive', null, renderedEvents[0]); // signal an external event landed - } - } - }); - - dragListener.startDrag(ev); // start listening immediately - } - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Selects a date range on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(start, end, ev) { - this.unselect(ev); - this.renderSelection(start, end); - this.reportSelection(start, end, ev); - }, - - - // Renders a visual indication of the selection - renderSelection: function(start, end) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(start, end, ev) { - this.isSelected = true; - this.trigger('select', null, start, end, ev); - }, - - - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - this.destroySelection(); - this.trigger('unselect', null, ev); - } - }, - - - // Unrenders a visual indication of selection - destroySelection: function() { - // subclasses should implement - }, - - - // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on - documentMousedown: function(ev) { - var ignore; - - // is there a selection, and has the user made a proper left click? - if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { - - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - } - -}; - - -// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the -// constructor. Going forward, methods should be part of the prototype. -function View(calendar) { - var t = this; - - // exports - t.calendar = calendar; - t.opt = opt; - t.trigger = trigger; - t.isEventDraggable = isEventDraggable; - t.isEventResizable = isEventResizable; - t.eventDrop = eventDrop; - t.eventResize = eventResize; - - // imports - var reportEventChange = calendar.reportEventChange; - - // locals - var options = calendar.options; - var nextDayThreshold = moment.duration(options.nextDayThreshold); - - - t.init(); // the "constructor" that concerns the prototype methods - - - function opt(name) { - var v = options[name]; - if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { - return smartProperty(v, t.name); - } - return v; - } - - - function trigger(name, thisObj) { - return calendar.trigger.apply( - calendar, - [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) - ); - } - - - - /* Event Editable Boolean Calculations - ------------------------------------------------------------------------------*/ - - - function isEventDraggable(event) { - var source = event.source || {}; - - return firstDefined( - event.startEditable, - source.startEditable, - opt('eventStartEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - function isEventResizable(event) { - var source = event.source || {}; - - return firstDefined( - event.durationEditable, - source.durationEditable, - opt('eventDurationEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - - /* Event Elements - ------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // Based off the settings of the view. Possible signatures: - // .getEventTimeText(event, formatStr) - // .getEventTimeText(startMoment, endMoment, formatStr) - // .getEventTimeText(startMoment, null, formatStr) - // `timeFormat` is used but the `formatStr` argument can be used to override. - t.getEventTimeText = function(event, formatStr) { - var start; - var end; - - if (typeof event === 'object' && typeof formatStr === 'object') { - // first two arguments are actually moments (or null). shift arguments. - start = event; - end = formatStr; - formatStr = arguments[2]; - } - else { - // otherwise, an event object was the first argument - start = event.start; - end = event.end; - } - - formatStr = formatStr || opt('timeFormat'); - - if (end && opt('displayEventEnd')) { - return calendar.formatRange(start, end, formatStr); - } - else { - return calendar.formatDate(start, formatStr); - } - }; - - - - /* Event Modification Reporting - ---------------------------------------------------------------------------------*/ - - - function eventDrop(el, event, newStart, ev) { - var mutateResult = calendar.mutateEvent(event, newStart, null); - - trigger( - 'eventDrop', - el, - event, - mutateResult.dateDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - function eventResize(el, event, newEnd, ev) { - var mutateResult = calendar.mutateEvent(event, null, newEnd); - - trigger( - 'eventResize', - el, - event, - mutateResult.durationDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - // ==================================================================================================== - // Utilities for day "cells" - // ==================================================================================================== - // The "basic" views are completely made up of day cells. - // The "agenda" views have day cells at the top "all day" slot. - // This was the obvious common place to put these utilities, but they should be abstracted out into - // a more meaningful class (like DayEventRenderer). - // ==================================================================================================== - - - // For determining how a given "cell" translates into a "date": - // - // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). - // Keep in mind that column indices are inverted with isRTL. This is taken into account. - // - // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). - // - // 3. Convert the "day offset" into a "date" (a Moment). - // - // The reverse transformation happens when transforming a date into a cell. - - - // exports - t.isHiddenDay = isHiddenDay; - t.skipHiddenDays = skipHiddenDays; - t.getCellsPerWeek = getCellsPerWeek; - t.dateToCell = dateToCell; - t.dateToDayOffset = dateToDayOffset; - t.dayOffsetToCellOffset = dayOffsetToCellOffset; - t.cellOffsetToCell = cellOffsetToCell; - t.cellToDate = cellToDate; - t.cellToCellOffset = cellToCellOffset; - t.cellOffsetToDayOffset = cellOffsetToDayOffset; - t.dayOffsetToDate = dayOffsetToDate; - t.rangeToSegments = rangeToSegments; - t.isMultiDayEvent = isMultiDayEvent; - - - // internals - var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var cellsPerWeek; - var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week - var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week - var isRTL = opt('isRTL'); - - - // initialize important internal variables - (function() { - - if (opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - // Loop through a hypothetical week and determine which - // days-of-week are hidden. Record in both hashes (one is the reverse of the other). - for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { - dayToCellMap[dayIndex] = cellIndex; - isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; - if (!isHiddenDayHash[dayIndex]) { - cellToDayMap[cellIndex] = dayIndex; - cellIndex++; - } - } - - cellsPerWeek = cellIndex; - if (!cellsPerWeek) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - })(); - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - function isHiddenDay(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return isHiddenDayHash[day]; - } - - - function getCellsPerWeek() { - return cellsPerWeek; - } - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // 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) - function skipHiddenDays(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - } - - - // - // TRANSFORMATIONS: cell -> cell offset -> day offset -> date - // - - // cell -> date (combines all transformations) - // Possible arguments: - // - row, col - // - { row:#, col: # } - function cellToDate() { - var cellOffset = cellToCellOffset.apply(null, arguments); - var dayOffset = cellOffsetToDayOffset(cellOffset); - var date = dayOffsetToDate(dayOffset); - return date; - } - - // cell -> cell offset - // Possible arguments: - // - row, col - // - { row:#, col:# } - function cellToCellOffset(row, col) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - if (typeof row == 'object') { - col = row.col; - row = row.row; - } - var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) - - return cellOffset; - } - - // cell offset -> day offset - function cellOffsetToDayOffset(cellOffset) { - var day0 = t.start.day(); // first date's day of week - cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week - return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks - cellToDayMap[ // # of days from partial last week - (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets - ] - - day0; // adjustment for beginning-of-week normalization - } - - // day offset -> date - function dayOffsetToDate(dayOffset) { - return t.start.clone().add(dayOffset, 'days'); - } - - - // - // TRANSFORMATIONS: date -> day offset -> cell offset -> cell - // - - // date -> cell (combines all transformations) - function dateToCell(date) { - var dayOffset = dateToDayOffset(date); - var cellOffset = dayOffsetToCellOffset(dayOffset); - var cell = cellOffsetToCell(cellOffset); - return cell; - } - - // date -> day offset - function dateToDayOffset(date) { - return date.clone().stripTime().diff(t.start, 'days'); - } - - // day offset -> cell offset - function dayOffsetToCellOffset(dayOffset) { - var day0 = t.start.day(); // first date's day of week - dayOffset += day0; // normalize dayOffset to beginning-of-week - return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks - dayToCellMap[ // # of cells from partial last week - (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets - ] - - dayToCellMap[day0]; // adjustment for beginning-of-week normalization - } - - // cell offset -> cell (object with row & col keys) - function cellOffsetToCell(cellOffset) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - var row = Math.floor(cellOffset / colCnt); - var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) - return { - row: row, - col: col - }; - } - - - // - // Converts a date range into an array of segment objects. - // "Segments" are horizontal stretches of time, sliced up by row. - // A segment object has the following properties: - // - row - // - cols - // - isStart - // - isEnd - // - function rangeToSegments(start, end) { - - var rowCnt = t.rowCnt; - var colCnt = t.colCnt; - var segments = []; // array of segments to return - - // day offset for given date range - var dayRange = computeDayRange(start, end); // convert to a whole-day range - var rangeDayOffsetStart = dateToDayOffset(dayRange.start); - var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value - - // first and last cell offset for the given date range - // "last" implies inclusivity - var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); - var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; - - // loop through all the rows in the view - for (var row=0; row