├── README.md ├── editor ├── css │ └── ui-lightness │ │ ├── images │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_228ef1_256x240.png │ │ ├── ui-icons_ef8c08_256x240.png │ │ ├── ui-icons_ffd27a_256x240.png │ │ └── ui-icons_ffffff_256x240.png │ │ └── jquery-ui.css ├── index.html ├── js │ └── jquery-ui.js ├── jsganttchart.editor.js └── style.css ├── gantt.png ├── jsganttchart.js ├── lib ├── backbone.js ├── backbone.localStorage.js ├── jquery.js └── underscore.js ├── project ├── gantt.js └── index.html └── themes ├── default.css └── default.images └── urgent.png /README.md: -------------------------------------------------------------------------------- 1 | JS Gantt Chart 2 | ============== 3 | 4 | Simply put, this is a JavaScript Gantt Chart generator. It generates a gantt chart from JSON. 5 | 6 | ![ScreenShot](https://raw.github.com/FlamingTempura/JSGanttChart/master/gantt.png) 7 | 8 | Editor 9 | ------ 10 | 11 | There is also a limited UI for creating and editing Gantt Chart. 12 | 13 | 14 | Requirements 15 | ------------ 16 | * jQuery 17 | * Backbone.js 18 | * Underscore.js 19 | 20 | Example 21 | ------- 22 | 23 | To produce the gantt chart as shown in the image above: 24 | 25 | ```javascript 26 | var gantt = JSGanttChart.create({ 27 | resources: { 28 | programmer: "Peter West", 29 | supervisor: "mc", 30 | cosupervisor: "max" 31 | }, 32 | types: { 33 | analysis: { 34 | name: "Analysis", 35 | color: "#C79810" // yellow 36 | }, 37 | critical: { 38 | name: "Critical", 39 | color: "#CC0000" // red 40 | }, 41 | programming: { 42 | name: "Programming", 43 | color: "#356AA0" // blue 44 | }, 45 | documentation: { 46 | name: "Documentation", 47 | color: "#FF7400" // orange 48 | } 49 | }, 50 | elements: [ 51 | { 52 | id: "meeting1", 53 | name: "Supervisor meeting", 54 | startDate: "1 October 2011 11:00", 55 | percentageDone: 100, 56 | resources: [ "programmer", "supervisor", "cosupervisor" ] 57 | }, 58 | { 59 | id: "brief", 60 | name: "Project Brief", 61 | predecessors: ["meeting1"], 62 | startDate: "3 October 2011", 63 | endDate: "14 October 2011 16:00", 64 | type: "critical", 65 | slackEndDate: "21 October 2011 16:00",//slackDuration: 4, // or slackEndDate: date 66 | elements: [ 67 | { 68 | id: "briefdraft1", 69 | name: "Draft 1", 70 | startDate: "3 October 2011", 71 | endDate: "14 October 2011 16:00", 72 | type: "analysis", 73 | percentageDone: 100, 74 | estimatedHours: 5 75 | }, 76 | { 77 | id: "briefdraft2", 78 | predecessors: ["briefdraft1"], 79 | name: "Draft 2", 80 | startDate: "15 October 2011", 81 | endDate: "17 October 2011", 82 | percentageDone: 90, 83 | estimatedHours: 5 84 | }, 85 | ], 86 | percentageDone: 95, 87 | estimatedHours: 10 88 | }, 89 | { 90 | id: "planning", 91 | name: "Project Planning", 92 | predecessors: ["brief"], 93 | startDate: "17 October 2011", 94 | endDate: "30 October 2011 14:00", 95 | percentageDone: 50, 96 | elements: [ 97 | { 98 | id: "gantt1", 99 | name: "Time planning", 100 | startDate: "17 October 2011", 101 | endDate: "21 October 2011 14:00", 102 | estimatedHours: 6, 103 | percentageDone: 60 104 | }, 105 | { 106 | id: "design1", 107 | name: "Initial mockups", 108 | startDate: "20 October 2011", 109 | endDate: "21 October 2011 14:00", 110 | estimatedHours: 6, 111 | percentageDone: 10 112 | }, 113 | { 114 | id: "prestudy", 115 | name: "Prestudy", 116 | startDate: "18 October 2011", 117 | endDate: "29 October 2011", 118 | type: "analysis", 119 | percentageDone: 2, 120 | } 121 | ] 122 | }, 123 | { 124 | id: "dev", 125 | name: "Software development", 126 | startDate: "1 November 2011", 127 | endDate: "1 March 2012", 128 | predecessors: ["planning"], 129 | elements: [ 130 | { 131 | id: "software1", 132 | name: "Software development P1", 133 | startDate: "1 November 2011", 134 | endDate: "17 December 2011" 135 | }, 136 | { 137 | id: "software2", 138 | name: "Software development P2", 139 | startDate: "12 January 2012", 140 | endDate: "14 February 2012" 141 | } 142 | ] 143 | }, 144 | { 145 | id: "study", 146 | name: "Study", 147 | startDate: "18 December 2011", 148 | endDate: "12 April 2012", 149 | type: "analysis", 150 | elements: [ 151 | { 152 | id: "study1", 153 | name: "First study", 154 | startDate: "18 December 2011", 155 | endDate: "11 January 2012", 156 | type: "analysis", 157 | predecessors: ["software1"] 158 | }, 159 | { 160 | id: "study2", 161 | name: "Second study", 162 | startDate: "15 February 2012", 163 | endDate: "12 April 2012", 164 | type: "analysis", 165 | predecessors: ["software2"] 166 | } 167 | ] 168 | }, 169 | { 170 | id: "reviewmeeting1", 171 | name: "Review meeting", 172 | startDate: "13 November 2011" 173 | }, 174 | { 175 | id: "progreport", 176 | name: "Progress Report", 177 | startDate: "7 December 2011", 178 | endDate: "14 December 2011 16:00", 179 | type: "documentation" 180 | }, 181 | { 182 | id: "finalreport", 183 | name: "Final Project Report", 184 | startDate: "2 April 2012", 185 | endDate: "2 May 2012", 186 | type: "documentation" 187 | } 188 | ] 189 | }); 190 | 191 | jQuery("#container").append(gantt.render().el); 192 | 193 | ``` 194 | -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/editor/css/ui-lightness/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /editor/css/ui-lightness/jquery-ui.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI CSS Framework 1.8.16 3 | * 4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Theming/API 9 | */ 10 | 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { display: none; } 14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 16 | .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 17 | .ui-helper-clearfix { display: inline-block; } 18 | /* required comment for clearfix to work in Opera \*/ 19 | * html .ui-helper-clearfix { height:1%; } 20 | .ui-helper-clearfix { display:block; } 21 | /* end clearfix */ 22 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 23 | 24 | 25 | /* Interaction Cues 26 | ----------------------------------*/ 27 | .ui-state-disabled { cursor: default !important; } 28 | 29 | 30 | /* Icons 31 | ----------------------------------*/ 32 | 33 | /* states and images */ 34 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 35 | 36 | 37 | /* Misc visuals 38 | ----------------------------------*/ 39 | 40 | /* Overlays */ 41 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 42 | 43 | 44 | /* 45 | * jQuery UI CSS Framework 1.8.16 46 | * 47 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 48 | * Dual licensed under the MIT or GPL Version 2 licenses. 49 | * http://jquery.org/license 50 | * 51 | * http://docs.jquery.com/UI/Theming/API 52 | * 53 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px 54 | */ 55 | 56 | 57 | /* Component containers 58 | ----------------------------------*/ 59 | .ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1.1em; } 60 | .ui-widget .ui-widget { font-size: 1em; } 61 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1em; } 62 | .ui-widget-content { border: 1px solid #dddddd; background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; color: #333333; } 63 | .ui-widget-content a { color: #333333; } 64 | .ui-widget-header { border: 1px solid #e78f08; background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; } 65 | .ui-widget-header a { color: #ffffff; } 66 | 67 | /* Interaction states 68 | ----------------------------------*/ 69 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #1c94c4; } 70 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; } 71 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #fbcb09; background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #c77405; } 72 | .ui-state-hover a, .ui-state-hover a:hover { color: #c77405; text-decoration: none; } 73 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #fbd850; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #eb8f00; } 74 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; text-decoration: none; } 75 | .ui-widget :active { outline: none; } 76 | 77 | /* Interaction Cues 78 | ----------------------------------*/ 79 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x; color: #363636; } 80 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } 81 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat; color: #ffffff; } 82 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } 83 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } 84 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 85 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 86 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 87 | 88 | /* Icons 89 | ----------------------------------*/ 90 | 91 | /* states and images */ 92 | .ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } 93 | .ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } 94 | .ui-widget-header .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } 95 | .ui-state-default .ui-icon { background-image: url(images/ui-icons_ef8c08_256x240.png); } 96 | .ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } 97 | .ui-state-active .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } 98 | .ui-state-highlight .ui-icon {background-image: url(images/ui-icons_228ef1_256x240.png); } 99 | .ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffd27a_256x240.png); } 100 | 101 | /* positioning */ 102 | .ui-icon-carat-1-n { background-position: 0 0; } 103 | .ui-icon-carat-1-ne { background-position: -16px 0; } 104 | .ui-icon-carat-1-e { background-position: -32px 0; } 105 | .ui-icon-carat-1-se { background-position: -48px 0; } 106 | .ui-icon-carat-1-s { background-position: -64px 0; } 107 | .ui-icon-carat-1-sw { background-position: -80px 0; } 108 | .ui-icon-carat-1-w { background-position: -96px 0; } 109 | .ui-icon-carat-1-nw { background-position: -112px 0; } 110 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 111 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 112 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 113 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 114 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 115 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 116 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 117 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 118 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 119 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 120 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 121 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 122 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 123 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 124 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 125 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 126 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 127 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 128 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 129 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 130 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 131 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 132 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 133 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 134 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 135 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 136 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 137 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 138 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 139 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 140 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 141 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 142 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 143 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 144 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 145 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 146 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 147 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 148 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 149 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 150 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 151 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 152 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 153 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 154 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 155 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 156 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 157 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 158 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 159 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 160 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 161 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 162 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 163 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 164 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 165 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 166 | .ui-icon-arrow-4 { background-position: 0 -80px; } 167 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 168 | .ui-icon-extlink { background-position: -32px -80px; } 169 | .ui-icon-newwin { background-position: -48px -80px; } 170 | .ui-icon-refresh { background-position: -64px -80px; } 171 | .ui-icon-shuffle { background-position: -80px -80px; } 172 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 173 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 174 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 175 | .ui-icon-folder-open { background-position: -16px -96px; } 176 | .ui-icon-document { background-position: -32px -96px; } 177 | .ui-icon-document-b { background-position: -48px -96px; } 178 | .ui-icon-note { background-position: -64px -96px; } 179 | .ui-icon-mail-closed { background-position: -80px -96px; } 180 | .ui-icon-mail-open { background-position: -96px -96px; } 181 | .ui-icon-suitcase { background-position: -112px -96px; } 182 | .ui-icon-comment { background-position: -128px -96px; } 183 | .ui-icon-person { background-position: -144px -96px; } 184 | .ui-icon-print { background-position: -160px -96px; } 185 | .ui-icon-trash { background-position: -176px -96px; } 186 | .ui-icon-locked { background-position: -192px -96px; } 187 | .ui-icon-unlocked { background-position: -208px -96px; } 188 | .ui-icon-bookmark { background-position: -224px -96px; } 189 | .ui-icon-tag { background-position: -240px -96px; } 190 | .ui-icon-home { background-position: 0 -112px; } 191 | .ui-icon-flag { background-position: -16px -112px; } 192 | .ui-icon-calendar { background-position: -32px -112px; } 193 | .ui-icon-cart { background-position: -48px -112px; } 194 | .ui-icon-pencil { background-position: -64px -112px; } 195 | .ui-icon-clock { background-position: -80px -112px; } 196 | .ui-icon-disk { background-position: -96px -112px; } 197 | .ui-icon-calculator { background-position: -112px -112px; } 198 | .ui-icon-zoomin { background-position: -128px -112px; } 199 | .ui-icon-zoomout { background-position: -144px -112px; } 200 | .ui-icon-search { background-position: -160px -112px; } 201 | .ui-icon-wrench { background-position: -176px -112px; } 202 | .ui-icon-gear { background-position: -192px -112px; } 203 | .ui-icon-heart { background-position: -208px -112px; } 204 | .ui-icon-star { background-position: -224px -112px; } 205 | .ui-icon-link { background-position: -240px -112px; } 206 | .ui-icon-cancel { background-position: 0 -128px; } 207 | .ui-icon-plus { background-position: -16px -128px; } 208 | .ui-icon-plusthick { background-position: -32px -128px; } 209 | .ui-icon-minus { background-position: -48px -128px; } 210 | .ui-icon-minusthick { background-position: -64px -128px; } 211 | .ui-icon-close { background-position: -80px -128px; } 212 | .ui-icon-closethick { background-position: -96px -128px; } 213 | .ui-icon-key { background-position: -112px -128px; } 214 | .ui-icon-lightbulb { background-position: -128px -128px; } 215 | .ui-icon-scissors { background-position: -144px -128px; } 216 | .ui-icon-clipboard { background-position: -160px -128px; } 217 | .ui-icon-copy { background-position: -176px -128px; } 218 | .ui-icon-contact { background-position: -192px -128px; } 219 | .ui-icon-image { background-position: -208px -128px; } 220 | .ui-icon-video { background-position: -224px -128px; } 221 | .ui-icon-script { background-position: -240px -128px; } 222 | .ui-icon-alert { background-position: 0 -144px; } 223 | .ui-icon-info { background-position: -16px -144px; } 224 | .ui-icon-notice { background-position: -32px -144px; } 225 | .ui-icon-help { background-position: -48px -144px; } 226 | .ui-icon-check { background-position: -64px -144px; } 227 | .ui-icon-bullet { background-position: -80px -144px; } 228 | .ui-icon-radio-off { background-position: -96px -144px; } 229 | .ui-icon-radio-on { background-position: -112px -144px; } 230 | .ui-icon-pin-w { background-position: -128px -144px; } 231 | .ui-icon-pin-s { background-position: -144px -144px; } 232 | .ui-icon-play { background-position: 0 -160px; } 233 | .ui-icon-pause { background-position: -16px -160px; } 234 | .ui-icon-seek-next { background-position: -32px -160px; } 235 | .ui-icon-seek-prev { background-position: -48px -160px; } 236 | .ui-icon-seek-end { background-position: -64px -160px; } 237 | .ui-icon-seek-start { background-position: -80px -160px; } 238 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 239 | .ui-icon-seek-first { background-position: -80px -160px; } 240 | .ui-icon-stop { background-position: -96px -160px; } 241 | .ui-icon-eject { background-position: -112px -160px; } 242 | .ui-icon-volume-off { background-position: -128px -160px; } 243 | .ui-icon-volume-on { background-position: -144px -160px; } 244 | .ui-icon-power { background-position: 0 -176px; } 245 | .ui-icon-signal-diag { background-position: -16px -176px; } 246 | .ui-icon-signal { background-position: -32px -176px; } 247 | .ui-icon-battery-0 { background-position: -48px -176px; } 248 | .ui-icon-battery-1 { background-position: -64px -176px; } 249 | .ui-icon-battery-2 { background-position: -80px -176px; } 250 | .ui-icon-battery-3 { background-position: -96px -176px; } 251 | .ui-icon-circle-plus { background-position: 0 -192px; } 252 | .ui-icon-circle-minus { background-position: -16px -192px; } 253 | .ui-icon-circle-close { background-position: -32px -192px; } 254 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 255 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 256 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 257 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 258 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 259 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 260 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 261 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 262 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 263 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 264 | .ui-icon-circle-check { background-position: -208px -192px; } 265 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 266 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 267 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 268 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 269 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 270 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 271 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 272 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 273 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 274 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 275 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 276 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 277 | 278 | 279 | /* Misc visuals 280 | ----------------------------------*/ 281 | 282 | /* Corner radius */ 283 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } 284 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } 285 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } 286 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } 287 | 288 | /* Overlays */ 289 | .ui-widget-overlay { background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); } 290 | .ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -khtml-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }/* 291 | * jQuery UI Resizable 1.8.16 292 | * 293 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 294 | * Dual licensed under the MIT or GPL Version 2 licenses. 295 | * http://jquery.org/license 296 | * 297 | * http://docs.jquery.com/UI/Resizable#theming 298 | */ 299 | .ui-resizable { position: relative;} 300 | .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } 301 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 302 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 303 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 304 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 305 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 306 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 307 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 308 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 309 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* 310 | * jQuery UI Selectable 1.8.16 311 | * 312 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 313 | * Dual licensed under the MIT or GPL Version 2 licenses. 314 | * http://jquery.org/license 315 | * 316 | * http://docs.jquery.com/UI/Selectable#theming 317 | */ 318 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 319 | /* 320 | * jQuery UI Accordion 1.8.16 321 | * 322 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 323 | * Dual licensed under the MIT or GPL Version 2 licenses. 324 | * http://jquery.org/license 325 | * 326 | * http://docs.jquery.com/UI/Accordion#theming 327 | */ 328 | /* IE/Win - Fix animation bug - #4615 */ 329 | .ui-accordion { width: 100%; } 330 | .ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } 331 | .ui-accordion .ui-accordion-li-fix { display: inline; } 332 | .ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } 333 | .ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } 334 | .ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } 335 | .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } 336 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } 337 | .ui-accordion .ui-accordion-content-active { display: block; } 338 | /* 339 | * jQuery UI Autocomplete 1.8.16 340 | * 341 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 342 | * Dual licensed under the MIT or GPL Version 2 licenses. 343 | * http://jquery.org/license 344 | * 345 | * http://docs.jquery.com/UI/Autocomplete#theming 346 | */ 347 | .ui-autocomplete { position: absolute; cursor: default; } 348 | 349 | /* workarounds */ 350 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 351 | 352 | /* 353 | * jQuery UI Menu 1.8.16 354 | * 355 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 356 | * Dual licensed under the MIT or GPL Version 2 licenses. 357 | * http://jquery.org/license 358 | * 359 | * http://docs.jquery.com/UI/Menu#theming 360 | */ 361 | .ui-menu { 362 | list-style:none; 363 | padding: 2px; 364 | margin: 0; 365 | display:block; 366 | float: left; 367 | } 368 | .ui-menu .ui-menu { 369 | margin-top: -3px; 370 | } 371 | .ui-menu .ui-menu-item { 372 | margin:0; 373 | padding: 0; 374 | zoom: 1; 375 | float: left; 376 | clear: left; 377 | width: 100%; 378 | } 379 | .ui-menu .ui-menu-item a { 380 | text-decoration:none; 381 | display:block; 382 | padding:.2em .4em; 383 | line-height:1.5; 384 | zoom:1; 385 | } 386 | .ui-menu .ui-menu-item a.ui-state-hover, 387 | .ui-menu .ui-menu-item a.ui-state-active { 388 | font-weight: normal; 389 | margin: -1px; 390 | } 391 | /* 392 | * jQuery UI Button 1.8.16 393 | * 394 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 395 | * Dual licensed under the MIT or GPL Version 2 licenses. 396 | * http://jquery.org/license 397 | * 398 | * http://docs.jquery.com/UI/Button#theming 399 | */ 400 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ 401 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 402 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 403 | .ui-button-icons-only { width: 3.4em; } 404 | button.ui-button-icons-only { width: 3.7em; } 405 | 406 | /*button text element */ 407 | .ui-button .ui-button-text { display: block; line-height: 1.4; } 408 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 409 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 410 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 411 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 412 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 413 | /* no icon support for input elements, provide padding by default */ 414 | input.ui-button { padding: .4em 1em; } 415 | 416 | /*button icon element(s) */ 417 | .ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } 418 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 419 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } 420 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 421 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 422 | 423 | /*button sets*/ 424 | .ui-buttonset { margin-right: 7px; } 425 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 426 | 427 | /* workarounds */ 428 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 429 | /* 430 | * jQuery UI Dialog 1.8.16 431 | * 432 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 433 | * Dual licensed under the MIT or GPL Version 2 licenses. 434 | * http://jquery.org/license 435 | * 436 | * http://docs.jquery.com/UI/Dialog#theming 437 | */ 438 | .ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } 439 | .ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } 440 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 441 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } 442 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } 443 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } 444 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 445 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 446 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 447 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 448 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } 449 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 450 | /* 451 | * jQuery UI Slider 1.8.16 452 | * 453 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 454 | * Dual licensed under the MIT or GPL Version 2 licenses. 455 | * http://jquery.org/license 456 | * 457 | * http://docs.jquery.com/UI/Slider#theming 458 | */ 459 | .ui-slider { position: relative; text-align: left; } 460 | .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } 461 | .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 462 | 463 | .ui-slider-horizontal { height: .8em; } 464 | .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } 465 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 466 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 467 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 468 | 469 | .ui-slider-vertical { width: .8em; height: 100px; } 470 | .ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } 471 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 472 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 473 | .ui-slider-vertical .ui-slider-range-max { top: 0; }/* 474 | * jQuery UI Tabs 1.8.16 475 | * 476 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 477 | * Dual licensed under the MIT or GPL Version 2 licenses. 478 | * http://jquery.org/license 479 | * 480 | * http://docs.jquery.com/UI/Tabs#theming 481 | */ 482 | .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 483 | .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } 484 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } 485 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } 486 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } 487 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } 488 | .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 489 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } 490 | .ui-tabs .ui-tabs-hide { display: none !important; } 491 | /* 492 | * jQuery UI Datepicker 1.8.16 493 | * 494 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 495 | * Dual licensed under the MIT or GPL Version 2 licenses. 496 | * http://jquery.org/license 497 | * 498 | * http://docs.jquery.com/UI/Datepicker#theming 499 | */ 500 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } 501 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } 502 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } 503 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } 504 | .ui-datepicker .ui-datepicker-prev { left:2px; } 505 | .ui-datepicker .ui-datepicker-next { right:2px; } 506 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; } 507 | .ui-datepicker .ui-datepicker-next-hover { right:1px; } 508 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } 509 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } 510 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 511 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 512 | .ui-datepicker select.ui-datepicker-month, 513 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 514 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 515 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } 516 | .ui-datepicker td { border: 0; padding: 1px; } 517 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 518 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } 519 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 520 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 521 | 522 | /* with multiple calendars */ 523 | .ui-datepicker.ui-datepicker-multi { width:auto; } 524 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 525 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 526 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 527 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 528 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 529 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 530 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 531 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 532 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } 533 | 534 | /* RTL support */ 535 | .ui-datepicker-rtl { direction: rtl; } 536 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 537 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 538 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 539 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 540 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 541 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 542 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 543 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 544 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 545 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 546 | 547 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 548 | .ui-datepicker-cover { 549 | display: none; /*sorry for IE5*/ 550 | display/**/: block; /*sorry for IE5*/ 551 | position: absolute; /*must have*/ 552 | z-index: -1; /*must have*/ 553 | filter: mask(); /*must have*/ 554 | top: -4px; /*must have*/ 555 | left: -4px; /*must have*/ 556 | width: 200px; /*must have*/ 557 | height: 200px; /*must have*/ 558 | }/* 559 | * jQuery UI Progressbar 1.8.16 560 | * 561 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 562 | * Dual licensed under the MIT or GPL Version 2 licenses. 563 | * http://jquery.org/license 564 | * 565 | * http://docs.jquery.com/UI/Progressbar#theming 566 | */ 567 | .ui-progressbar { height:2em; text-align: left; } 568 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } -------------------------------------------------------------------------------- /editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gantt Chart editor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 44 | 45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /editor/jsganttchart.editor.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | (function ($, _, Backbone, JSGanttChart) { 3 | 'use strict'; 4 | 5 | var ToolbarView = Backbone.View.extend({ 6 | className: "toolbar", 7 | $el: undefined, 8 | 9 | initialize: function () { 10 | this.$el = $(this.el); 11 | }, 12 | 13 | render: function () { 14 | var this_ = this; 15 | $.fn.append.apply(this.$el.html(""), _(this.options.buttons).map(function (button) { 16 | return jQuery('') 17 | .click(function () { 18 | button.action.call(this_); 19 | }); 20 | })); 21 | return this; 22 | } 23 | }), 24 | 25 | FieldSetView = Backbone.View.extend({ 26 | className: "fieldset", 27 | tagName: "table", 28 | $el: undefined, 29 | fieldInputs: undefined, 30 | model: undefined, 31 | initialize: function () { 32 | this.$el = $(this.el); 33 | }, 34 | render: function () { 35 | var this_ = this; 36 | this.fieldInputs = []; 37 | $.fn.append.apply(this.$el.html(""), _(this.options.fields).map(function (field) { 38 | var html = field.type === "textarea" ? '' : '', 39 | row = jQuery('' + field.name + '' + html + ''), 40 | input = row.find("input, textarea"); 41 | this_.fieldInputs.push(input); 42 | switch (field.type) { 43 | case "date": 44 | input.datepicker({ 45 | dateformar: "D d M yy" 46 | }); 47 | break; 48 | } 49 | return row; 50 | })); 51 | return this; 52 | }, 53 | load: function (model) { 54 | var this_ = this; 55 | this.model = model; 56 | _(this.options.fields).each(function (field, i) { 57 | this_.fieldInputs[i].val(field.load(model)); 58 | }); 59 | }, 60 | save: function() { 61 | var this_ = this, 62 | newsettings = {}; 63 | _(this.options.fields).each(function (field, i) { 64 | _(newsettings).extend(field.save(this_.model, this_.fieldInputs[i].val())); 65 | }); 66 | this.model.save(newsettings); 67 | } 68 | }), 69 | 70 | DialogView = Backbone.View.extend({ 71 | className: "dialog", 72 | $el: undefined, 73 | initialize: function () { 74 | this.dialogOptions = { 75 | autoOpen: false, 76 | show: "fade", 77 | hide: "fade" 78 | }; 79 | this.$el = $(this.el); 80 | }, 81 | show: function () { 82 | this.$el.dialog("open"); 83 | }, 84 | hide: function () { 85 | this.$el.dialog("close"); 86 | }, 87 | render: function () { 88 | this.$el.dialog(this.dialogOptions); 89 | return this; 90 | } 91 | }), 92 | 93 | EditDialogView = DialogView.extend({ 94 | fieldset: undefined, 95 | model: undefined, 96 | render: function () { 97 | var this_ = this, 98 | apply = function () { this_.fieldset.save(); } 99 | 100 | _(this.dialogOptions).extend({ 101 | buttons: { 102 | Apply: function () { apply.call(); }, 103 | OK: function () { 104 | apply.call(); 105 | this_.hide(); 106 | }, 107 | Cancel: function () { this_.hide(); }, 108 | Delete: function () { 109 | this_.model.destroy(); 110 | this_.hide(); 111 | } 112 | }, 113 | title: "Add / Edit stage", 114 | resizable: false, 115 | width: 600 116 | }); 117 | 118 | this.fieldset = new FieldSetView({ 119 | fields: [ 120 | { 121 | name: "ID", 122 | load: function (model) { return model.get("id"); }, 123 | save: function (model, value) { 124 | return { id: value }; 125 | model.collection.sort(); 126 | } }, 127 | { 128 | name: "Order", 129 | load: function (model) { return model.get("order"); }, 130 | save: function (model, value) { return { order: parseInt(value) }; } }, 131 | { 132 | name: "Name", 133 | load: function (model) { return model.get("name"); }, 134 | save: function (model, value) { return { name: value }; } }, 135 | { 136 | name: "Description", 137 | type: "textarea", 138 | load: function (model) { return model.get("description"); }, 139 | save: function (model, value) { return { description: value }; } }, 140 | { 141 | name: "Start date", 142 | type: "date", 143 | load: function (model) { return model.get("startDate"); }, 144 | save: function (model, value) { return { startDate: value ? new Date(value) : undefined }; } }, 145 | { 146 | name: "End date", 147 | type: "date", 148 | load: function (model) { return model.get("endDate"); }, 149 | save: function (model, value) { return { endDate: value ? new Date(value) : undefined }; } }, 150 | { 151 | name: "Slack end date", 152 | type: "date", 153 | load: function (model) { return model.get("slackEndDate"); }, 154 | save: function (model, value) { return { slackEndDate: value ? new Date(value) : undefined }; } }, 155 | { 156 | name: "Type", 157 | load: function (model) { return model.get("type"); }, 158 | save: function (model, value) { return { type: value }; } }, 159 | { 160 | name: "Parent", 161 | load: function (model) { return model.get("parentElement"); }, 162 | save: function (model, value) { console.log({ parentElement: value.trim() === "" ? undefined : value.trim() });return { parentElement: value.trim() === "" ? undefined : value.trim() }; } }, 163 | { 164 | name: "Percentage done", 165 | load: function (model) { return model.get("percentageDone"); }, 166 | save: function (model, value) { return { percentageDone: value === undefined ? undefined : parseInt(value) }; } }, 167 | { 168 | name: "Hours expected", 169 | load: function (model) { return model.get("estimatedHours"); }, 170 | save: function (model, value) { return { estimatedHours: value === undefined ? undefined : parseInt(value) }; } }, 171 | { 172 | name: "Resource (comma separated)", 173 | load: function (model) { return (model.get("resources") || []).join(", "); }, 174 | save: function (model, value) { 175 | return { resources: _(value.split(",")).chain() 176 | .map(function (r) { return r.trim(); }) 177 | .reject(function (r) { return r === ""; }).value() }; 178 | } 179 | }, 180 | { 181 | name: "Predecessors (dependancies,
comma separated)", 182 | load: function (model) { return (model.get("predecessors") || []).join(", "); }, 183 | save: function (model, value) { 184 | return { predecessors: _(value.split(",")).chain() 185 | .map(function (r) { return r.trim(); }) 186 | .reject(function (r) { return r === ""; }).value() }; 187 | } 188 | }, 189 | { 190 | name: "Icons", 191 | type: "textarea", 192 | load: function (model) { return JSON.stringify(model.get("icons") || []); }, 193 | save: function (model, value) { 194 | var icons = JSON.parse(value); 195 | _(icons).each(function (icon) { 196 | icon.date = new Date(icon.date); 197 | }); 198 | return { icons: icons }; 199 | } 200 | } 201 | ] 202 | }); 203 | 204 | this.$el.html("").append(this.fieldset.render().el); 205 | return DialogView.prototype.render.apply(this); 206 | }, 207 | load: function (model) { 208 | this.model = model; 209 | this.fieldset.load(model); 210 | } 211 | }), 212 | 213 | JSONDialogView = DialogView.extend({ 214 | textarea: undefined, 215 | 216 | render: function () { 217 | var this_ = this, 218 | textarea = jQuery(""), 219 | apply = function () { 220 | this_.options.gantt.setJSON(JSON.parse(this_.val())); 221 | }, 222 | toolbar = new ToolbarView({ 223 | buttons: [ 224 | { 225 | name: "Apply", 226 | action: function () { 227 | apply.call(); 228 | } 229 | }, 230 | { 231 | name: "OK", 232 | action: function () { 233 | apply.call(); 234 | this_.hide(); 235 | } 236 | }, 237 | { 238 | name: "Cancel", 239 | action: function () { 240 | this_.hide(); 241 | } 242 | }, 243 | ] 244 | }); 245 | 246 | this.textarea = textarea; 247 | 248 | this.$el.html("").append(textarea, toolbar.render().el).hide(); 249 | 250 | return DialogView.prototype.render.apply(this); 251 | }, 252 | val: function () { return $.fn.val.apply(this.textarea, arguments); } 253 | }), 254 | 255 | EditorView = Backbone.View.extend({ 256 | className: "editor", 257 | $el: undefined, 258 | gantt: undefined, 259 | editDialog: undefined, 260 | jsonDialog: undefined, 261 | toolbar: undefined, 262 | 263 | initialize: function () { 264 | var this_ = this; 265 | this.$el = $(this.el); 266 | this.gantt = this.options.gantt; 267 | this.editDialog = new EditDialogView(); 268 | this.jsonDialog = new JSONDialogView({ gantt: this.gantt }); 269 | this.toolbar = new ToolbarView({ 270 | buttons: [ 271 | { 272 | name: "New Stage", 273 | action: function () { 274 | this_.editDialog.load(this_.gantt.newElementModel()); 275 | this_.editDialog.show(); 276 | } 277 | }, 278 | { 279 | name: "View Resources", 280 | action: function () { 281 | this_.resourcesDialog.show(); 282 | } 283 | }, 284 | { 285 | name: "View Types", 286 | action: function () { 287 | this_.resourcesDialog.show(); 288 | } 289 | }, 290 | { 291 | name: "View/Edit JSON", 292 | action: function () { 293 | this_.jsonDialog.val(JSON.stringify(this_.gantt.getJSON(), undefined, " ")); 294 | this_.jsonDialog.show(); 295 | } 296 | } 297 | ] 298 | }); 299 | 300 | this.gantt.bind("row_click", function (e, model) { 301 | e.preventDefault(); 302 | e.stopPropagation(); 303 | this_.editDialog.load(model); 304 | this_.editDialog.show(); 305 | }); 306 | }, 307 | render: function () { 308 | this.$el.html("").append(this.gantt.render().el, this.toolbar.render().el); 309 | this.editDialog.render(); 310 | this.jsonDialog.render(); 311 | return this; 312 | } 313 | }), 314 | 315 | editor; 316 | 317 | JSGanttChart.Editor = function (options) { 318 | editor = new EditorView({ gantt: options.gantt }); 319 | }; 320 | 321 | _(JSGanttChart.Editor).extend({ 322 | create: function () { 323 | var F = function () {}, // Dummy function 324 | o; 325 | F.prototype = JSGanttChart.Editor.prototype; 326 | o = new F(); 327 | JSGanttChart.Editor.apply(o, arguments); 328 | o.constructor = JSGanttChart.Editor; 329 | return o; 330 | } 331 | }); 332 | 333 | _(JSGanttChart.Editor.prototype).extend(Backbone.Events, { 334 | render: function () { 335 | return editor.render(); 336 | } 337 | }); 338 | 339 | }(jQuery, _, Backbone, JSGanttChart)); -------------------------------------------------------------------------------- /editor/style.css: -------------------------------------------------------------------------------- 1 | /* Editor elements */ 2 | 3 | .editor { 4 | margin-top: 50px; 5 | } 6 | 7 | body, table, input { 8 | font-family: verdana, sans-serif; 9 | font-size: 11px; 10 | color: #333; 11 | } 12 | 13 | .editor > .toolbar { 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | background: #f7f7f6; 19 | } 20 | 21 | .toolbar { 22 | text-align: center; 23 | } 24 | 25 | .fieldset th { 26 | text-align: right; 27 | } 28 | 29 | .fieldset, .fieldset td, .fieldset th { 30 | margin: 0; 31 | padding: 1px; 32 | border: 0; 33 | } 34 | 35 | .toolbar { 36 | padding: 5px; 37 | } 38 | 39 | .toolbar input { 40 | border: 1px solid #ccc; 41 | background: #e7e7e7; 42 | font-size: 11px; 43 | padding: 4px; 44 | } 45 | 46 | #editform .form, #jsonform .form { 47 | margin: 0 auto; 48 | width: 100%; 49 | height: 100%; 50 | } 51 | 52 | #jsonform .form { 53 | width: 99%; 54 | height: 100%; 55 | } 56 | 57 | #editform .form > table { 58 | margin: 0 auto; 59 | } 60 | 61 | textarea { 62 | width: 100%; 63 | } 64 | 65 | .gantt-container tr { 66 | cursor: pointer; 67 | } 68 | -------------------------------------------------------------------------------- /gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlamingTempura/JSGanttChart/f8a1355c5ee9d40a8741cf7c2926d55a88a1ede7/gantt.png -------------------------------------------------------------------------------- /jsganttchart.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true*/ 2 | (function ($, _, Backbone) { 3 | 'use strict'; 4 | 5 | var root = window, 6 | 7 | jsgtThis, 8 | 9 | ganttView, 10 | collection, 11 | 12 | monthNames = [ "January", "February", "March", "April", "May", "June", 13 | "July", "August", "September", "October", "November", "December" ], 14 | 15 | fieldNames = { 16 | name: 'Project stage', 17 | resources: "Resources", 18 | percentageDone: "Status", 19 | estimatedHours: 'Estim. Hours' 20 | }, 21 | 22 | mandatoryFields = ['id', 'name', 'startDate'], 23 | 24 | GanttElementModel = Backbone.Model.extend({ 25 | defaults: { 26 | order: 0, 27 | name: undefined, 28 | description: undefined, 29 | startDate: undefined, 30 | endDate: undefined, 31 | slackEndDate: undefined, 32 | type: undefined, 33 | percentageDone: undefined, 34 | hoursExpected: undefined, 35 | resources: undefined, 36 | predecessors: undefined, 37 | icons: undefined 38 | }, 39 | 40 | collection: undefined, 41 | 42 | initialize: function (model, options) { 43 | this.collection = options.collection; 44 | this.normalize(); 45 | if (this.collection.hasOwnProperty("localStorage")) { 46 | this.save(); 47 | } 48 | }, 49 | 50 | normalize: function () { 51 | var this_ = this; 52 | // Ensure the element has all mandatory fields 53 | _(mandatoryFields).each(function (field) { 54 | if (!this_.has(field)) { 55 | throw "element " + this_.get("id") + " is missing " + field + "."; 56 | } 57 | }); 58 | 59 | if (!_(this.get("startDate")).isDate()) { 60 | this.set({ startDate: new Date(this.get("startDate")) }); 61 | } 62 | 63 | if (this.has("endDate") && !_(this.get("endDate")).isDate()) { 64 | this.set({ endDate: new Date(this.get("endDate")) }); 65 | } 66 | 67 | if (!this.has("endDate")) { 68 | this.set({ endDate: new Date(this.get("startDate").getTime()) }); 69 | if (this.has("duration")) { 70 | this.get("endDate").setDate(this.get("startDate").getDate() + this.get("duration")); 71 | this.unset("duration"); 72 | } else { 73 | this.get("endDate").setDate(this.get("startDate").getDate() + 1); 74 | } 75 | } 76 | 77 | if (this.has("slackEndDate")) { 78 | if (!_(this.get("slackEndDate")).isDate()) { 79 | this.set({ slackEndDate: new Date(this.get("slackEndDate")) }); 80 | } 81 | } else if (this.has("slackDuration")) { 82 | var date = new Date(this.get("endDate")); 83 | date.setDate(date.getDate() + this.get("slackDuration")); 84 | this.set({ slackEndDate: date }); 85 | } 86 | 87 | if (this.has("elements")) { 88 | this.collection.add(_(this.get("elements")).map(function (el) { 89 | var element = _(el).clone(); 90 | element.parentElement = this_.get("id"); 91 | return element; 92 | })); 93 | this.unset("elements"); 94 | } 95 | 96 | if (this.has("icons")) { 97 | _(this.get("icons")).each(function (icon) { 98 | if (!_(icon.date).isDate()) { 99 | icon.date = new Date(icon.date); 100 | } 101 | }); 102 | } 103 | } 104 | }), 105 | 106 | GanttElementCollection = Backbone.Collection.extend({ 107 | model: GanttElementModel, 108 | initialize: function (models, options) { 109 | var this_ = this, 110 | triggerChange = function () { this_.trigger("change"); }; 111 | 112 | this.bind("add", triggerChange); 113 | this.bind("remove", triggerChange); 114 | this.bind("reset", triggerChange); 115 | }, 116 | 117 | add: function (models) { 118 | var i, 119 | l; 120 | if (_.isArray(models)) { 121 | for (i = 0, l = models.length; i < l; i = i + 1) { 122 | this.add(models[i]); 123 | } 124 | } else { 125 | Backbone.Collection.prototype.add.call(this, models, { at: this.length }); // Order properly 126 | } 127 | return this; 128 | }, 129 | 130 | sort: function (model) { 131 | var this_ = this; 132 | this.reset(collection.sortBy(function (model) { 133 | var order; 134 | if (model.has("parentElement") && this_.get(model.get("parentElement"))) { /// HACK 135 | order = this_.get(model.get("parentElement")).get("order") + (0.00001 * model.get("order") + 0.00001); 136 | } else { 137 | order = model.get("order"); 138 | } 139 | console.log(model.get("name"), "Order", order, "Parent", model.get("parentElement")) 140 | return order; 141 | })); 142 | } 143 | }), 144 | 145 | /* options: 146 | fields 147 | */ 148 | DataTableView = Backbone.View.extend({ 149 | className: "gantt-data-table", 150 | tagName: "table", 151 | $el: undefined, 152 | 153 | initialize: function () { 154 | _.bindAll(this, "render"); 155 | this.options.collection.bind("change", this.render); 156 | this.$el = $(this.el); 157 | }, 158 | 159 | highlight: function (model) { 160 | if (model) { 161 | this.$el.find('.jsgt_' + model.get("id")) 162 | .addClass("highlight") 163 | .siblings() 164 | .removeClass("highlight"); 165 | } else { 166 | this.$el.find('tr').removeClass("highlight"); 167 | } 168 | }, 169 | 170 | render: function () { 171 | var this_ = this; 172 | 173 | this.$el.html(''); 174 | 175 | // Populate headers 176 | this.$el.append($.fn.append.apply($(''), _(this_.options.fields).map(function (field) { 177 | return $('' + fieldNames[field] + ''); 178 | }))); 179 | 180 | // Populate data 181 | $.fn.append.apply(this.$el, this.options.collection.map(function (model) { 182 | var row = $(''); 183 | return $.fn.append.apply(row, _(this_.options.fields).map(function (field) { 184 | var str = (model.has(field) ? model.get(field) : ''); 185 | if (field === "name" && model.get("parentElement") && model.get("parentElement").trim()) { 186 | str = "    " + str; 187 | row.addClass("child"); 188 | } else if (field === "resources") { 189 | str = _(model.get(field)).reduce(function (memo, resource) { 190 | return (memo ? memo + ", " : "") + this_.options.resources[resource]; 191 | }, ""); 192 | } else if (field === "percentageDone") { 193 | if (str === 100) { 194 | str = '
Done
'; 195 | } else if (!str || str === 0) { 196 | str = '
Not started
'; 197 | } else { 198 | str = '
In progress: ' + str + '%
'; 199 | } 200 | } 201 | return $('' + (str || " ") + ''); 202 | })) 203 | .click(function (e) { this_.trigger("row_click", e, model); }) 204 | .mouseenter(function (e) { this_.trigger("row_enter", e, model); }) 205 | .mouseleave(function (e) { this_.trigger("row_leave", e, model); }); 206 | })); 207 | 208 | return this; 209 | } 210 | }), 211 | 212 | GanttElementView = Backbone.View.extend({ 213 | className: "gantt-element", 214 | $el: undefined, 215 | 216 | initialize: function () { 217 | _.bindAll(this, "render"); 218 | this.options.model.bind("change", this.render); 219 | this.$el = $(this.el); 220 | }, 221 | 222 | render: function () { 223 | var this_ = this, 224 | model = this.options.model, 225 | noOfDays = Math.round((model.get("endDate").getTime() - model.get("startDate").getTime()) / (24 * 60 * 60 * 1000)), 226 | dayFromStart = Math.round((model.get("startDate").getTime() - this.options.firstDate.getTime()) / (24 * 60 * 60 * 1000)), 227 | el; 228 | 229 | this.$el.css({ width: noOfDays * 23 - 3 }); 230 | 231 | if (model.has("type") && this.options.types.hasOwnProperty(model.get("type"))) { 232 | this.$el.css({ borderBottomColor: this.options.types[model.get("type")].color }); 233 | } 234 | 235 | if (model.has("percentageDone") && model.get("percentageDone") > 0) { 236 | el = $('
'); 237 | el.css({ width: model.get("percentageDone") + "%" }); 238 | this.$el.append(el, $('
' + (model.get("percentageDone") < 100 ? model.get("percentageDone") + "% done" : "Done") + '
')); 239 | } 240 | 241 | if (model.has("slackEndDate")) { 242 | el = $('
'); 243 | noOfDays = Math.round((model.get("slackEndDate").getTime() - model.get("endDate").getTime()) / (24 * 60 * 60 * 1000)); 244 | el.css({ left: "100%", width: noOfDays * 23 }); 245 | this.$el.append(el); 246 | } 247 | 248 | if (model.has("predecessors")) { 249 | $.fn.append.apply(this.$el, _(model.get("predecessors")).map(function (predecessor) { 250 | var predecessorModel = this_.options.collection.get(predecessor); 251 | if (predecessorModel) { 252 | el = $('
'); 253 | noOfDays = Math.round((model.get("startDate").getTime() - predecessorModel.get("endDate").getTime()) / (24 * 60 * 60 * 1000)); 254 | var noOfRows = collection.indexOf(model) - collection.indexOf(predecessorModel); 255 | el.css({ right: "100%", bottom: "100%", width: noOfDays * 23, height: noOfRows * 17 - 5 }); 256 | return el; 257 | } 258 | })); 259 | } 260 | 261 | return this; 262 | } 263 | }), 264 | 265 | GanttTableView = Backbone.View.extend({ 266 | className: "gantt-table", 267 | tagName: "table", 268 | $el: undefined, 269 | 270 | initialize: function () { 271 | _.bindAll(this, "render"); 272 | this.options.collection.bind("change", this.render); 273 | this.$el = $(this.el); 274 | }, 275 | 276 | highlight: function (model) { 277 | if (model) { 278 | this.$el.find('.jsgt_' + model.get("id")) 279 | .addClass("highlight") 280 | .siblings() 281 | .removeClass("highlight"); 282 | } else { 283 | this.$el.find('tr').removeClass("highlight"); 284 | } 285 | }, 286 | 287 | render: function () { 288 | this.$el.html(''); 289 | var this_ = this, 290 | firstDate, 291 | lastDate, 292 | dateIterator, 293 | today = new Date(); 294 | 295 | // Determine when the gantt chart starts and finishes 296 | this.options.collection.each(function (model) { 297 | var startDate = model.get("startDate").getTime(), 298 | endDate = model.get("endDate").getTime(); 299 | firstDate = (!firstDate || startDate < firstDate) ? startDate : firstDate; 300 | lastDate = (!lastDate || endDate > lastDate) ? endDate : lastDate; 301 | }); 302 | 303 | firstDate = new Date(firstDate); 304 | lastDate = new Date(lastDate); 305 | 306 | var monthRow = $(''), 307 | dayRow = $(''), 308 | currMonth, 309 | currMonthSize, 310 | currMonthEl; 311 | 312 | dateIterator = new Date(firstDate.getTime()); 313 | // Populate days 314 | while (dateIterator <= lastDate) { 315 | if (dateIterator.getMonth() !== currMonth) { 316 | if (currMonthEl) { 317 | currMonthEl.attr({ colspan: currMonthSize }); 318 | } 319 | currMonth = dateIterator.getMonth(); 320 | currMonthSize = 0; 321 | currMonthEl = $('' + monthNames[dateIterator.getMonth()] + ' ' + dateIterator.getFullYear() + ''); 322 | monthRow.append(currMonthEl); 323 | } 324 | var el = $('' + dateIterator.getDate() + ''), 325 | dateString = dateIterator.toDateString(); 326 | 327 | if (today.toDateString() === dateString) { 328 | el.addClass("important"); 329 | } 330 | if (dateIterator.getDay() === 6) { 331 | el.addClass("markend"); 332 | } 333 | this.options.collection.map(function (model) { 334 | if (model.has("icons")) { 335 | model.get("icons").map(function (icon) { 336 | if (icon.date.toDateString() === dateString) { 337 | el.append('
'); 338 | } 339 | }); 340 | } 341 | }); 342 | dayRow.append(el); 343 | dateIterator.setDate(dateIterator.getDate() + 1); 344 | currMonthSize = currMonthSize + 1; 345 | } 346 | if (currMonthEl) { 347 | currMonthEl.attr({ colspan: currMonthSize }); 348 | } 349 | this.$el.append(monthRow, dayRow); 350 | 351 | $.fn.append.apply(this.$el, this.options.collection.map(function (model) { 352 | var row = $(''), 353 | elementView = new GanttElementView({ 354 | model: model, 355 | firstDate: firstDate, 356 | types: this_.options.types, 357 | collection: this_.options.collection 358 | }), 359 | dateIterator = new Date(firstDate.getTime()), 360 | elementHolder = $('
 
'), 361 | modelDate = model.get("startDate"); 362 | 363 | if (model.has("parentElement")) { 364 | row.addClass("child"); 365 | } 366 | 367 | var html = "", 368 | classes = []; 369 | 370 | while (dateIterator <= lastDate) { 371 | classes = ""; 372 | 373 | if (dateIterator.getDay() === 6) { 374 | classes += " markend"; 375 | } 376 | if (dateIterator.getDay() === 6 || dateIterator.getDay() === 0) { 377 | classes += " weekend"; 378 | } 379 | html += '
'; 380 | 381 | _(model.get("icons")).each(function (icon) { 382 | if (icon.date.toDateString() === dateIterator.toDateString()) { 383 | html += '
' + icon.description + '
'; 384 | } 385 | }); 386 | 387 | html += '
'; 388 | 389 | dateIterator.setDate(dateIterator.getDate() + 1); 390 | } 391 | 392 | row.append(html); 393 | 394 | row.click(function (e) { this_.trigger("row_click", e, model); }) 395 | .mouseenter(function (e) { this_.trigger("row_enter", e, model); }) 396 | .mouseleave(function (e) { this_.trigger("row_leave", e, model); }) 397 | .find("." + modelDate.getDate() + "-" + modelDate.getMonth() + "-" + modelDate.getFullYear()) 398 | .append(elementHolder.append(elementView.render().el)); 399 | 400 | return row; 401 | })); 402 | 403 | return this; 404 | } 405 | }), 406 | 407 | KeyView = Backbone.View.extend({ 408 | className: "gantt-key", 409 | $el: undefined, 410 | 411 | initialize: function () { 412 | _.bindAll(this, "render"); 413 | this.$el = $(this.el); 414 | }, 415 | 416 | render: function () { 417 | this.$el.append("Key

"); 418 | 419 | $.fn.append.apply(this.$el, _(this.options.types).map(function (type) { 420 | return '
' + type.name + '
'; 421 | })); 422 | 423 | return this; 424 | } 425 | }), 426 | 427 | // Options: 428 | // collection: collection of type GanttElementCollection 429 | // displayKey 430 | // fields: array of fields 431 | // types: mapping of types to name+colour 432 | GanttContainerView = Backbone.View.extend({ 433 | className: "gantt-container", 434 | dataView: undefined, 435 | ganttView: undefined, 436 | keyView: undefined, 437 | $el: undefined, 438 | 439 | initialize: function () { 440 | var this_ = this; 441 | 442 | this.dataView = new DataTableView({ 443 | collection: this.options.collection, 444 | fields: this.options.fields, 445 | resources: this.options.resources 446 | }); 447 | this.ganttView = new GanttTableView({ 448 | collection: this.options.collection, 449 | types: this.options.types 450 | }); 451 | this.keyView = new KeyView({ types: this.options.types }); 452 | this.$el = $(this.el); 453 | 454 | var rowClick = function (e, model) { 455 | this_.trigger("row_click", e, model); 456 | }, 457 | rowEnter = function (e, model) { 458 | this_.dataView.highlight(model); 459 | this_.ganttView.highlight(model); 460 | this_.trigger("row_enter", e, model); 461 | }, 462 | rowLeave = function (e, model) { 463 | this_.dataView.highlight(); 464 | this_.ganttView.highlight(); 465 | this_.trigger("row_leave", e, model); 466 | }; 467 | 468 | this.dataView.bind("row_click", rowClick); 469 | this.ganttView.bind("row_click", rowClick); 470 | 471 | this.dataView.bind("row_enter", rowEnter); 472 | this.ganttView.bind("row_enter", rowEnter); 473 | this.dataView.bind("row_leave", rowLeave); 474 | this.ganttView.bind("row_leave", rowLeave); 475 | }, 476 | 477 | render: function () { 478 | var this_ = this; 479 | this.$el.html('') 480 | .append(this.dataView.render().el, this.ganttView.render().el); 481 | if (this.options.displayKey) { 482 | this.$el.append(this.keyView.render().el); 483 | } 484 | setTimeout(function () { 485 | console.log(this_.dataView.$el.outerWidth()) 486 | this_.ganttView.$el.css({ marginLeft: this_.dataView.$el.outerWidth() }); 487 | }); 488 | return this; 489 | } 490 | }), 491 | 492 | JSGanttChart = root.JSGanttChart = function (options) { 493 | jsgtThis = this; 494 | 495 | if (!options) { 496 | options = {}; 497 | } 498 | 499 | _(options).defaults({ 500 | displayKey: true, 501 | fields: [ "name", "resources", "percentageDone", "estimatedHours" ], 502 | types: {}, 503 | resources: {} 504 | }); 505 | 506 | collection = new GanttElementCollection(options.elements); 507 | 508 | if (options.hasOwnProperty("localStorage")) { 509 | collection.localStorage = options.localStorage; 510 | collection.fetch(); 511 | collection.sort(); 512 | } 513 | 514 | ganttView = new GanttContainerView({ 515 | collection: collection, 516 | displayKey: options.displayKey, 517 | fields: options.fields, 518 | types: options.types, 519 | resources: options.resources 520 | }); 521 | 522 | ganttView.bind("row_click", function (e, model) { 523 | jsgtThis.trigger("row_click", e, model); 524 | }); 525 | }; 526 | 527 | _(JSGanttChart).extend({ 528 | create: function () { 529 | var F = function () {}, // Dummy function 530 | o; 531 | F.prototype = JSGanttChart.prototype; 532 | o = new F(); 533 | JSGanttChart.apply(o, arguments); 534 | o.constructor = JSGanttChart; 535 | return o; 536 | } 537 | }); 538 | 539 | _(JSGanttChart.prototype).extend(Backbone.Events, { 540 | setElements: function (newelements) {}, 541 | 542 | setTypes: function (newtypes) {}, 543 | 544 | render: function () { 545 | return ganttView.render(); 546 | }, 547 | 548 | newElementModel: function () { 549 | var model = new GanttElementModel({ 550 | id: Math.round(Math.random() * 1000000), 551 | name: "New stage", 552 | startDate: new Date() 553 | }, { collection: collection }); 554 | collection.add(model); 555 | return model; 556 | }, 557 | 558 | getJSON: function () { 559 | return collection.toJSON(); 560 | }, 561 | 562 | setJSON: function (json) { 563 | collection.reset(json); 564 | } 565 | }); 566 | 567 | }(jQuery, _, Backbone)); -------------------------------------------------------------------------------- /lib/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.5.3 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | 7 | (function(){ 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // Save a reference to the global object. 13 | var root = this; 14 | 15 | // Save the previous value of the `Backbone` variable. 16 | var previousBackbone = root.Backbone; 17 | 18 | // The top-level namespace. All public Backbone classes and modules will 19 | // be attached to this. Exported for both CommonJS and the browser. 20 | var Backbone; 21 | if (typeof exports !== 'undefined') { 22 | Backbone = exports; 23 | } else { 24 | Backbone = root.Backbone = {}; 25 | } 26 | 27 | // Current version of the library. Keep in sync with `package.json`. 28 | Backbone.VERSION = '0.5.3'; 29 | 30 | // Require Underscore, if we're on the server, and it's not already present. 31 | var _ = root._; 32 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; 33 | 34 | // For Backbone's purposes, jQuery or Zepto owns the `$` variable. 35 | var $ = root.jQuery || root.Zepto; 36 | 37 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 38 | // to its previous owner. Returns a reference to this Backbone object. 39 | Backbone.noConflict = function() { 40 | root.Backbone = previousBackbone; 41 | return this; 42 | }; 43 | 44 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will 45 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 46 | // `X-Http-Method-Override` header. 47 | Backbone.emulateHTTP = false; 48 | 49 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 50 | // `application/json` requests ... will encode the body as 51 | // `application/x-www-form-urlencoded` instead and will send the model in a 52 | // form param named `model`. 53 | Backbone.emulateJSON = false; 54 | 55 | // Backbone.Events 56 | // ----------------- 57 | 58 | // A module that can be mixed in to *any object* in order to provide it with 59 | // custom events. You may `bind` or `unbind` a callback function to an event; 60 | // `trigger`-ing an event fires all callbacks in succession. 61 | // 62 | // var object = {}; 63 | // _.extend(object, Backbone.Events); 64 | // object.bind('expand', function(){ alert('expanded'); }); 65 | // object.trigger('expand'); 66 | // 67 | Backbone.Events = { 68 | 69 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 70 | // Passing `"all"` will bind the callback to all events fired. 71 | bind : function(ev, callback, context) { 72 | var calls = this._callbacks || (this._callbacks = {}); 73 | var list = calls[ev] || (calls[ev] = []); 74 | list.push([callback, context]); 75 | return this; 76 | }, 77 | 78 | // Remove one or many callbacks. If `callback` is null, removes all 79 | // callbacks for the event. If `ev` is null, removes all bound callbacks 80 | // for all events. 81 | unbind : function(ev, callback) { 82 | var calls; 83 | if (!ev) { 84 | this._callbacks = {}; 85 | } else if (calls = this._callbacks) { 86 | if (!callback) { 87 | calls[ev] = []; 88 | } else { 89 | var list = calls[ev]; 90 | if (!list) return this; 91 | for (var i = 0, l = list.length; i < l; i++) { 92 | if (list[i] && callback === list[i][0]) { 93 | list[i] = null; 94 | break; 95 | } 96 | } 97 | } 98 | } 99 | return this; 100 | }, 101 | 102 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 103 | // same arguments as `trigger` is, apart from the event name. 104 | // Listening for `"all"` passes the true event name as the first argument. 105 | trigger : function(eventName) { 106 | var list, calls, ev, callback, args; 107 | var both = 2; 108 | if (!(calls = this._callbacks)) return this; 109 | while (both--) { 110 | ev = both ? eventName : 'all'; 111 | if (list = calls[ev]) { 112 | for (var i = 0, l = list.length; i < l; i++) { 113 | if (!(callback = list[i])) { 114 | list.splice(i, 1); i--; l--; 115 | } else { 116 | args = both ? Array.prototype.slice.call(arguments, 1) : arguments; 117 | callback[0].apply(callback[1] || this, args); 118 | } 119 | } 120 | } 121 | } 122 | return this; 123 | } 124 | 125 | }; 126 | 127 | // Backbone.Model 128 | // -------------- 129 | 130 | // Create a new model, with defined attributes. A client id (`cid`) 131 | // is automatically generated and assigned for you. 132 | Backbone.Model = function(attributes, options) { 133 | var defaults; 134 | attributes || (attributes = {}); 135 | if (defaults = this.defaults) { 136 | if (_.isFunction(defaults)) defaults = defaults.call(this); 137 | attributes = _.extend({}, defaults, attributes); 138 | } 139 | this.attributes = {}; 140 | this._escapedAttributes = {}; 141 | this.cid = _.uniqueId('c'); 142 | this.set(attributes, {silent : true}); 143 | this._changed = false; 144 | this._previousAttributes = _.clone(this.attributes); 145 | if (options && options.collection) this.collection = options.collection; 146 | this.initialize(attributes, options); 147 | }; 148 | 149 | // Attach all inheritable methods to the Model prototype. 150 | _.extend(Backbone.Model.prototype, Backbone.Events, { 151 | 152 | // A snapshot of the model's previous attributes, taken immediately 153 | // after the last `"change"` event was fired. 154 | _previousAttributes : null, 155 | 156 | // Has the item been changed since the last `"change"` event? 157 | _changed : false, 158 | 159 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 160 | // CouchDB users may want to set this to `"_id"`. 161 | idAttribute : 'id', 162 | 163 | // Initialize is an empty function by default. Override it with your own 164 | // initialization logic. 165 | initialize : function(){}, 166 | 167 | // Return a copy of the model's `attributes` object. 168 | toJSON : function() { 169 | return _.clone(this.attributes); 170 | }, 171 | 172 | // Get the value of an attribute. 173 | get : function(attr) { 174 | return this.attributes[attr]; 175 | }, 176 | 177 | // Get the HTML-escaped value of an attribute. 178 | escape : function(attr) { 179 | var html; 180 | if (html = this._escapedAttributes[attr]) return html; 181 | var val = this.attributes[attr]; 182 | return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); 183 | }, 184 | 185 | // Returns `true` if the attribute contains a value that is not null 186 | // or undefined. 187 | has : function(attr) { 188 | return this.attributes[attr] != null; 189 | }, 190 | 191 | // Set a hash of model attributes on the object, firing `"change"` unless you 192 | // choose to silence it. 193 | set : function(attrs, options) { 194 | 195 | // Extract attributes and options. 196 | options || (options = {}); 197 | if (!attrs) return this; 198 | if (attrs.attributes) attrs = attrs.attributes; 199 | var now = this.attributes, escaped = this._escapedAttributes; 200 | 201 | // Run validation. 202 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 203 | 204 | // Check for changes of `id`. 205 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 206 | 207 | // We're about to start triggering change events. 208 | var alreadyChanging = this._changing; 209 | this._changing = true; 210 | 211 | // Update attributes. 212 | for (var attr in attrs) { 213 | var val = attrs[attr]; 214 | if (!_.isEqual(now[attr], val)) { 215 | now[attr] = val; 216 | delete escaped[attr]; 217 | this._changed = true; 218 | if (!options.silent) this.trigger('change:' + attr, this, val, options); 219 | } 220 | } 221 | 222 | // Fire the `"change"` event, if the model has been changed. 223 | if (!alreadyChanging && !options.silent && this._changed) this.change(options); 224 | this._changing = false; 225 | return this; 226 | }, 227 | 228 | // Remove an attribute from the model, firing `"change"` unless you choose 229 | // to silence it. `unset` is a noop if the attribute doesn't exist. 230 | unset : function(attr, options) { 231 | if (!(attr in this.attributes)) return this; 232 | options || (options = {}); 233 | var value = this.attributes[attr]; 234 | 235 | // Run validation. 236 | var validObj = {}; 237 | validObj[attr] = void 0; 238 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 239 | 240 | // Remove the attribute. 241 | delete this.attributes[attr]; 242 | delete this._escapedAttributes[attr]; 243 | if (attr == this.idAttribute) delete this.id; 244 | this._changed = true; 245 | if (!options.silent) { 246 | this.trigger('change:' + attr, this, void 0, options); 247 | this.change(options); 248 | } 249 | return this; 250 | }, 251 | 252 | // Clear all attributes on the model, firing `"change"` unless you choose 253 | // to silence it. 254 | clear : function(options) { 255 | options || (options = {}); 256 | var attr; 257 | var old = this.attributes; 258 | 259 | // Run validation. 260 | var validObj = {}; 261 | for (attr in old) validObj[attr] = void 0; 262 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 263 | 264 | this.attributes = {}; 265 | this._escapedAttributes = {}; 266 | this._changed = true; 267 | if (!options.silent) { 268 | for (attr in old) { 269 | this.trigger('change:' + attr, this, void 0, options); 270 | } 271 | this.change(options); 272 | } 273 | return this; 274 | }, 275 | 276 | // Fetch the model from the server. If the server's representation of the 277 | // model differs from its current attributes, they will be overriden, 278 | // triggering a `"change"` event. 279 | fetch : function(options) { 280 | options || (options = {}); 281 | var model = this; 282 | var success = options.success; 283 | options.success = function(resp, status, xhr) { 284 | if (!model.set(model.parse(resp, xhr), options)) return false; 285 | if (success) success(model, resp); 286 | }; 287 | options.error = wrapError(options.error, model, options); 288 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 289 | }, 290 | 291 | // Set a hash of model attributes, and sync the model to the server. 292 | // If the server returns an attributes hash that differs, the model's 293 | // state will be `set` again. 294 | save : function(attrs, options) { 295 | options || (options = {}); 296 | if (attrs && !this.set(attrs, options)) return false; 297 | var model = this; 298 | var success = options.success; 299 | options.success = function(resp, status, xhr) { 300 | if (!model.set(model.parse(resp, xhr), options)) return false; 301 | if (success) success(model, resp, xhr); 302 | }; 303 | options.error = wrapError(options.error, model, options); 304 | var method = this.isNew() ? 'create' : 'update'; 305 | return (this.sync || Backbone.sync).call(this, method, this, options); 306 | }, 307 | 308 | // Destroy this model on the server if it was already persisted. Upon success, the model is removed 309 | // from its collection, if it has one. 310 | destroy : function(options) { 311 | options || (options = {}); 312 | if (this.isNew()) return this.trigger('destroy', this, this.collection, options); 313 | var model = this; 314 | var success = options.success; 315 | options.success = function(resp) { 316 | model.trigger('destroy', model, model.collection, options); 317 | if (success) success(model, resp); 318 | }; 319 | options.error = wrapError(options.error, model, options); 320 | return (this.sync || Backbone.sync).call(this, 'delete', this, options); 321 | }, 322 | 323 | // Default URL for the model's representation on the server -- if you're 324 | // using Backbone's restful methods, override this to change the endpoint 325 | // that will be called. 326 | url : function() { 327 | var base = getUrl(this.collection) || this.urlRoot || urlError(); 328 | if (this.isNew()) return base; 329 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); 330 | }, 331 | 332 | // **parse** converts a response into the hash of attributes to be `set` on 333 | // the model. The default implementation is just to pass the response along. 334 | parse : function(resp, xhr) { 335 | return resp; 336 | }, 337 | 338 | // Create a new model with identical attributes to this one. 339 | clone : function() { 340 | return new this.constructor(this); 341 | }, 342 | 343 | // A model is new if it has never been saved to the server, and lacks an id. 344 | isNew : function() { 345 | return this.id == null; 346 | }, 347 | 348 | // Call this method to manually fire a `change` event for this model. 349 | // Calling this will cause all objects observing the model to update. 350 | change : function(options) { 351 | this.trigger('change', this, options); 352 | this._previousAttributes = _.clone(this.attributes); 353 | this._changed = false; 354 | }, 355 | 356 | // Determine if the model has changed since the last `"change"` event. 357 | // If you specify an attribute name, determine if that attribute has changed. 358 | hasChanged : function(attr) { 359 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 360 | return this._changed; 361 | }, 362 | 363 | // Return an object containing all the attributes that have changed, or false 364 | // if there are no changed attributes. Useful for determining what parts of a 365 | // view need to be updated and/or what attributes need to be persisted to 366 | // the server. 367 | changedAttributes : function(now) { 368 | now || (now = this.attributes); 369 | var old = this._previousAttributes; 370 | var changed = false; 371 | for (var attr in now) { 372 | if (!_.isEqual(old[attr], now[attr])) { 373 | changed = changed || {}; 374 | changed[attr] = now[attr]; 375 | } 376 | } 377 | return changed; 378 | }, 379 | 380 | // Get the previous value of an attribute, recorded at the time the last 381 | // `"change"` event was fired. 382 | previous : function(attr) { 383 | if (!attr || !this._previousAttributes) return null; 384 | return this._previousAttributes[attr]; 385 | }, 386 | 387 | // Get all of the attributes of the model at the time of the previous 388 | // `"change"` event. 389 | previousAttributes : function() { 390 | return _.clone(this._previousAttributes); 391 | }, 392 | 393 | // Run validation against a set of incoming attributes, returning `true` 394 | // if all is well. If a specific `error` callback has been passed, 395 | // call that instead of firing the general `"error"` event. 396 | _performValidation : function(attrs, options) { 397 | var error = this.validate(attrs); 398 | if (error) { 399 | if (options.error) { 400 | options.error(this, error, options); 401 | } else { 402 | this.trigger('error', this, error, options); 403 | } 404 | return false; 405 | } 406 | return true; 407 | } 408 | 409 | }); 410 | 411 | // Backbone.Collection 412 | // ------------------- 413 | 414 | // Provides a standard collection class for our sets of models, ordered 415 | // or unordered. If a `comparator` is specified, the Collection will maintain 416 | // its models in sort order, as they're added and removed. 417 | Backbone.Collection = function(models, options) { 418 | options || (options = {}); 419 | if (options.comparator) this.comparator = options.comparator; 420 | _.bindAll(this, '_onModelEvent', '_removeReference'); 421 | this._reset(); 422 | if (models) this.reset(models, {silent: true}); 423 | this.initialize.apply(this, arguments); 424 | }; 425 | 426 | // Define the Collection's inheritable methods. 427 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 428 | 429 | // The default model for a collection is just a **Backbone.Model**. 430 | // This should be overridden in most cases. 431 | model : Backbone.Model, 432 | 433 | // Initialize is an empty function by default. Override it with your own 434 | // initialization logic. 435 | initialize : function(){}, 436 | 437 | // The JSON representation of a Collection is an array of the 438 | // models' attributes. 439 | toJSON : function() { 440 | return this.map(function(model){ return model.toJSON(); }); 441 | }, 442 | 443 | // Add a model, or list of models to the set. Pass **silent** to avoid 444 | // firing the `added` event for every new model. 445 | add : function(models, options) { 446 | if (_.isArray(models)) { 447 | for (var i = 0, l = models.length; i < l; i++) { 448 | this._add(models[i], options); 449 | } 450 | } else { 451 | this._add(models, options); 452 | } 453 | return this; 454 | }, 455 | 456 | // Remove a model, or a list of models from the set. Pass silent to avoid 457 | // firing the `removed` event for every model removed. 458 | remove : function(models, options) { 459 | if (_.isArray(models)) { 460 | for (var i = 0, l = models.length; i < l; i++) { 461 | this._remove(models[i], options); 462 | } 463 | } else { 464 | this._remove(models, options); 465 | } 466 | return this; 467 | }, 468 | 469 | // Get a model from the set by id. 470 | get : function(id) { 471 | if (id == null) return null; 472 | return this._byId[id.id != null ? id.id : id]; 473 | }, 474 | 475 | // Get a model from the set by client id. 476 | getByCid : function(cid) { 477 | return cid && this._byCid[cid.cid || cid]; 478 | }, 479 | 480 | // Get the model at the given index. 481 | at: function(index) { 482 | return this.models[index]; 483 | }, 484 | 485 | // Force the collection to re-sort itself. You don't need to call this under normal 486 | // circumstances, as the set will maintain sort order as each item is added. 487 | sort : function(options) { 488 | options || (options = {}); 489 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 490 | this.models = this.sortBy(this.comparator); 491 | if (!options.silent) this.trigger('reset', this, options); 492 | return this; 493 | }, 494 | 495 | // Pluck an attribute from each model in the collection. 496 | pluck : function(attr) { 497 | return _.map(this.models, function(model){ return model.get(attr); }); 498 | }, 499 | 500 | // When you have more items than you want to add or remove individually, 501 | // you can reset the entire set with a new list of models, without firing 502 | // any `added` or `removed` events. Fires `reset` when finished. 503 | reset : function(models, options) { 504 | models || (models = []); 505 | options || (options = {}); 506 | this.each(this._removeReference); 507 | this._reset(); 508 | this.add(models, {silent: true}); 509 | if (!options.silent) this.trigger('reset', this, options); 510 | return this; 511 | }, 512 | 513 | // Fetch the default set of models for this collection, resetting the 514 | // collection when they arrive. If `add: true` is passed, appends the 515 | // models to the collection instead of resetting. 516 | fetch : function(options) { 517 | options || (options = {}); 518 | var collection = this; 519 | var success = options.success; 520 | options.success = function(resp, status, xhr) { 521 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); 522 | if (success) success(collection, resp); 523 | }; 524 | options.error = wrapError(options.error, collection, options); 525 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 526 | }, 527 | 528 | // Create a new instance of a model in this collection. After the model 529 | // has been created on the server, it will be added to the collection. 530 | // Returns the model, or 'false' if validation on a new model fails. 531 | create : function(model, options) { 532 | var coll = this; 533 | options || (options = {}); 534 | model = this._prepareModel(model, options); 535 | if (!model) return false; 536 | var success = options.success; 537 | options.success = function(nextModel, resp, xhr) { 538 | coll.add(nextModel, options); 539 | if (success) success(nextModel, resp, xhr); 540 | }; 541 | model.save(null, options); 542 | return model; 543 | }, 544 | 545 | // **parse** converts a response into a list of models to be added to the 546 | // collection. The default implementation is just to pass it through. 547 | parse : function(resp, xhr) { 548 | return resp; 549 | }, 550 | 551 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 552 | // underscore methods are proxied because it relies on the underscore 553 | // constructor. 554 | chain: function () { 555 | return _(this.models).chain(); 556 | }, 557 | 558 | // Reset all internal state. Called when the collection is reset. 559 | _reset : function(options) { 560 | this.length = 0; 561 | this.models = []; 562 | this._byId = {}; 563 | this._byCid = {}; 564 | }, 565 | 566 | // Prepare a model to be added to this collection 567 | _prepareModel: function(model, options) { 568 | if (!(model instanceof Backbone.Model)) { 569 | var attrs = model; 570 | model = new this.model(attrs, {collection: this}); 571 | if (model.validate && !model._performValidation(attrs, options)) model = false; 572 | } else if (!model.collection) { 573 | model.collection = this; 574 | } 575 | return model; 576 | }, 577 | 578 | // Internal implementation of adding a single model to the set, updating 579 | // hash indexes for `id` and `cid` lookups. 580 | // Returns the model, or 'false' if validation on a new model fails. 581 | _add : function(model, options) { 582 | options || (options = {}); 583 | model = this._prepareModel(model, options); 584 | if (!model) return false; 585 | var already = this.getByCid(model); 586 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 587 | this._byId[model.id] = model; 588 | this._byCid[model.cid] = model; 589 | var index = options.at != null ? options.at : 590 | this.comparator ? this.sortedIndex(model, this.comparator) : 591 | this.length; 592 | this.models.splice(index, 0, model); 593 | model.bind('all', this._onModelEvent); 594 | this.length++; 595 | if (!options.silent) model.trigger('add', model, this, options); 596 | return model; 597 | }, 598 | 599 | // Internal implementation of removing a single model from the set, updating 600 | // hash indexes for `id` and `cid` lookups. 601 | _remove : function(model, options) { 602 | options || (options = {}); 603 | model = this.getByCid(model) || this.get(model); 604 | if (!model) return null; 605 | delete this._byId[model.id]; 606 | delete this._byCid[model.cid]; 607 | this.models.splice(this.indexOf(model), 1); 608 | this.length--; 609 | if (!options.silent) model.trigger('remove', model, this, options); 610 | this._removeReference(model); 611 | return model; 612 | }, 613 | 614 | // Internal method to remove a model's ties to a collection. 615 | _removeReference : function(model) { 616 | if (this == model.collection) { 617 | delete model.collection; 618 | } 619 | model.unbind('all', this._onModelEvent); 620 | }, 621 | 622 | // Internal method called every time a model in the set fires an event. 623 | // Sets need to update their indexes when models change ids. All other 624 | // events simply proxy through. "add" and "remove" events that originate 625 | // in other collections are ignored. 626 | _onModelEvent : function(ev, model, collection, options) { 627 | if ((ev == 'add' || ev == 'remove') && collection != this) return; 628 | if (ev == 'destroy') { 629 | this._remove(model, options); 630 | } 631 | if (model && ev === 'change:' + model.idAttribute) { 632 | delete this._byId[model.previous(model.idAttribute)]; 633 | this._byId[model.id] = model; 634 | } 635 | this.trigger.apply(this, arguments); 636 | } 637 | 638 | }); 639 | 640 | // Underscore methods that we want to implement on the Collection. 641 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 642 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 643 | 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 644 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; 645 | 646 | // Mix in each Underscore method as a proxy to `Collection#models`. 647 | _.each(methods, function(method) { 648 | Backbone.Collection.prototype[method] = function() { 649 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 650 | }; 651 | }); 652 | 653 | // Backbone.Router 654 | // ------------------- 655 | 656 | // Routers map faux-URLs to actions, and fire events when routes are 657 | // matched. Creating a new one sets its `routes` hash, if not set statically. 658 | Backbone.Router = function(options) { 659 | options || (options = {}); 660 | if (options.routes) this.routes = options.routes; 661 | this._bindRoutes(); 662 | this.initialize.apply(this, arguments); 663 | }; 664 | 665 | // Cached regular expressions for matching named param parts and splatted 666 | // parts of route strings. 667 | var namedParam = /:([\w\d]+)/g; 668 | var splatParam = /\*([\w\d]+)/g; 669 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 670 | 671 | // Set up all inheritable **Backbone.Router** properties and methods. 672 | _.extend(Backbone.Router.prototype, Backbone.Events, { 673 | 674 | // Initialize is an empty function by default. Override it with your own 675 | // initialization logic. 676 | initialize : function(){}, 677 | 678 | // Manually bind a single named route to a callback. For example: 679 | // 680 | // this.route('search/:query/p:num', 'search', function(query, num) { 681 | // ... 682 | // }); 683 | // 684 | route : function(route, name, callback) { 685 | Backbone.history || (Backbone.history = new Backbone.History); 686 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 687 | Backbone.history.route(route, _.bind(function(fragment) { 688 | var args = this._extractParameters(route, fragment); 689 | callback.apply(this, args); 690 | this.trigger.apply(this, ['route:' + name].concat(args)); 691 | }, this)); 692 | }, 693 | 694 | // Simple proxy to `Backbone.history` to save a fragment into the history. 695 | navigate : function(fragment, triggerRoute) { 696 | Backbone.history.navigate(fragment, triggerRoute); 697 | }, 698 | 699 | // Bind all defined routes to `Backbone.history`. We have to reverse the 700 | // order of the routes here to support behavior where the most general 701 | // routes can be defined at the bottom of the route map. 702 | _bindRoutes : function() { 703 | if (!this.routes) return; 704 | var routes = []; 705 | for (var route in this.routes) { 706 | routes.unshift([route, this.routes[route]]); 707 | } 708 | for (var i = 0, l = routes.length; i < l; i++) { 709 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]); 710 | } 711 | }, 712 | 713 | // Convert a route string into a regular expression, suitable for matching 714 | // against the current location hash. 715 | _routeToRegExp : function(route) { 716 | route = route.replace(escapeRegExp, "\\$&") 717 | .replace(namedParam, "([^\/]*)") 718 | .replace(splatParam, "(.*?)"); 719 | return new RegExp('^' + route + '$'); 720 | }, 721 | 722 | // Given a route, and a URL fragment that it matches, return the array of 723 | // extracted parameters. 724 | _extractParameters : function(route, fragment) { 725 | return route.exec(fragment).slice(1); 726 | } 727 | 728 | }); 729 | 730 | // Backbone.History 731 | // ---------------- 732 | 733 | // Handles cross-browser history management, based on URL fragments. If the 734 | // browser does not support `onhashchange`, falls back to polling. 735 | Backbone.History = function() { 736 | this.handlers = []; 737 | _.bindAll(this, 'checkUrl'); 738 | }; 739 | 740 | // Cached regex for cleaning hashes. 741 | var hashStrip = /^#*/; 742 | 743 | // Cached regex for detecting MSIE. 744 | var isExplorer = /msie [\w.]+/; 745 | 746 | // Has the history handling already been started? 747 | var historyStarted = false; 748 | 749 | // Set up all inheritable **Backbone.History** properties and methods. 750 | _.extend(Backbone.History.prototype, { 751 | 752 | // The default interval to poll for hash changes, if necessary, is 753 | // twenty times a second. 754 | interval: 50, 755 | 756 | // Get the cross-browser normalized URL fragment, either from the URL, 757 | // the hash, or the override. 758 | getFragment : function(fragment, forcePushState) { 759 | if (fragment == null) { 760 | if (this._hasPushState || forcePushState) { 761 | fragment = window.location.pathname; 762 | var search = window.location.search; 763 | if (search) fragment += search; 764 | if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); 765 | } else { 766 | fragment = window.location.hash; 767 | } 768 | } 769 | return decodeURIComponent(fragment.replace(hashStrip, '')); 770 | }, 771 | 772 | // Start the hash change handling, returning `true` if the current URL matches 773 | // an existing route, and `false` otherwise. 774 | start : function(options) { 775 | 776 | // Figure out the initial configuration. Do we need an iframe? 777 | // Is pushState desired ... is it available? 778 | if (historyStarted) throw new Error("Backbone.history has already been started"); 779 | this.options = _.extend({}, {root: '/'}, this.options, options); 780 | this._wantsPushState = !!this.options.pushState; 781 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); 782 | var fragment = this.getFragment(); 783 | var docMode = document.documentMode; 784 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 785 | if (oldIE) { 786 | this.iframe = $('