├── LICENSE ├── README.md ├── bower.json ├── demo ├── dashboard.css ├── index.html └── themes │ ├── orange.css │ └── red.css ├── package.json └── src └── fsm-sticky-header.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Future State Mobile 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sticky-headers 2 | ============== 3 | 4 | An AngularJS directive for making headers that won't scroll past the top of the screen. 5 | 6 | [Demo Page](http://rawgit.com/FutureStateMobile/sticky-header/master/demo/index.html) 7 | 8 | 9 | 10 | 11 | 12 | How to use it 13 | ------------- 14 | 15 | Just include jQuery, Angular, and the sticky-headers JavaScript file in your page. You can also install it 16 | using either `bower` or `npm`: 17 | 18 | ``` 19 | bower install fsm-sticky-header 20 | 21 | # or 22 | 23 | npm install fsm-sticky-header 24 | ``` 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | Then include the `fsm` Angular module in your own module: 35 | 36 | ```js 37 | angular.module('MyHappyModule', ['fsm']); 38 | ``` 39 | 40 | Then add the directive to the element that you with to stick to the top of the page 41 | 42 | ```html 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ... 60 | 61 |
Column One HeaderColumn Two Header
table1 data1table1 data1
table1 data2table1 data2
62 | ``` 63 | 64 | Options 65 | -------- 66 | 67 | * scroll-body 68 | * this is the JQuery selector of the element that your header is bound to. Sticky header will follow the position of that element and keep the header on top of that element as it scrolls off the page. 69 | * scroll-stop 70 | * this is how many pixels from the top of the page your elment will stop scrolling at, just in case you have a header on the top of your page. 71 | * scrollable-container 72 | * If you have a scrollable element such as a div, rather than the web page body scrolling, you'll need to specify that element id here. 73 | 74 | Browser Support 75 | -------- 76 | 77 | We support the current versions of Chrome, Firefox, Safari, Microsoft Edge and Internet Explorer 10+. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fsm-sticky-header", 3 | "version": "1.0.3", 4 | "main": [ 5 | "src/fsm-sticky-header.js" 6 | ], 7 | "dependencies": { 8 | "angular": "~1.3.0", 9 | "jquery": ">=1.7.0" 10 | }, 11 | "ignore": [ 12 | "demo", 13 | "LICENSE", 14 | "README.md" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /demo/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | ************************************************************ 3 | This is the CSS used by the Stick-Header angular directive. 4 | ************************************************************ 5 | */ 6 | .fsm-sticky-header { 7 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.15); 8 | border-radius: 2px !important; 9 | } 10 | 11 | /* 12 | ************************************************************ 13 | This css give the main page behiour and style for the menu 14 | menu buttons, header etc. 15 | ************************************************************ 16 | */ 17 | 18 | /* Dial down the suck just a little bit in Internet Explorer */ 19 | @-ms-viewport{ width: auto !important; } 20 | 21 | body, html { 22 | height: 100%; 23 | font-family: verdana; 24 | padding: 0; 25 | margin: 0; 26 | background-color: #e5e5e5; 27 | } 28 | 29 | .fsm-dashboard { 30 | padding-top: 50px; 31 | position: relative; 32 | width: 100%; 33 | height: 100%; 34 | padding-left: 0; 35 | transition-property: padding-left; 36 | transition-duration: .4s; 37 | } 38 | 39 | @media (min-width: 768px) { 40 | .fsm-dashboard { 41 | padding-left: 250px; 42 | } 43 | 44 | .fsm-menu-toggle .fsm-dashboard { 45 | padding-left: 0; 46 | } 47 | } 48 | 49 | .fsm-spin-forward { 50 | transform: rotate(360deg); 51 | } 52 | 53 | .fsm-spin-backward { 54 | transform: rotate(-360deg); 55 | } 56 | 57 | /* 58 | ************************************************************ 59 | Header CSS 60 | ************************************************************ 61 | */ 62 | .fsm-header { 63 | position: fixed; 64 | left: 0px; 65 | top: 0px; 66 | width: 100%; 67 | height: 50px; 68 | background-color: #FFF; 69 | color: #262626; 70 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 71 | text-align: center; 72 | z-index: 10002; 73 | overflow: none; 74 | transition-property: box-shadow; 75 | transition-duration: .4s; 76 | } 77 | 78 | .fsm-menu-toggle .fsm-header { 79 | box-shadow: 250px 2px 4px 0 rgba(0,0,0,.15); 80 | } 81 | 82 | @media (min-width: 768px) { 83 | .fsm-header { 84 | box-shadow: 250px 2px 4px 0 rgba(0,0,0,.15); 85 | } 86 | 87 | .fsm-menu-toggle .fsm-header { 88 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.15); 89 | } 90 | } 91 | 92 | .fsm-header .fsm-logo { 93 | background: #FFF no-repeat center center fixed; 94 | border: 1px dotted #000; 95 | font-size: 22px; 96 | position: fixed; 97 | height: 35px; 98 | width: 150px; 99 | left: 55px; 100 | top: 7px; 101 | } 102 | 103 | /* 104 | ************************************************************ 105 | Side Menu 106 | ************************************************************ 107 | */ 108 | .fsm-menu { 109 | border-top: 1px solid #d8d8d8; 110 | background-color: #FFF; 111 | color: #262626; 112 | box-shadow: 0px 0px 2px 2px rgba(0,0,0,.15); 113 | width: 250px; 114 | position: fixed; 115 | top: 50px; 116 | left: -260px; 117 | bottom: 0px; 118 | overflow-x: none; 119 | overflow-y: auto; 120 | z-index: 10001; 121 | transition-property: left; 122 | transition-duration: .4s; 123 | } 124 | 125 | .fsm-menu-toggle .fsm-menu { 126 | left: 0; 127 | } 128 | 129 | @media (min-width: 768px) { 130 | .fsm-menu { 131 | left: 0; 132 | } 133 | 134 | .fsm-menu-toggle .fsm-menu { 135 | left: -254px; 136 | } 137 | } 138 | 139 | /* 140 | ************************************************************ 141 | Menu Button 142 | ************************************************************ 143 | */ 144 | .fsm-menu-button { 145 | position: fixed; 146 | padding: 0; 147 | border: 0; 148 | left: 0px; 149 | top: 0px; 150 | width: 50px; 151 | height: 50px; 152 | color: #FFF; 153 | background: #000; 154 | cursor: pointer; 155 | } 156 | 157 | .fsm-menu-button i { 158 | position: relative; 159 | margin: 0; 160 | padding: 0; 161 | left: -1px; 162 | top: 11px; 163 | transition-property: all; 164 | transition-duration: .4s; 165 | } 166 | 167 | .fsm-menu-button-open { 168 | display: block; 169 | } 170 | 171 | .fsm-menu-button-closed { 172 | display: none; 173 | } 174 | 175 | .fsm-menu-toggle .fsm-menu-button-open { 176 | display: none; 177 | } 178 | 179 | .fsm-menu-toggle .fsm-menu-button-closed { 180 | display: block; 181 | } 182 | 183 | @media (min-width: 768px) { 184 | .fsm-menu-button-open { 185 | display: none; 186 | } 187 | 188 | .fsm-menu-button-closed { 189 | display: block; 190 | } 191 | 192 | .fsm-menu-toggle .fsm-menu-button-open { 193 | display: block; 194 | } 195 | 196 | .fsm-menu-toggle .fsm-menu-button-closed { 197 | display: none; 198 | } 199 | } 200 | 201 | /* 202 | ************************************************************ 203 | Content Section 204 | ************************************************************ 205 | */ 206 | /* No padding on phones, cause we want as much space as possible */ 207 | .fsm-page-content { 208 | padding: 0px; 209 | transition-property: padding; 210 | transition-duration: .4s; 211 | } 212 | 213 | /* some padding on tablets cause we can */ 214 | @media (min-width: 768px) { 215 | .fsm-page-content { 216 | padding: 5px; 217 | } 218 | } 219 | 220 | /* Lots of padding on desktops cause it just makes it easier to look at. */ 221 | @media (min-width: 992px) { 222 | .fsm-page-content { 223 | padding: 20px; 224 | } 225 | } 226 | 227 | /* 228 | ************************************************************ 229 | This stuff is not needed for the dashboard, but is just used 230 | for elements on the demo page like the table and stuff 231 | ************************************************************ 232 | */ 233 | .section { 234 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.15); 235 | background-color: #FFF; 236 | width: %100; 237 | } 238 | 239 | table { 240 | border: 1px solid #d8d8d8; 241 | width: 100%; 242 | border-collapse: collapse; 243 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 244 | background-color: #FFF; 245 | border-radius: 2px; 246 | } 247 | 248 | th { 249 | background-color: #eee; 250 | padding: 10px; 251 | text-align: left; 252 | } 253 | 254 | th > tr { 255 | /*width: 100%;*/ 256 | border-top: 1px solid #d8d8d8; 257 | } 258 | 259 | tr { 260 | border-top: 1px solid #d8d8d8; 261 | } 262 | 263 | tr:nth-child(odd) { 264 | background: #eee; 265 | } 266 | 267 | td { 268 | padding: 10px; 269 | } 270 | 271 | .text-area { 272 | border: 1px solid #d8d8d8; 273 | margin-bottom: 20px; 274 | line-height: 1.5; 275 | border-radius: 2px; 276 | } 277 | 278 | .text-area > h2 { 279 | background: #F1F1F1; 280 | padding: 10px; 281 | font: 28px Verdana, Serif; 282 | margin: 0 0 20px 0; 283 | } 284 | 285 | .text-area .text-content { 286 | padding: 2px; 287 | } 288 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 | Menu 44 | 48 |
49 |
50 | 51 |
52 | 53 |

Just a block of text

54 |
55 |

56 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus at commodo neque, in pellentesque massa. Quisque nibh nisi, eleifend at finibus quis, euismod in metus. Vestibulum sit amet ultricies enim. Nullam odio elit, sollicitudin non varius sit amet, pretium eu metus. Mauris lacinia feugiat odio, et bibendum ligula consectetur et. Proin aliquet eros vel pulvinar mattis. Cras a rutrum lorem, ut luctus dolor. Sed vel eros ut orci sagittis euismod. Curabitur gravida elit sit amet mi rhoncus malesuada. 57 |

58 |

59 | Nunc non massa vel odio tempor hendrerit ut id erat. Nunc auctor mattis sem in gravida. Vestibulum finibus vehicula purus nec gravida. Sed ac vehicula massa, id tristique tortor. Proin euismod, libero non consectetur consectetur, urna nulla dignissim urna, vel laoreet lectus lorem a dui. Proin ac est mauris. Cras et nisl id velit vulputate posuere. Cras convallis bibendum ligula non accumsan. 60 |

61 |

62 | Nunc auctor, velit a gravida iaculis, turpis augue egestas purus, sit amet laoreet eros arcu quis nisl. Suspendisse pellentesque in dolor nec rutrum. Nullam dictum imperdiet luctus. Sed et efficitur urna. Pellentesque volutpat sagittis nisi, sodales interdum nulla mattis a. Etiam vitae elit ac nibh tempor tristique. Nullam quis diam mi. Etiam pretium vel erat sed porta. 63 |

64 |

65 | Donec finibus ut nunc ut ultricies. Duis a felis dui. Nullam ante odio, commodo at posuere eget, tristique at orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas facilisis magna et libero sodales convallis. Fusce et tincidunt nisi. Nulla vitae egestas risus. Nunc rutrum nisl et posuere eleifend. Phasellus ac nisl ut orci suscipit tincidunt. 66 |

67 |

68 | Vivamus vitae molestie metus. Aenean ac nisi sed sem efficitur aliquam. Nunc varius justo sed ante blandit, ac luctus eros ullamcorper. Mauris lacus purus, cursus at nisi eu, ultricies luctus augue. Proin tristique tempor velit sit amet venenatis. Suspendisse potenti. Sed congue turpis sit amet risus facilisis, non laoreet nunc condimentum. Vivamus ornare orci bibendum nisl iaculis accumsan. Mauris volutpat fermentum enim, ac tristique metus vehicula a. 69 |

70 |
71 | 72 |
73 | 74 |
75 | 76 |

Another block of text

77 |
78 |

79 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus at commodo neque, in pellentesque massa. Quisque nibh nisi, eleifend at finibus quis, euismod in metus. Vestibulum sit amet ultricies enim. Nullam odio elit, sollicitudin non varius sit amet, pretium eu metus. Mauris lacinia feugiat odio, et bibendum ligula consectetur et. Proin aliquet eros vel pulvinar mattis. Cras a rutrum lorem, ut luctus dolor. Sed vel eros ut orci sagittis euismod. Curabitur gravida elit sit amet mi rhoncus malesuada. 80 |

81 |

82 | Nunc non massa vel odio tempor hendrerit ut id erat. Nunc auctor mattis sem in gravida. Vestibulum finibus vehicula purus nec gravida. Sed ac vehicula massa, id tristique tortor. Proin euismod, libero non consectetur consectetur, urna nulla dignissim urna, vel laoreet lectus lorem a dui. Proin ac est mauris. Cras et nisl id velit vulputate posuere. Cras convallis bibendum ligula non accumsan. 83 |

84 |

85 | Nunc auctor, velit a gravida iaculis, turpis augue egestas purus, sit amet laoreet eros arcu quis nisl. Suspendisse pellentesque in dolor nec rutrum. Nullam dictum imperdiet luctus. Sed et efficitur urna. Pellentesque volutpat sagittis nisi, sodales interdum nulla mattis a. Etiam vitae elit ac nibh tempor tristique. Nullam quis diam mi. Etiam pretium vel erat sed porta. 86 |

87 |

88 | Donec finibus ut nunc ut ultricies. Duis a felis dui. Nullam ante odio, commodo at posuere eget, tristique at orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas facilisis magna et libero sodales convallis. Fusce et tincidunt nisi. Nulla vitae egestas risus. Nunc rutrum nisl et posuere eleifend. Phasellus ac nisl ut orci suscipit tincidunt. 89 |

90 |

91 | Vivamus vitae molestie metus. Aenean ac nisi sed sem efficitur aliquam. Nunc varius justo sed ante blandit, ac luctus eros ullamcorper. Mauris lacus purus, cursus at nisi eu, ultricies luctus augue. Proin tristique tempor velit sit amet venenatis. Suspendisse potenti. Sed congue turpis sit amet risus facilisis, non laoreet nunc condimentum. Vivamus ornare orci bibendum nisl iaculis accumsan. Mauris volutpat fermentum enim, ac tristique metus vehicula a. 92 |

93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
Total records: {{::test.data.length | number: 0}}Displayed records: {{dataPage.length | number: 0}}
NumberColumn OneColumn TwoColumn Three
{{::row.id}}{{::row.column1}}{{::row.column2}}{{::row.longText}}.
118 | 119 |
120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /demo/themes/orange.css: -------------------------------------------------------------------------------- 1 | /* 2 | ************************************************************ 3 | This css give the main page behiour and style for the menu 4 | menu buttons, header etc. 5 | ************************************************************ 6 | */ 7 | body, html { 8 | background-color: #F9F9F9; 9 | } 10 | 11 | /* 12 | ************************************************************ 13 | Header CSS 14 | ************************************************************ 15 | */ 16 | .fsm-header { 17 | background-color: #C63D0F; 18 | color: #FDF3E7; 19 | z-index: 10001; 20 | } 21 | 22 | .fsm-menu-toggle .fsm-header { 23 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 24 | } 25 | 26 | @media (min-width: 768px) { 27 | .fsm-header { 28 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 29 | } 30 | 31 | .fsm-menu-toggle .fsm-header { 32 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.15); 33 | } 34 | } 35 | 36 | .fsm-header .fsm-logo { 37 | background: #C63D0F no-repeat center center fixed; 38 | border: 1px dotted #FDF3E7; 39 | position: fixed; 40 | right: 7px; 41 | left: auto; 42 | } 43 | 44 | /* 45 | ************************************************************ 46 | Side Menu 47 | ************************************************************ 48 | */ 49 | .fsm-menu { 50 | border-top: 0; 51 | top: 0px; 52 | background-color: #3B3738; 53 | color: #FDF3E7; 54 | z-index: 10002; 55 | } 56 | /* 57 | ************************************************************ 58 | Menu Button 59 | ************************************************************ 60 | */ 61 | .fsm-menu-button { 62 | position: fixed; 63 | left: -10px; 64 | top: 3px; 65 | width: 48px; 66 | height: 44px; 67 | border-radius: 10px; 68 | color: #f1f1f1; 69 | background: #3B3738; 70 | z-index: 10002; 71 | transition-property: left; 72 | transition-duration: .4s; 73 | } 74 | 75 | .fsm-menu-button i { 76 | position: relative; 77 | top: 8px; 78 | } 79 | 80 | .fsm-menu-toggle .fsm-menu-button { 81 | left: 240px; 82 | } 83 | 84 | @media (min-width: 768px) { 85 | .fsm-menu-button { 86 | left: 240px; 87 | } 88 | 89 | .fsm-menu-toggle .fsm-menu-button { 90 | left: -10px; 91 | } 92 | } -------------------------------------------------------------------------------- /demo/themes/red.css: -------------------------------------------------------------------------------- 1 | /* 2 | ************************************************************ 3 | This css give the main page behiour and style for the menu 4 | menu buttons, header etc. 5 | ************************************************************ 6 | */ 7 | body, html { 8 | background-color: #D6D9D5; 9 | } 10 | 11 | /* 12 | ************************************************************ 13 | Header CSS 14 | ************************************************************ 15 | */ 16 | .fsm-header { 17 | background-color: #0A2433; 18 | color: #f1f1f1; 19 | z-index: 10001; 20 | } 21 | 22 | .fsm-menu-toggle .fsm-header { 23 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 24 | } 25 | 26 | @media (min-width: 768px) { 27 | .fsm-header { 28 | box-shadow: 0px 2px 4px 0 rgba(0,0,0,.15); 29 | } 30 | 31 | .fsm-menu-toggle .fsm-header { 32 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.15); 33 | } 34 | } 35 | 36 | .fsm-header .fsm-logo { 37 | background: #0A2433 no-repeat center center fixed; 38 | border: 1px dotted #f1f1f1; 39 | color: #f1f1f1; 40 | position: fixed; 41 | right: 7px; 42 | left: auto; 43 | } 44 | 45 | /* 46 | ************************************************************ 47 | Side Menu 48 | ************************************************************ 49 | */ 50 | .fsm-menu { 51 | border-top: 0px solid #0a2433; 52 | top: 0px; 53 | color: #f1f1f1; 54 | background-color: #ab3333; 55 | z-index: 10002; 56 | } 57 | /* 58 | ************************************************************ 59 | Menu Button 60 | ************************************************************ 61 | */ 62 | .fsm-menu-button { 63 | position: fixed; 64 | left: -10px; 65 | top: 3px; 66 | width: 48px; 67 | height: 44px; 68 | border-radius: 10px; 69 | color: #f1f1f1; 70 | background: #ab3333; 71 | transition-property: left; 72 | transition-duration: .4s; 73 | } 74 | 75 | .fsm-menu-button i { 76 | position: relative; 77 | top: 8px; 78 | } 79 | 80 | .fsm-menu-toggle .fsm-menu-button { 81 | left: 240px; 82 | } 83 | 84 | @media (min-width: 768px) { 85 | .fsm-menu-button { 86 | left: 240px; 87 | } 88 | 89 | .fsm-menu-toggle .fsm-menu-button { 90 | left: -10px; 91 | } 92 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sticky-header", 3 | "version": "0.1.0", 4 | "description": "An angularjs directive for making headers that don't scroll past the top of the screen", 5 | "main": "src/fsm-sticky-header.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/FutureStateMobile/sticky-header" 9 | }, 10 | "dependencies": { 11 | "angular": "1.4.*", 12 | "jquery": "2.1.*" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/FutureStateMobile/sticky-header/issues" 16 | }, 17 | "homepage": "https://github.com/FutureStateMobile/sticky-header", 18 | "author": "Future State Mobile", 19 | "contributors": [ 20 | { "name": "Jonathan Petitcolas", "email": "petitcolas.jonathan@gmail.com" }, 21 | { "name": "Keith Larsen" }, 22 | { "name": "Ivan Rey" }, 23 | { "name": "Michael Schramm" }, 24 | { "name": "Valentin Hervieu" }, 25 | { "name": "Anthanh" }, 26 | { "name": "Chase Florell" }, 27 | { "name": "cmelo" }, 28 | { "name": "gijskooij" }, 29 | { "name": "raz-varren" } 30 | ], 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /src/fsm-sticky-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(angular){ 4 | var fsm = angular.module('fsm', []); 5 | 6 | fsm.directive('fsmStickyHeader', [function(){ 7 | return { 8 | restrict: 'EA', 9 | replace: false, 10 | scope: { 11 | scrollBody: '@', 12 | scrollStop: '=', 13 | scrollableContainer: '=', 14 | contentOffset: '=', 15 | fsmZIndex: '=' 16 | }, 17 | link: function(scope, element, attributes, control){ 18 | var content, 19 | header = $(element, this), 20 | clonedHeader = null, 21 | scrollableContainer = $(scope.scrollableContainer), 22 | contentOffset = scope.contentOffset || 0; 23 | 24 | var unbindScrollBodyWatcher = scope.$watch('scrollBody', function(newValue, oldValue) { 25 | content = $(scope.scrollBody); 26 | init(); 27 | unbindScrollBodyWatcher(); 28 | }); 29 | 30 | if (scrollableContainer.length === 0){ 31 | scrollableContainer = $(window); 32 | } 33 | 34 | function setColumnHeaderSizes() { 35 | if (clonedHeader.is('tr') || clonedHeader.is('thead')) { 36 | var clonedColumns = clonedHeader.find('th'); 37 | header.find('th').each(function (index, column) { 38 | var clonedColumn = $(clonedColumns[index]); 39 | //clonedColumn.css( 'width', column.offsetWidth + 'px'); fixed thead width 40 | // fluid thead / table 41 | var finalWidthSet = column.offsetWidth / ($(window).innerWidth()-20)*100; // $(window) can be replace with a custom wrapper / container 42 | clonedColumn.css('width',finalWidthSet + '%'); 43 | }); 44 | } 45 | }; 46 | 47 | function determineVisibility(){ 48 | var scrollTop = scrollableContainer.scrollTop() + scope.scrollStop; 49 | var scrollLeft = -scrollableContainer.scrollLeft() + content.offset().left; 50 | var contentTop = content.offset().top + contentOffset; 51 | var contentBottom = contentTop + content.outerHeight(false); 52 | 53 | if ( (scrollTop > contentTop) && (scrollTop < contentBottom) ) { 54 | if (!clonedHeader){ 55 | createClone(); 56 | clonedHeader.css({ "visibility": "visible"}); 57 | } 58 | 59 | if ( scrollTop < contentBottom && scrollTop > contentBottom - clonedHeader.outerHeight(false) ){ 60 | var top = contentBottom - scrollTop + scope.scrollStop - clonedHeader.outerHeight(false); 61 | clonedHeader.css('top', top + 'px'); 62 | } else { 63 | calculateSize(); 64 | } 65 | clonedHeader.css('left', scrollLeft + 'px'); 66 | } else { 67 | if (clonedHeader){ 68 | /* 69 | * remove cloned element (switched places with original on creation) 70 | */ 71 | header.remove(); 72 | header = clonedHeader; 73 | clonedHeader = null; 74 | 75 | header.removeClass('fsm-sticky-header'); 76 | header.css({ 77 | position: 'relative', 78 | left: 0, 79 | top: 0, 80 | width: 'auto', 81 | 'z-index': 0, 82 | visibility: 'visible' 83 | }); 84 | } 85 | } 86 | }; 87 | 88 | function calculateSize() { 89 | clonedHeader.css({ 90 | top: scope.scrollStop, 91 | width: header.outerWidth(), 92 | left: header.offset().left 93 | }); 94 | 95 | setColumnHeaderSizes(); 96 | }; 97 | 98 | function createClone(){ 99 | /* 100 | * switch place with cloned element, to keep binding intact 101 | */ 102 | clonedHeader = header; 103 | header = clonedHeader.clone(); 104 | clonedHeader.after(header); 105 | clonedHeader.addClass('fsm-sticky-header'); 106 | clonedHeader.css({ 107 | position: 'fixed', 108 | 'z-index': scope.fsmZIndex || 10000, 109 | visibility: 'hidden' 110 | }); 111 | calculateSize(); 112 | }; 113 | 114 | function init() { 115 | scrollableContainer.on('scroll.fsmStickyHeader', determineVisibility).trigger("scroll"); 116 | scrollableContainer.on('resize.fsmStickyHeader', determineVisibility); 117 | 118 | scope.$on('$destroy', function () { 119 | scrollableContainer.off('.fsmStickyHeader'); 120 | }); 121 | } 122 | } 123 | }; 124 | }]); 125 | 126 | fsm.directive('fsmMenu', ['$location', function ($location) { 127 | return { 128 | restrict: 'A', 129 | scope: {}, 130 | link: function (scope, el, attrs) { 131 | var navigationMenu = angular.element(el); 132 | var menuTitles = navigationMenu.find('.fsm-menu-title'); 133 | var menuItems = navigationMenu.find('[tc-menu-item]:visible'); 134 | var activeMenu = 0; 135 | 136 | function menuOnClick(e) { 137 | var menuTitle = angular.element(e.currentTarget); 138 | // Find if our menu item has submenu items 139 | var submenu = menuTitle.next('.fsm-sub-menu'); 140 | 141 | // If there is submenus, then we wan to either open or close the folder 142 | submenu.slideToggle('fast', function () { 143 | // swap the arrow from up to down, or vise-versa 144 | var chevron = menuTitle.children('[class*=\'fa-chevron\']'); 145 | chevron.toggleClass('fa-chevron-down fa-chevron-up'); 146 | 147 | // Find the list of currently visible menu items for keyboarding through. 148 | menuItems = navigationMenu.find('[tc-menu-item]:visible'); 149 | }); 150 | 151 | } 152 | 153 | function menuOnKeydown(e) { 154 | activeMenu = getCurrentMenuItem(); 155 | 156 | if (e.keyCode === 9) { 157 | tabOutOfMenu(e); 158 | } else if (e.keyCode === 13 || e.keyCode === 37 || e.keyCode === 39) { 159 | triggerMenuItem(e); 160 | } else if (e.keyCode === 38 || e.keyCode === 40) { 161 | highlightMenuItem(e); 162 | } 163 | } 164 | 165 | function tabOutOfMenu(e) { 166 | if (e.shiftKey) { 167 | $('[fsm-menu-button]').focus(); 168 | } else { 169 | var firstTabItem = $('[tabindex=1]'); 170 | 171 | if (firstTabItem.length == 0) { 172 | return; 173 | } else { 174 | firstTabItem.focus(); 175 | } 176 | } 177 | 178 | e.preventDefault(); 179 | e.stopPropagation(); 180 | } 181 | 182 | function triggerMenuItem(e) { 183 | e.currentTarget = $(menuItems[activeMenu]).children('a').first(); 184 | menuOnClick(e); 185 | } 186 | 187 | function highlightMenuItem(e) { 188 | e.preventDefault(); 189 | e.stopPropagation(); 190 | 191 | if (e.keyCode === 38) { 192 | if (activeMenu > 0) { 193 | $(menuItems[activeMenu - 1]).children('a').first().focus(); 194 | } else { 195 | $(menuItems[menuItems.length - 1]).children('a').first().focus(); 196 | } 197 | } else if (e.keyCode === 40) { 198 | if (activeMenu < (menuItems.length - 1)) { 199 | $(menuItems[activeMenu + 1]).children('a').first().focus(); 200 | } else { 201 | $(menuItems[0]).children('a').first().focus(); 202 | } 203 | } 204 | } 205 | 206 | function getCurrentMenuItem() { 207 | var focusedMenu = navigationMenu.find(':focus'); 208 | 209 | if (focusedMenu.length === 1) { 210 | return menuItems.index(focusedMenu.parent('[tc-menu-item]').first()); 211 | } 212 | 213 | return menuItems.index(navigationMenu.find('[tc-menu-item].active').first()); 214 | } 215 | 216 | function menuOnFocus(e) { 217 | e.preventDefault(); 218 | e.stopPropagation(); 219 | 220 | activeMenu = getCurrentMenuItem(); 221 | $(menuItems[activeMenu]).children('a').first().focus(); 222 | } 223 | 224 | menuTitles.click(menuOnClick); 225 | navigationMenu.keydown(menuOnKeydown); 226 | navigationMenu.focus(menuOnFocus); 227 | 228 | scope.$watch(function () { 229 | return $location.path(); 230 | }, function (path) { 231 | // grab all the a tags that start with #, indicating and angular route. 232 | var allMenuItems = el.find('a[href^=\'#\']'); 233 | 234 | angular.forEach(allMenuItems, function (menuItem) { 235 | var menu = angular.element(menuItem); 236 | var link = menu.attr('href').split('#')[1]; 237 | 238 | if (path.indexOf(link) == 0) { 239 | // activate the menu item 240 | angular.element(menuItem.parentElement).addClass('active'); 241 | 242 | // open up the submenus if they are not open 243 | var submenus = menu.parents('.fsm-sub-menu'); 244 | submenus.show(); 245 | 246 | // turn the chevrons up on the menu titles 247 | var chevrons = submenus.prev().children('[class*=\'fa-chevron\']'); 248 | chevrons.removeClass('fa-chevron-down'); 249 | chevrons.addClass('fa-chevron-up'); 250 | } else { 251 | angular.element(menuItem.parentElement).removeClass('active'); 252 | } 253 | }); 254 | 255 | menuItems = navigationMenu.find('[tc-menu-item]:visible'); 256 | }); 257 | } 258 | } 259 | }]); 260 | 261 | fsm.directive('fsmMenuButton', function () { 262 | return { 263 | restrict: 'EA', 264 | replace: false, 265 | scope: {}, 266 | link: function (scope, element, attributes, control) { 267 | var menuButton = $(element, this); 268 | 269 | menuButton.addClass('fsm-menu-button'); 270 | menuButton.keydown(menuOnKeydown); 271 | $('body').keydown(bodyOnKeydown); 272 | 273 | function bodyOnKeydown(e) { 274 | if (e.keyCode === 77 && e.ctrlKey && e.altKey) { 275 | if (isMenuClosed()) { 276 | menuOnClick(); 277 | } 278 | $('[fsm-menu]').focus(); 279 | } else if (e.keyCode === 77 && e.ctrlKey) { 280 | menuButton.focus(); 281 | menuOnClick(); 282 | } 283 | } 284 | 285 | function isMenuClosed() { 286 | return $('body').hasClass('fsm-menu-toggle'); 287 | } 288 | 289 | function menuOnClick() { 290 | $('body').toggleClass('fsm-menu-toggle'); 291 | setMenuSpin(); 292 | setTimeout(setMenuSpin, 50); 293 | }; 294 | 295 | function menuOnKeydown(e) { 296 | if (e.keyCode === 32 || e.keyCode === 13) { 297 | e.preventDefault(); 298 | e.stopPropagation(); 299 | menuOnClick(); 300 | } 301 | } 302 | 303 | function setMenuSpin() { 304 | menuButton.find('.fsm-menu-button-open').toggleClass('fsm-spin-forward'); 305 | menuButton.find('.fsm-menu-button-close').toggleClass('fsm-spin-backward'); 306 | }; 307 | 308 | menuButton.on('click.fsmMenuButton', menuOnClick); 309 | 310 | scope.$on('$destroy', function() { 311 | menuButton.off('.fsmMenuButton'); 312 | }); 313 | } 314 | } 315 | }); 316 | 317 | fsm.directive('fsmBigData', ['$filter', function ($filter) { 318 | 319 | return { 320 | restrict: 'AE', 321 | scope: {}, 322 | replace: false, 323 | transclude: true, 324 | link: function (scope, element, attrs, controller, transclude) { 325 | var orderBy = $filter('orderBy'); 326 | var currentPage = 0; 327 | var pagedDataName = attrs.fsmBigData.split(' of ')[0]; 328 | var rightHandExpression = attrs.fsmBigData.split(' of ')[1]; 329 | var pageSize = parseInt(rightHandExpression.split(' take ')[1], 10); 330 | var sourceData = rightHandExpression.split(' take ')[0]; 331 | 332 | // Interesting things can be done here with the source object... 333 | // var displayGetter = $parse(sourceData); 334 | // var displaySetter = displayGetter.assign; 335 | // var results = orderBy(displayGetter(scope.$parent), sortColumns); 336 | // displaySetter(scope.$parent, results) 337 | 338 | var rawData = []; 339 | var sortedData = []; 340 | var pagedData = []; 341 | var page = $(window); 342 | var sortTypes = [ 'None', 'Ascending', 'Descending' ]; 343 | var sortColumns = []; 344 | 345 | transclude(function (clone, transcludedScope) { 346 | 347 | transcludedScope.sortTypes = sortTypes; 348 | 349 | element.append(clone); 350 | transcludedScope[pagedDataName] = pagedData; 351 | 352 | function nextPage() { 353 | var dataSlice = sortedData.slice(pageSize * currentPage, (pageSize * (currentPage + 1))); 354 | if (dataSlice.length > 0) { 355 | pagedData.push.apply(pagedData, dataSlice); 356 | currentPage++; 357 | } 358 | } 359 | 360 | function renderData() { 361 | if (sortColumns.length){ 362 | sortedData = orderBy(rawData, sortColumns); 363 | } 364 | else { 365 | sortedData = rawData; 366 | } 367 | 368 | pagedData.length = 0; 369 | currentPage = 0; 370 | nextPage(); 371 | } 372 | 373 | function addSortColumn(columnName, sortType) { 374 | 375 | // If this column is currently in the sort stack, remove it. 376 | for (var i = 0; i < sortColumns.length; i ++){ 377 | if (sortColumns[i].indexOf(columnName) > -1) { 378 | sortColumns.splice(i, 1); 379 | } 380 | } 381 | 382 | // Push this sort on the top of the stack (aka. array) 383 | if (sortType > 0) { 384 | var direction = ''; 385 | if (sortTypes[sortType] === 'Descending'){ 386 | direction = '-'; 387 | } 388 | sortColumns.unshift(direction + columnName); 389 | } 390 | 391 | renderData(); 392 | } 393 | 394 | function onPageScroll() { 395 | var s = $(window).scrollTop(); 396 | var d = $(document).height(); 397 | var c = $(window).height(); 398 | var scrollPercent = (s / (d-c)); 399 | 400 | if (scrollPercent > 0.98) { 401 | // We use scope.apply here to tell angular about these changes because 402 | // they happen outside of angularjs context... we're using jquery here 403 | // to figure out when we need to load another page of data. 404 | transcludedScope.$apply(nextPage); 405 | } 406 | } 407 | 408 | transcludedScope.$parent.$watchCollection(sourceData, function (newData) { 409 | if (newData){ 410 | rawData = newData; 411 | renderData(); 412 | } 413 | }); 414 | 415 | transcludedScope.addSortColumn = addSortColumn; 416 | 417 | page.on('scroll.fsmBigData', onPageScroll).trigger('scroll'); 418 | 419 | transcludedScope.$on('$destroy', function() { 420 | page.off('.fsmBigData'); 421 | }); 422 | }); 423 | } 424 | }; 425 | }]); 426 | 427 | fsm.directive('fsmSort', [function () { 428 | var sortIconTemplate = ''; 429 | 430 | return { 431 | restrict: 'AE', 432 | replace: false, 433 | scope: {}, 434 | link: function (scope, element, attrs) { 435 | var columnHeader = element; 436 | var columnName = attrs.fsmSort; 437 | var sortIcon = angular.element(sortIconTemplate); 438 | columnHeader.append(' '); 439 | columnHeader.append(sortIcon); 440 | var currentSortType = 0; 441 | 442 | function swapIcons(){ 443 | sortIcon.removeClass('fa-sort-desc fa-sort-asc fa-sort'); 444 | 445 | var classToAdd = 'fa-sort'; 446 | 447 | if (scope.$parent.sortTypes[currentSortType] === 'Ascending'){ 448 | classToAdd = 'fa-sort-asc'; 449 | } else if(scope.$parent.sortTypes[currentSortType] === 'Descending') { 450 | classToAdd = 'fa-sort-desc'; 451 | } 452 | 453 | sortIcon.addClass(classToAdd); 454 | }; 455 | 456 | function applySort() { 457 | // Find the kind of sort this should now be 458 | currentSortType ++; 459 | if (currentSortType == scope.$parent.sortTypes.length ){ 460 | currentSortType = 0; 461 | } 462 | 463 | scope.$apply( scope.$parent.addSortColumn(columnName, currentSortType) ); 464 | 465 | swapIcons(); 466 | }; 467 | 468 | columnHeader.css({ cursor: 'pointer' }); 469 | 470 | columnHeader.on('click.fsmSort', applySort); 471 | 472 | columnHeader.on('keydown.fsmSort', function(e) { 473 | if (e.keyCode === 13) { 474 | applySort(); 475 | e.preventDefault(); 476 | e.stopPropagation(); 477 | } 478 | }); 479 | 480 | scope.$on('$destroy', function() { 481 | columnHeader.off('.fsmSort'); 482 | }); 483 | } 484 | }; 485 | }]); 486 | })(window.angular); 487 | --------------------------------------------------------------------------------