├── .gitignore ├── .htaccess ├── LICENSE ├── README.md ├── bower.json ├── gulpfile.js ├── icon.png ├── index.css ├── index.html ├── package.json ├── php ├── Parsedown.php └── index.php ├── scroll-scope.js ├── scroll-scope.min.js └── splash.png /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .DS_Store 3 | .idea 4 | .project 5 | *.log 6 | 7 | node_modules/ 8 | vendor/ 9 | bower_components/ 10 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # Set the default handler. 2 | DirectoryIndex index.php index.html index.htm 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jerry Jäppinen 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 | 2 | # *scroll-scope.js* 3 | 4 | Small jQuery plugin to **keep parent element still when scrolling an element to its boundary**. 5 | 6 | Commonly in scroll interaction, user hovers a mouse cursor over a scrollable element and uses trackpad or mouse wheel to scroll the element. When an element reaches its boundary, its parent element continues scrolling. Usually this means that the user will continue moving down the page when attempting to interact with an specific container. This is a common issue with dropdown menus and modal dialogs. 7 | 8 | This behavior is most prevalent in desktop browsers. To fix this problem that shouldn't even exist, scroll-scope.js exists. 9 | 10 | See project home and demos on [eiskis.net/scroll-scope](http://eiskis.net/scroll-scope). 11 | 12 | ## Get the plugin 13 | 14 | Current version is *0.1.0*. 15 | 16 | - [scroll-scope.js](https://raw.githubusercontent.com/Eiskis/scroll-scope/master/scroll-scope.js) 17 | - [scroll-scope.min.js](https://raw.githubusercontent.com/Eiskis/scroll-scope/master/scroll-scope.min.js) 18 | 19 | Install with Bower: 20 | 21 | ```sh 22 | bower install scroll-scope 23 | ``` 24 | 25 | File [issues or pull requests](https://github.com/Eiskis/scroll-scope/issues) to share potential improvements. The plugin is released under MIT. Source is available on [GitHub](https://github.com/Eiskis/scroll-scope). To contribute, clone the repo, make changes in *scroll-scope.js* and build the minified version with `gulp`. 26 | 27 | 28 | 29 | ## Usage 30 | 31 | Add the `data-scroll-scope` attribute to any scrollable element on the page: 32 | 33 | ```html 34 | 35 |
36 | 37 | 38 |
39 | ``` 40 | 41 | Include and initialize: 42 | 43 | ```html 44 | 45 | 46 | 47 | 48 | 49 | 52 | ``` 53 | 54 | The plugin works declaratively, meaning that it's attached to the document object (or any parent container you choose) instead of individual scrollable containers. This means that any DOM elements that are added or removed after page load do not need to be bound separately. You can even toggle force on and off during runtime if you please. 55 | 56 | 57 | 58 | ### Options 59 | 60 | You can change which elements and events are targeted by setting them upon initialization. Note that having events listed here does not mean they're blocked automatically, rather the plugin listens to these events and evaluates them when encountered. 61 | 62 | Here are the defaults: 63 | 64 | ```js 65 | $(document).scrollScope({ 66 | elements: '[data-scroll-scope]', 67 | forcedElements: '[data-scroll-scope="force"]', 68 | events: 'DOMMouseScroll mousewheel scroll touchstart touchmove' 69 | }); 70 | ``` 71 | 72 | 73 | ### Advanced use 74 | 75 | If you need full access to `ScrollScope` object (for example to `getTargetedElements()` or `unbind()` it later), instantiate a raw `ScrollScope` object yourself. You need to bind it to the document yourself in this case: 76 | 77 | ```js 78 | // Create new instance 79 | var myScrollScopeInstance = new ScrollScope({ 80 | options: 'here' 81 | }); 82 | 83 | // Bind to document 84 | myScrollScopeInstance.bind(document); 85 | 86 | // Use the object for whatever you wish 87 | myScrollScopeInstance.mainContainer.css('background', 'blue'); 88 | myScrollScopeInstance.getTargetedElements().css('background', 'red'); 89 | 90 | // Detach from document if no longer needed 91 | myScrollScope.unbind(); 92 | ``` 93 | 94 | 95 | 96 | ## Credits 97 | 98 | Plugin by [Jerry Jäppinen](http://eiskis.net/) (under [MIT](https://github.com/Eiskis/scroll-scope/blob/master/LICENSE)). 99 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scroll-scope", 3 | "main": "scroll-scope.js", 4 | "version": "0.1.0", 5 | "homepage": "http://eiskis.net/scroll-scope", 6 | "authors": [ 7 | "Jerry Jäppinen " 8 | ], 9 | "description": "Small jQuery plugin to keep parent element still when scrolling an element to its boundary.", 10 | "keywords": [ 11 | "scroll-scope", 12 | "scroll", 13 | "scope", 14 | "jquery", 15 | "block" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | // Gulp + autoloaded plugins 3 | var args = require('yargs').argv; 4 | var del = require('del'); 5 | var gulp = require('gulp'); 6 | 7 | // Mostly autoloaded plugins 8 | var plugins = require('gulp-load-plugins')(); 9 | 10 | 11 | // 12 | // Config 13 | // 14 | var config = { 15 | source: ['scroll-scope.js'] 16 | }; 17 | 18 | 19 | 20 | // 21 | // TASKS 22 | // 23 | 24 | gulp.task('build', function () { 25 | var files = gulp.src(config.source); 26 | return files 27 | .pipe(plugins.plumber()) 28 | .pipe(plugins.uglify({ 29 | preserveComments: 'some' 30 | })) 31 | .pipe(plugins.rename({suffix: '.min'})) 32 | .pipe(gulp.dest('./')); 33 | }); 34 | 35 | 36 | 37 | // 38 | // Default 39 | // 40 | gulp.task('default', [], function () { 41 | gulp.start('build'); 42 | }); 43 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/scroll-scope/8fbe1e38dcca035e361717902adeac523a60f9d8/icon.png -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | 2 | /*Base styles*/ 3 | * { 4 | -webkit-overflow-scrolling: touch; 5 | } 6 | 7 | html { 8 | color: #293a3d; 9 | font-family: "Lato", "Helvetica Neue", "Helvetica", "Roboto", "Segoe UI", "Open Sans", "Arial", sans-serif; 10 | line-height: 1.5; 11 | -webkit-font-smoothing: antialiased; 12 | cursor: default; 13 | } 14 | 15 | ::selection, 16 | .container .container ::selection, 17 | .container .container .container .container ::selection { 18 | background-color: rgba(128, 212, 169, 0.25); 19 | } 20 | ::-moz-selection, 21 | .container .container ::selection, 22 | .container .container .container .container ::selection { 23 | background-color: rgba(128, 212, 169, 0.25); 24 | } 25 | pre ::selection { 26 | background-color: rgba(172, 243, 206, 0.25); 27 | } 28 | pre ::-moz-selection { 29 | background-color: rgba(172, 243, 206, 0.25); 30 | } 31 | .container ::selection, 32 | .container .container .container ::selection { 33 | background-color: rgba(255, 255, 255, 0.5); 34 | } 35 | .container ::-moz-selection, 36 | .container .container .container ::-moz-selection { 37 | background-color: rgba(255, 255, 255, 0.5); 38 | } 39 | 40 | html, 41 | body { 42 | min-height: 100%; 43 | margin: 0; 44 | padding: 0; 45 | } 46 | 47 | form, 48 | div, 49 | a, 50 | h1, 51 | h2, 52 | h3, 53 | ul, 54 | li { 55 | border-width: 0; 56 | border-color: #d5d8db; 57 | border-style: solid; 58 | } 59 | 60 | 61 | 62 | /*Typo*/ 63 | h1, 64 | h2, 65 | h3 { 66 | line-height: 1.4; 67 | text-transform: uppercase; 68 | font-weight: 600; 69 | color: #80d4a9; 70 | } 71 | 72 | h1 em, 73 | h2 em, 74 | h3 em { 75 | text-transform: none; 76 | font-style: inherit; 77 | } 78 | 79 | h2, 80 | h3 { 81 | margin-top: 12%; 82 | } 83 | 84 | h1 { 85 | font-size: 1.6em; 86 | } 87 | 88 | h2 { 89 | font-size: 1.6em; 90 | padding-top: 12%; 91 | border-top-width: 1px; 92 | } 93 | 94 | h3 { 95 | font-size: 1.2em; 96 | } 97 | @media screen and (min-width: 30em) { 98 | h1 { 99 | font-size: 3em; 100 | } 101 | h1, 102 | h2 { 103 | text-align: center; 104 | } 105 | } 106 | @media screen and (min-width: 70em) { 107 | h1 { 108 | font-size: 4em; 109 | } 110 | } 111 | @media screen and (min-width: 40em) { 112 | h2, 113 | h3 { 114 | margin-top: 8%; 115 | } 116 | h2 { 117 | padding-top: 8%; 118 | } 119 | } 120 | 121 | a { 122 | color: #11b7d4; 123 | text-decoration: none; 124 | border-color: rgba(57, 159, 177, 0.3); 125 | border-style: dashed; 126 | border-bottom-width: 1px; 127 | 128 | -webkit-transition-property: background-color, border-color, color; 129 | -moz-transition-property: background-color, border-color, color; 130 | -ms-transition-property: background-color, border-color, color; 131 | transition-property: background-color, border-color, color; 132 | 133 | -webkit-transition-duration: 125ms; 134 | -moz-transition-duration: 125ms; 135 | -ms-transition-duration: 125ms; 136 | transition-duration: 125ms; 137 | } 138 | a:focus, 139 | a:hover { 140 | color: #80d4a9; 141 | border-bottom-color: #80d4a9; 142 | } 143 | 144 | .button { 145 | display: inline-block; 146 | border-width: 0; 147 | padding: 0.8em 1.4em; 148 | background-color: #80d4a9; 149 | color: #fff; 150 | font-weight: bold; 151 | text-transform: uppercase; 152 | font-size: 0.8em; 153 | border-radius: 3px; 154 | text-align: center; 155 | 156 | -webkit-transition-duration: 750ms; 157 | -moz-transition-duration: 750ms; 158 | -ms-transition-duration: 750ms; 159 | transition-duration: 750ms; 160 | } 161 | .button.block { 162 | display: block; 163 | } 164 | .button:focus, 165 | .button:hover { 166 | color: #fff; 167 | background-color: #5cccae; 168 | 169 | -webkit-transition-duration: 125ms; 170 | -moz-transition-duration: 125ms; 171 | -ms-transition-duration: 125ms; 172 | transition-duration: 125ms; 173 | } 174 | .button.share:focus, 175 | .button.share:hover { 176 | background-color: #55acee; 177 | } 178 | .share-buttons { 179 | padding-left: 26px; 180 | } 181 | .share-buttons a, 182 | .share-buttons div { 183 | display: inline-block; 184 | margin-left: 8px !important; 185 | margin-right: 8px !important; 186 | } 187 | 188 | 189 | 190 | /*Decorations*/ 191 | body { 192 | background-color: #80d4a9; 193 | } 194 | .body { 195 | background-color: #fff; 196 | } 197 | .body-content { 198 | max-width: 40em; 199 | margin-left: auto; 200 | margin-right: auto; 201 | padding: 2% 6% 3em 6%; 202 | } 203 | .zigzag { 204 | display: block; 205 | clear: both; 206 | width: 100%; 207 | height: 1em; 208 | border-top-width: 1em; 209 | border-color: #80d4a9; 210 | 211 | background: 212 | linear-gradient(135deg, #80d4a9 25%, transparent 25%) 1em 0, 213 | linear-gradient(225deg, #80d4a9 25%, transparent 25%) 1em 0; 214 | background-size: 2em 2em; 215 | background-position: 50% 0; 216 | } 217 | .zigzag.reverse { 218 | border-top-width: 0; 219 | border-bottom-width: 4em; 220 | background: 221 | linear-gradient(315deg, #80d4a9 25%, transparent 25%), 222 | linear-gradient(45deg, #80d4a9 25%, transparent 25%); 223 | background-size: 2em 2em; 224 | background-position: 50% -1em; 225 | } 226 | 227 | 228 | 229 | /*Layout*/ 230 | .center { 231 | text-align: center; 232 | } 233 | .clear { 234 | clear: both; 235 | } 236 | @media screen and (max-width: 30em) { 237 | .extra { 238 | display: none; 239 | } 240 | } 241 | .half { 242 | float: left; 243 | clear: none; 244 | width: 48%; 245 | } 246 | .half + .half { 247 | margin-left: 4%; 248 | } 249 | @media screen and (max-width: 30em) { 250 | .half code { 251 | font-size: 0.9em; 252 | } 253 | } 254 | @media screen and (min-width: 46em) { 255 | pre, 256 | .pull { 257 | margin-left: -2em; 258 | margin-right: -2em; 259 | } 260 | h2 { 261 | margin-left: -1.25em; 262 | margin-right: -1.25em; 263 | padding-left: 1.25em; 264 | padding-right: 1.25em; 265 | } 266 | } 267 | @media screen and (min-width: 54em) { 268 | pre, 269 | .pull { 270 | margin-left: -6em; 271 | margin-right: -6em; 272 | } 273 | h2 { 274 | margin-left: -3.75em; 275 | margin-right: -3.75em; 276 | padding-left: 3.75em; 277 | padding-right: 3.75em; 278 | } 279 | } 280 | 281 | 282 | 283 | /*Code*/ 284 | pre { 285 | font-family: inherit; 286 | font-size: inherit; 287 | tab-size: 4; 288 | } 289 | code { 290 | background-color: #282b2e; 291 | color: #e0e2e4; 292 | } 293 | .hljs, 294 | code { 295 | display: inline-block; 296 | border-radius: 3px; 297 | padding-left: 0.3em; 298 | padding-right: 0.3em; 299 | } 300 | pre code span { 301 | color: #e0e2e4; 302 | -webkit-transition-property: color, background-color; 303 | -moz-transition-property: color, background-color; 304 | -ms-transition-property: color, background-color; 305 | transition-property: color, background-color; 306 | -webkit-transition-duration: 250ms; 307 | -moz-transition-duration: 250ms; 308 | -ms-transition-duration: 250ms; 309 | transition-duration: 250ms; 310 | } 311 | .hljs, 312 | pre code { 313 | display: block; 314 | padding: 1em; 315 | overflow: auto; 316 | } 317 | pre code { 318 | border-radius: 3px; 319 | overflow: auto; 320 | max-height: 16em; 321 | } 322 | @media screen and (min-width: 54em) { 323 | .hljs, 324 | pre code { 325 | padding: 1.5em 2em; 326 | } 327 | } 328 | 329 | 330 | 331 | /*Readme adjustments*/ 332 | 333 | .readme .share-buttons { 334 | margin-top: 4%; 335 | text-align: center; 336 | } 337 | 338 | /*Download links*/ 339 | 340 | /*Hide link to this page*/ 341 | .readme > h1:first-of-type + p + p + p + p { 342 | display: none; 343 | } 344 | 345 | /*Hide unnecessary download help*/ 346 | /*.readme > h2:first-of-type + p,*/ 347 | .readme > h2:first-of-type + p + ul + p { 348 | display: none; 349 | } 350 | 351 | /*Download links*/ 352 | .readme > h2:first-of-type + p + ul { 353 | max-width: 32em; 354 | margin-left: auto; 355 | margin-right: auto; 356 | margin-bottom: 5em; 357 | padding-left: 0; 358 | list-style: none; 359 | } 360 | .readme > h2:first-of-type + p + ul li { 361 | width: 48%; 362 | float: left; 363 | clear: none; 364 | } 365 | .readme > h2:first-of-type + p + ul li:first-of-type { 366 | margin-right: 4%; 367 | } 368 | .readme > h2:first-of-type + p + ul li a { 369 | display: block; 370 | border-width: 0; 371 | padding: 0.4em 0; 372 | background-color: #80d4a9; 373 | color: #fff; 374 | font-weight: bold; 375 | border-radius: 3px; 376 | text-align: center; 377 | font-size: 0.85em; 378 | 379 | -webkit-transition-duration: 750ms; 380 | -moz-transition-duration: 750ms; 381 | -ms-transition-duration: 750ms; 382 | transition-duration: 750ms; 383 | } 384 | .readme > h2:first-of-type + p + ul li a:focus, 385 | .readme > h2:first-of-type + p + ul li a:hover { 386 | color: #fff; 387 | background-color: #5cccae; 388 | 389 | -webkit-transition-duration: 125ms; 390 | -moz-transition-duration: 125ms; 391 | -ms-transition-duration: 125ms; 392 | transition-duration: 125ms; 393 | } 394 | .readme > h2:first-of-type + p + ul + p { 395 | clear: both; 396 | } 397 | @media screen and (min-width: 30em) { 398 | .readme > h2:first-of-type + p + ul + p + pre { 399 | max-width: 18em; 400 | margin-left: auto; 401 | margin-right: auto; 402 | } 403 | .readme > h2:first-of-type + p, 404 | .readme > h2:first-of-type + p + ul + p, 405 | .readme > h2:first-of-type + p + ul + p + pre, 406 | .readme > h2:first-of-type + p + ul + p + pre + p, 407 | .readme > h2:first-of-type + p + ul + p + pre + p + p { 408 | text-align: center; 409 | } 410 | } 411 | 412 | /*Remove credits*/ 413 | .readme > h2:last-of-type, 414 | .readme > p:last-of-type { 415 | display: none; 416 | } 417 | 418 | 419 | 420 | /*Demo containers*/ 421 | .container { 422 | padding: 10px 20px; 423 | display: block; 424 | font-weight: bold; 425 | border-radius: 3px; 426 | } 427 | @media screen and (min-width: 480px) { 428 | .container { 429 | padding-left: 40px; 430 | padding-right: 40px; 431 | } 432 | } 433 | @media screen and (min-height: 640px) { 434 | .container { 435 | padding-top: 30px; 436 | padding-bottom: 30px; 437 | } 438 | } 439 | 440 | .container p { 441 | margin-top: 0; 442 | margin-bottom: 0; 443 | } 444 | .container p + p { 445 | margin-top: 2em; 446 | } 447 | 448 | /*Background patterns*/ 449 | .container { 450 | color: #48534e; 451 | background-color: #9adebb; 452 | background-image: -webkit-repeating-linear-gradient(45deg, transparent, transparent 20px, #aae8c8 20px, #aae8c8 40px); 453 | background-image: repeating-linear-gradient(45deg, transparent, transparent 20px, #aae8c8 20px, #aae8c8 40px); 454 | } 455 | 456 | .container .container { 457 | color: #fff; 458 | background-color: #393d42; 459 | background-image: -webkit-repeating-linear-gradient(135deg, transparent, transparent 40px, #434950 40px, #434950 80px); 460 | background-image: repeating-linear-gradient(-45deg, transparent, transparent 40px, #434950 40px, #434950 80px); 461 | } 462 | .container .container .container { 463 | color: #48534e; 464 | background-color: #ccf3df; 465 | background-image: -webkit-repeating-linear-gradient(135deg, transparent, transparent 20px, #aae8c8 20px, #aae8c8 40px); 466 | background-image: repeating-linear-gradient(-45deg, transparent, transparent 20px, #aae8c8 20px, #aae8c8 40px); 467 | } 468 | .container .container .container .container { 469 | color: #fff; 470 | background-color: #51575e; 471 | background-image: -webkit-repeating-linear-gradient(45deg, transparent, transparent 40px, #434950 40px, #434950 80px); 472 | background-image: repeating-linear-gradient(45deg, transparent, transparent 40px, #434950 40px, #434950 80px); 473 | } 474 | 475 | 476 | 477 | /*Container heights*/ 478 | .height-1, 479 | .height-2, 480 | .height-3, 481 | .height-4 { 482 | overflow-y: auto; 483 | } 484 | .height-1 { 485 | height: 150px; 486 | } 487 | 488 | .height-2 { 489 | height: 200px; 490 | } 491 | 492 | .height-3 { 493 | height: 250px; 494 | } 495 | 496 | .height-4 { 497 | height: 300px; 498 | } 499 | 500 | 501 | 502 | /*Demo form*/ 503 | .dummyform { 504 | border-width: 1px; 505 | border-radius: 3px; 506 | padding: 0.5em 1.5em; 507 | } 508 | .dummyform-content { 509 | position: relative; 510 | } 511 | .dummyform ul { 512 | padding-left: 0; 513 | margin-top: 0; 514 | margin-bottom: 0; 515 | list-style: none; 516 | 517 | position: absolute; 518 | left: 0; 519 | width: 20em; 520 | max-width: 100%; 521 | max-height: 8em; 522 | overflow: auto; 523 | 524 | background-color: #fff; 525 | border-radius: 3px; 526 | box-shadow: 0 2px 3px 1px rgba(40, 43, 46, 0.25); 527 | } 528 | .dummyform ul a { 529 | display: block; 530 | padding: 0.4em 0.7em; 531 | border-bottom-width: 1px; 532 | border-color: #e1e3e5; 533 | border-style: solid; 534 | } 535 | /*.dummyform ul a:focus,*/ 536 | .dummyform ul a:hover { 537 | color: #11b7d4; 538 | background-color: rgba(40, 43, 46, 0.03); 539 | } 540 | .dummyform ul li:last-of-type a { 541 | border-bottom-width: 0; 542 | } 543 | 544 | 545 | 546 | /*Demo modal*/ 547 | .modal { 548 | position: fixed; 549 | z-index: 100; 550 | width: 100%; 551 | height: 100%; 552 | left: 0; 553 | top: 0; 554 | overflow: hidden; 555 | } 556 | .modal.closed { 557 | display: none; 558 | } 559 | .modal-overlay { 560 | position: fixed; 561 | z-index: 1; 562 | width: 100%; 563 | height: 100%; 564 | top: 0; 565 | left: 0; 566 | 567 | background-color: rgba(0, 0, 0, 0.5); 568 | cursor: pointer; 569 | 570 | -webkit-transition: background-color 1000ms; 571 | -moz-transition: background-color 1000ms; 572 | -ms-transition: background-color 1000ms; 573 | transition: background-color 1000ms; 574 | } 575 | .modal-overlay:hover { 576 | background-color: rgba(0, 0, 0, 0.6); 577 | 578 | -webkit-transition-duration: 125ms; 579 | -moz-transition-duration: 125ms; 580 | -ms-transition-duration: 125ms; 581 | transition-duration: 125ms; 582 | } 583 | .modal-content { 584 | -webkit-box-sizing: border-box; 585 | -moz-box-sizing: border-box; 586 | -ms-box-sizing: border-box; 587 | box-sizing: border-box; 588 | position: fixed; 589 | z-index: 2; 590 | overflow: auto; 591 | top: 5%; 592 | left: 5%; 593 | width: 90%; 594 | height: 30em; 595 | max-height: 90%; 596 | background-color: #fff; 597 | border-radius: 3px; 598 | padding: 0.5em 1em; 599 | box-shadow: 0 1px 2px rgba(40, 43, 46, 0.25); 600 | } 601 | @media screen and (min-height: 40em) { 602 | .modal-content { 603 | top: 10%; 604 | max-height: 80%; 605 | } 606 | } 607 | @media screen and (min-width: 40em) { 608 | .modal-content { 609 | left: 10%; 610 | width: 80%; 611 | padding: 1.5em 2em; 612 | } 613 | } 614 | @media screen and (min-width: 70em) { 615 | .modal-content { 616 | left: 20%; 617 | width: 60%; 618 | } 619 | } 620 | @media screen and (min-width: 100em) { 621 | .modal-content { 622 | left: 30%; 623 | width: 40%; 624 | } 625 | } 626 | 627 | 628 | 629 | /*Wrapup*/ 630 | .footer { 631 | margin-left: auto; 632 | margin-right: auto; 633 | line-height: 1; 634 | } 635 | .footer a { 636 | display: inline-block; 637 | border-bottom-width: 0; 638 | } 639 | .footer img { 640 | display: inline-block; 641 | max-width: 100%; 642 | max-height: 3em; 643 | border-radius: 4em; 644 | vertical-align: middle; 645 | } 646 | .footer ul { 647 | padding-left: 0; 648 | list-style: none; 649 | } 650 | .footer li:nth-of-type(2n) { 651 | line-height: 3em; 652 | } 653 | .footer li { 654 | box-sizing: border-box; 655 | display: inline-block; 656 | width: 20%; 657 | } 658 | .footer li:first-of-type { 659 | padding-right: 0.5em; 660 | text-align: right; 661 | } 662 | .footer li:last-of-type { 663 | padding-left: 0.5em; 664 | text-align: left; 665 | } 666 | .footer li:first-of-type, 667 | .footer li:last-of-type { 668 | padding-top: 0.5em; 669 | width: 40%; 670 | } 671 | .footer li:first-of-type a, 672 | .footer li:last-of-type a { 673 | padding: 0.5em; 674 | } 675 | @media screen and (min-width: 30em) { 676 | .footer img { 677 | max-height: 4em; 678 | } 679 | .footer li:nth-of-type(2n) { 680 | line-height: 4em; 681 | } 682 | .footer li:first-of-type, 683 | .footer li:last-of-type { 684 | padding-top: 1em; 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | scroll-scope.js 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 |

scroll-scope.js

56 | 57 |

58 | This plugin is old. Things have changed over the years and it probably won't work. You should use something else. 59 |

60 | 61 | 62 |

Small jQuery plugin to keep parent element still when scrolling an element to its boundary.

63 |

Commonly in scroll interaction, user hovers a mouse cursor over a scrollable element and uses trackpad or mouse wheel to scroll the element. When an element reaches its boundary, its parent element continues scrolling. Usually this means that the user will continue moving down the page when attempting to interact with an specific container. This is a common issue with dropdown menus and modal dialogs.

64 |

This behavior is most prevalent in desktop browsers. To fix this problem that shouldn't even exist, scroll-scope.js exists.

65 |

See project home and demos on scroll-scope.vercel.app.

66 |

Get the plugin

67 |

Current version is 0.1.0.

68 | 72 |

Install with Bower:

73 |
bower install scroll-scope
74 |

File issues or pull requests to share potential improvements. The plugin is released under MIT. Source is available on GitHub. To contribute, clone the repo, make changes in scroll-scope.js and build the minified version with gulp.

75 |

Usage

76 |

Add the data-scroll-scope attribute to any scrollable element on the page:

77 |
<!-- Scope scrolling of element when it overflows -->
 78 | 	<div class="my-scrollable-element" data-scroll-scope>
 79 | 	
 80 | 	<!-- Scope scrolling of element whether or not it overflows -->
 81 | 	<div class="another-scrollable-element" data-scroll-scope="force">
82 |

Include and initialize:

83 |
<!-- jQuery comes first, then the plugin -->
 84 | 	<script type="text/javascript" src="//code.jquery.com/jquery-2.1.4.min.js"></script>
 85 | 	<script type="text/javascript" src="scroll-scope.min.js"></script>
 86 | 	
 87 | 	<!-- Activate the plugin on your page -->
 88 | 	<script type="text/javascript">
 89 | 		$(document).scrollScope();
 90 | 	</script>
91 |

The plugin works declaratively, meaning that it's attached to the document object (or any parent container you choose) instead of individual scrollable containers. This means that any DOM elements that are added or removed after page load do not need to be bound separately. You can even toggle force on and off during runtime if you please.

92 |

Options

93 |

You can change which elements and events are targeted by setting them upon initialization. Note that having events listed here does not mean they're blocked automatically, rather the plugin listens to these events and evaluates them when encountered.

94 |

Here are the defaults:

95 |
$(document).scrollScope({
 96 | 		elements: '[data-scroll-scope]',
 97 | 		forcedElements: '[data-scroll-scope="force"]',
 98 | 		events: 'DOMMouseScroll mousewheel scroll touchstart touchmove'
 99 | 	});
100 |

Advanced use

101 |

If you need full access to ScrollScope object (for example to getTargetedElements() or unbind() it later), instantiate a raw ScrollScope object yourself. You need to bind it to the document yourself in this case:

102 |
// Create new instance
103 | 	var myScrollScopeInstance = new ScrollScope({
104 | 		options: 'here'
105 | 	});
106 | 	
107 | 	// Bind to document
108 | 	myScrollScopeInstance.bind(document);
109 | 	
110 | 	// Use the object for whatever you wish
111 | 	myScrollScopeInstance.mainContainer.css('background', 'blue');
112 | 	myScrollScopeInstance.getTargetedElements().css('background', 'red');
113 | 	
114 | 	// Detach from document if no longer needed
115 | 	myScrollScope.unbind();
116 |

Credits

117 |

Plugin by Jerry Jäppinen (under MIT).

118 |
119 | 120 |
121 | 122 | 123 | 124 |

Demos

125 | 126 |
127 | 128 |
129 |

Default behavior

130 |
131 |
132 |

data-scroll-scope

133 |
134 |
135 | 136 |
137 | 138 |
139 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

140 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

141 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

142 |
143 | 144 |
145 | 146 |
147 | 148 |
149 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

150 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

151 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

152 |
153 | 154 |
155 |
156 |
157 | 158 |

When scrolling the container on the right, scrolling should stop when you get to the end and your document should stay still. For some browsers, this is also the default behavior.

159 | 160 |

Also keep in mind that especially mobile browsers allow the user to keep scrolling the parent if the child is at its respective boundary when initiating the scroll. If you want to block scrolling even in these cases, use force (see modal dialog example below).

161 | 162 | 163 | 164 |

Real-life use case: dropdown

165 | 166 |

The document will stay still even when you scroll the results container to the end.

167 | 168 |
169 |
170 | 171 |

172 | 173 | 180 | 181 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

182 | 183 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

184 | 185 |
186 | 187 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

188 | 189 |
190 | 191 |
192 |
193 | 194 | 195 | 196 |

Real-life use case: code block

197 | 198 |

199 | 200 |
/*A page with lots of code blocks could set a max-height for them*/
201 | pre {
202 | 	overflow: auto;
203 | 	max-height: 16em;
204 | }
205 | 
206 | /*Lots of code*/
207 | code {
208 | 	display: inline-block;
209 | 	color: #555;
210 | 	background-color: #f6f6f6;
211 | 	border-radius: 3px;
212 | 	padding-left: 0.3em;
213 | 	padding-right: 0.3em;
214 | }
215 | pre code {
216 | 	display: block;
217 | 	padding: 1em;
218 | 	overflow: auto;
219 | }
220 | 221 | 222 | 223 |

Real-life use case: modal dialog

224 | 225 | 251 | 252 |

Modal dialog implementations tend to scroll the document. Even Bootstrap's modal dialog overlay lets the scroll event through on mobile Safari!

253 | 254 |

Open the demo dialog

255 | 256 |

In this quite trivial custom dialog implementation, we scope the scrolling in both the overall container and the content area so the document maintains its position. We also want to use force to disable parent scrolling even when the areas do not overflow.

257 | 258 |

Note! When scroll events are blocked with force, mobile Safari also blocks click events for that element. To close the modal on overlay click, we must attach the click event handler to an element that does not use data-scroll-scope.

259 | 260 |

The source looks like this:

261 | 262 |
<div class="modal" data-scroll-scope="force">
263 | 	<div class="modal-content" data-scroll-scope="force">
264 | 		...
265 | 	</div>
266 | 	<div class="modal-overlay" data-action="toggle-modal"></div>
267 | </div>
268 | 269 |
// Quick custom toggle
270 | $(document).on('click', '[data-action="toggle-modal"]', function (event) {
271 | 	event.preventDefault();
272 | 	$('.modal').toggleClass('closed');
273 | });
274 | 275 |
.modal {
276 | 	position: fixed;
277 | 	z-index: 100;
278 | 	width: 100%;
279 | 	height: 100%;
280 | 	left: 0;
281 | 	top: 0;
282 | 	overflow: hidden;
283 | }
284 | 	.modal.closed {
285 | 		display: none;
286 | 	}
287 | .modal-overlay {
288 | 	position: fixed;
289 | 	z-index: 1;
290 | 	width: 100%;
291 | 	height: 100%;
292 | 	top: 0;
293 | 	left: 0;
294 | 	background-color: rgba(0, 0, 0, 0.5);
295 | }
296 | .modal-content {
297 | 	position: fixed;
298 | 	z-index: 2;
299 | 	overflow: auto;
300 | 	top: 5%;
301 | 	left: 5%;
302 | 	width: 90%;
303 | 	height: 30em;
304 | 	max-height: 90%;
305 | 	background-color: #fff;
306 | }
307 | 308 | 309 | 310 |

Nested containers

311 | 312 |
313 |
314 |
315 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

316 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

317 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

318 |
319 |
320 |
321 | 322 | 323 | 324 |

Impractical number of nested containers

325 | 326 |
327 |
328 |
329 |
330 |
331 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

332 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

333 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

334 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

335 |
336 |
337 |
338 |
339 |
340 | 341 | 342 | 343 | 352 | 353 | 354 | 355 |
356 |
357 |
358 | 359 | 360 | 361 | 362 | 363 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scroll-scope", 3 | "version": "0.1.0", 4 | "description": "Small jQuery plugin to keep parent element still when scrolling an element to its boundary.", 5 | "main": "scroll-scope.js", 6 | "dependencies": {}, 7 | "scripts": { 8 | "dev": "npx http-server" 9 | }, 10 | "devDependencies": { 11 | "del": "^1.2.0", 12 | "gulp": "^3.8.11", 13 | "gulp-load-plugins": "^0.10.0", 14 | "gulp-plumber": "^1.0.1", 15 | "gulp-rename": "^1.2.2", 16 | "gulp-uglify": "^1.2.0", 17 | "yargs": "^3.9.1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "" 22 | }, 23 | "author": "Jerry Jäppinen ", 24 | "license": "MIT", 25 | "homepage": "http://eiskis.net/scroll-scope" 26 | } 27 | -------------------------------------------------------------------------------- /php/Parsedown.php: -------------------------------------------------------------------------------- 1 | DefinitionData = array(); 28 | 29 | # standardize line breaks 30 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 31 | 32 | # remove surrounding line breaks 33 | $text = trim($text, "\n"); 34 | 35 | # split text into lines 36 | $lines = explode("\n", $text); 37 | 38 | # iterate through lines to identify blocks 39 | $markup = $this->lines($lines); 40 | 41 | # trim line breaks 42 | $markup = trim($markup, "\n"); 43 | 44 | return $markup; 45 | } 46 | 47 | # 48 | # Setters 49 | # 50 | 51 | function setBreaksEnabled($breaksEnabled) 52 | { 53 | $this->breaksEnabled = $breaksEnabled; 54 | 55 | return $this; 56 | } 57 | 58 | protected $breaksEnabled; 59 | 60 | function setMarkupEscaped($markupEscaped) 61 | { 62 | $this->markupEscaped = $markupEscaped; 63 | 64 | return $this; 65 | } 66 | 67 | protected $markupEscaped; 68 | 69 | function setUrlsLinked($urlsLinked) 70 | { 71 | $this->urlsLinked = $urlsLinked; 72 | 73 | return $this; 74 | } 75 | 76 | protected $urlsLinked = true; 77 | 78 | # 79 | # Lines 80 | # 81 | 82 | protected $BlockTypes = array( 83 | '#' => array('Header'), 84 | '*' => array('Rule', 'List'), 85 | '+' => array('List'), 86 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 87 | '0' => array('List'), 88 | '1' => array('List'), 89 | '2' => array('List'), 90 | '3' => array('List'), 91 | '4' => array('List'), 92 | '5' => array('List'), 93 | '6' => array('List'), 94 | '7' => array('List'), 95 | '8' => array('List'), 96 | '9' => array('List'), 97 | ':' => array('Table'), 98 | '<' => array('Comment', 'Markup'), 99 | '=' => array('SetextHeader'), 100 | '>' => array('Quote'), 101 | '[' => array('Reference'), 102 | '_' => array('Rule'), 103 | '`' => array('FencedCode'), 104 | '|' => array('Table'), 105 | '~' => array('FencedCode'), 106 | ); 107 | 108 | # ~ 109 | 110 | protected $DefinitionTypes = array( 111 | '[' => array('Reference'), 112 | ); 113 | 114 | # ~ 115 | 116 | protected $unmarkedBlockTypes = array( 117 | 'Code', 118 | ); 119 | 120 | # 121 | # Blocks 122 | # 123 | 124 | private function lines(array $lines) 125 | { 126 | $CurrentBlock = null; 127 | 128 | foreach ($lines as $line) 129 | { 130 | if (chop($line) === '') 131 | { 132 | if (isset($CurrentBlock)) 133 | { 134 | $CurrentBlock['interrupted'] = true; 135 | } 136 | 137 | continue; 138 | } 139 | 140 | if (strpos($line, "\t") !== false) 141 | { 142 | $parts = explode("\t", $line); 143 | 144 | $line = $parts[0]; 145 | 146 | unset($parts[0]); 147 | 148 | foreach ($parts as $part) 149 | { 150 | $shortage = 4 - mb_strlen($line, 'utf-8') % 4; 151 | 152 | $line .= str_repeat(' ', $shortage); 153 | $line .= $part; 154 | } 155 | } 156 | 157 | $indent = 0; 158 | 159 | while (isset($line[$indent]) and $line[$indent] === ' ') 160 | { 161 | $indent ++; 162 | } 163 | 164 | $text = $indent > 0 ? substr($line, $indent) : $line; 165 | 166 | # ~ 167 | 168 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 169 | 170 | # ~ 171 | 172 | if (isset($CurrentBlock['incomplete'])) 173 | { 174 | $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); 175 | 176 | if (isset($Block)) 177 | { 178 | $CurrentBlock = $Block; 179 | 180 | continue; 181 | } 182 | else 183 | { 184 | if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) 185 | { 186 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 187 | } 188 | 189 | unset($CurrentBlock['incomplete']); 190 | } 191 | } 192 | 193 | # ~ 194 | 195 | $marker = $text[0]; 196 | 197 | # ~ 198 | 199 | $blockTypes = $this->unmarkedBlockTypes; 200 | 201 | if (isset($this->BlockTypes[$marker])) 202 | { 203 | foreach ($this->BlockTypes[$marker] as $blockType) 204 | { 205 | $blockTypes []= $blockType; 206 | } 207 | } 208 | 209 | # 210 | # ~ 211 | 212 | foreach ($blockTypes as $blockType) 213 | { 214 | $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); 215 | 216 | if (isset($Block)) 217 | { 218 | $Block['type'] = $blockType; 219 | 220 | if ( ! isset($Block['identified'])) 221 | { 222 | $Blocks []= $CurrentBlock; 223 | 224 | $Block['identified'] = true; 225 | } 226 | 227 | if (method_exists($this, 'block'.$blockType.'Continue')) 228 | { 229 | $Block['incomplete'] = true; 230 | } 231 | 232 | $CurrentBlock = $Block; 233 | 234 | continue 2; 235 | } 236 | } 237 | 238 | # ~ 239 | 240 | if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) 241 | { 242 | $CurrentBlock['element']['text'] .= "\n".$text; 243 | } 244 | else 245 | { 246 | $Blocks []= $CurrentBlock; 247 | 248 | $CurrentBlock = $this->paragraph($Line); 249 | 250 | $CurrentBlock['identified'] = true; 251 | } 252 | } 253 | 254 | # ~ 255 | 256 | if (isset($CurrentBlock['incomplete']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) 257 | { 258 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 259 | } 260 | 261 | # ~ 262 | 263 | $Blocks []= $CurrentBlock; 264 | 265 | unset($Blocks[0]); 266 | 267 | # ~ 268 | 269 | $markup = ''; 270 | 271 | foreach ($Blocks as $Block) 272 | { 273 | if (isset($Block['hidden'])) 274 | { 275 | continue; 276 | } 277 | 278 | $markup .= "\n"; 279 | $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); 280 | } 281 | 282 | $markup .= "\n"; 283 | 284 | # ~ 285 | 286 | return $markup; 287 | } 288 | 289 | # 290 | # Code 291 | 292 | protected function blockCode($Line, $Block = null) 293 | { 294 | if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) 295 | { 296 | return; 297 | } 298 | 299 | if ($Line['indent'] >= 4) 300 | { 301 | $text = substr($Line['body'], 4); 302 | 303 | $Block = array( 304 | 'element' => array( 305 | 'name' => 'pre', 306 | 'handler' => 'element', 307 | 'text' => array( 308 | 'name' => 'code', 309 | 'text' => $text, 310 | ), 311 | ), 312 | ); 313 | 314 | return $Block; 315 | } 316 | } 317 | 318 | protected function blockCodeContinue($Line, $Block) 319 | { 320 | if ($Line['indent'] >= 4) 321 | { 322 | if (isset($Block['interrupted'])) 323 | { 324 | $Block['element']['text']['text'] .= "\n"; 325 | 326 | unset($Block['interrupted']); 327 | } 328 | 329 | $Block['element']['text']['text'] .= "\n"; 330 | 331 | $text = substr($Line['body'], 4); 332 | 333 | $Block['element']['text']['text'] .= $text; 334 | 335 | return $Block; 336 | } 337 | } 338 | 339 | protected function blockCodeComplete($Block) 340 | { 341 | $text = $Block['element']['text']['text']; 342 | 343 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 344 | 345 | $Block['element']['text']['text'] = $text; 346 | 347 | return $Block; 348 | } 349 | 350 | # 351 | # Comment 352 | 353 | protected function blockComment($Line) 354 | { 355 | if ($this->markupEscaped) 356 | { 357 | return; 358 | } 359 | 360 | if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') 361 | { 362 | $Block = array( 363 | 'markup' => $Line['body'], 364 | ); 365 | 366 | if (preg_match('/-->$/', $Line['text'])) 367 | { 368 | $Block['closed'] = true; 369 | } 370 | 371 | return $Block; 372 | } 373 | } 374 | 375 | protected function blockCommentContinue($Line, array $Block) 376 | { 377 | if (isset($Block['closed'])) 378 | { 379 | return; 380 | } 381 | 382 | $Block['markup'] .= "\n" . $Line['body']; 383 | 384 | if (preg_match('/-->$/', $Line['text'])) 385 | { 386 | $Block['closed'] = true; 387 | } 388 | 389 | return $Block; 390 | } 391 | 392 | # 393 | # Fenced Code 394 | 395 | protected function blockFencedCode($Line) 396 | { 397 | if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) 398 | { 399 | $Element = array( 400 | 'name' => 'code', 401 | 'text' => '', 402 | ); 403 | 404 | if (isset($matches[2])) 405 | { 406 | $class = 'language-'.$matches[2]; 407 | 408 | $Element['attributes'] = array( 409 | 'class' => $class, 410 | ); 411 | } 412 | 413 | $Block = array( 414 | 'char' => $Line['text'][0], 415 | 'element' => array( 416 | 'name' => 'pre', 417 | 'handler' => 'element', 418 | 'text' => $Element, 419 | ), 420 | ); 421 | 422 | return $Block; 423 | } 424 | } 425 | 426 | protected function blockFencedCodeContinue($Line, $Block) 427 | { 428 | if (isset($Block['complete'])) 429 | { 430 | return; 431 | } 432 | 433 | if (isset($Block['interrupted'])) 434 | { 435 | $Block['element']['text']['text'] .= "\n"; 436 | 437 | unset($Block['interrupted']); 438 | } 439 | 440 | if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) 441 | { 442 | $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); 443 | 444 | $Block['complete'] = true; 445 | 446 | return $Block; 447 | } 448 | 449 | $Block['element']['text']['text'] .= "\n".$Line['body'];; 450 | 451 | return $Block; 452 | } 453 | 454 | protected function blockFencedCodeComplete($Block) 455 | { 456 | $text = $Block['element']['text']['text']; 457 | 458 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 459 | 460 | $Block['element']['text']['text'] = $text; 461 | 462 | return $Block; 463 | } 464 | 465 | # 466 | # Header 467 | 468 | protected function blockHeader($Line) 469 | { 470 | if (isset($Line['text'][1])) 471 | { 472 | $level = 1; 473 | 474 | while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') 475 | { 476 | $level ++; 477 | } 478 | 479 | if ($level > 6) 480 | { 481 | return; 482 | } 483 | 484 | $text = trim($Line['text'], '# '); 485 | 486 | $Block = array( 487 | 'element' => array( 488 | 'name' => 'h' . min(6, $level), 489 | 'text' => $text, 490 | 'handler' => 'line', 491 | ), 492 | ); 493 | 494 | return $Block; 495 | } 496 | } 497 | 498 | # 499 | # List 500 | 501 | protected function blockList($Line) 502 | { 503 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); 504 | 505 | if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) 506 | { 507 | $Block = array( 508 | 'indent' => $Line['indent'], 509 | 'pattern' => $pattern, 510 | 'element' => array( 511 | 'name' => $name, 512 | 'handler' => 'elements', 513 | ), 514 | ); 515 | 516 | $Block['li'] = array( 517 | 'name' => 'li', 518 | 'handler' => 'li', 519 | 'text' => array( 520 | $matches[2], 521 | ), 522 | ); 523 | 524 | $Block['element']['text'] []= & $Block['li']; 525 | 526 | return $Block; 527 | } 528 | } 529 | 530 | protected function blockListContinue($Line, array $Block) 531 | { 532 | if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) 533 | { 534 | if (isset($Block['interrupted'])) 535 | { 536 | $Block['li']['text'] []= ''; 537 | 538 | unset($Block['interrupted']); 539 | } 540 | 541 | unset($Block['li']); 542 | 543 | $text = isset($matches[1]) ? $matches[1] : ''; 544 | 545 | $Block['li'] = array( 546 | 'name' => 'li', 547 | 'handler' => 'li', 548 | 'text' => array( 549 | $text, 550 | ), 551 | ); 552 | 553 | $Block['element']['text'] []= & $Block['li']; 554 | 555 | return $Block; 556 | } 557 | 558 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) 559 | { 560 | return $Block; 561 | } 562 | 563 | if ( ! isset($Block['interrupted'])) 564 | { 565 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 566 | 567 | $Block['li']['text'] []= $text; 568 | 569 | return $Block; 570 | } 571 | 572 | if ($Line['indent'] > 0) 573 | { 574 | $Block['li']['text'] []= ''; 575 | 576 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 577 | 578 | $Block['li']['text'] []= $text; 579 | 580 | unset($Block['interrupted']); 581 | 582 | return $Block; 583 | } 584 | } 585 | 586 | # 587 | # Quote 588 | 589 | protected function blockQuote($Line) 590 | { 591 | if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 592 | { 593 | $Block = array( 594 | 'element' => array( 595 | 'name' => 'blockquote', 596 | 'handler' => 'lines', 597 | 'text' => (array) $matches[1], 598 | ), 599 | ); 600 | 601 | return $Block; 602 | } 603 | } 604 | 605 | protected function blockQuoteContinue($Line, array $Block) 606 | { 607 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 608 | { 609 | if (isset($Block['interrupted'])) 610 | { 611 | $Block['element']['text'] []= ''; 612 | 613 | unset($Block['interrupted']); 614 | } 615 | 616 | $Block['element']['text'] []= $matches[1]; 617 | 618 | return $Block; 619 | } 620 | 621 | if ( ! isset($Block['interrupted'])) 622 | { 623 | $Block['element']['text'] []= $Line['text']; 624 | 625 | return $Block; 626 | } 627 | } 628 | 629 | # 630 | # Rule 631 | 632 | protected function blockRule($Line) 633 | { 634 | if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) 635 | { 636 | $Block = array( 637 | 'element' => array( 638 | 'name' => 'hr' 639 | ), 640 | ); 641 | 642 | return $Block; 643 | } 644 | } 645 | 646 | # 647 | # Setext 648 | 649 | protected function blockSetextHeader($Line, array $Block = null) 650 | { 651 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 652 | { 653 | return; 654 | } 655 | 656 | if (chop($Line['text'], $Line['text'][0]) === '') 657 | { 658 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 659 | 660 | return $Block; 661 | } 662 | } 663 | 664 | # 665 | # Markup 666 | 667 | protected function blockMarkup($Line) 668 | { 669 | if ($this->markupEscaped) 670 | { 671 | return; 672 | } 673 | 674 | if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) 675 | { 676 | if (in_array($matches[1], $this->textLevelElements)) 677 | { 678 | return; 679 | } 680 | 681 | $Block = array( 682 | 'name' => $matches[1], 683 | 'depth' => 0, 684 | 'markup' => $Line['text'], 685 | ); 686 | 687 | $length = strlen($matches[0]); 688 | 689 | $remainder = substr($Line['text'], $length); 690 | 691 | if (trim($remainder) === '') 692 | { 693 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 694 | { 695 | $Block['closed'] = true; 696 | 697 | $Block['void'] = true; 698 | } 699 | } 700 | else 701 | { 702 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 703 | { 704 | return; 705 | } 706 | 707 | if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) 708 | { 709 | $Block['closed'] = true; 710 | } 711 | } 712 | 713 | return $Block; 714 | } 715 | } 716 | 717 | protected function blockMarkupContinue($Line, array $Block) 718 | { 719 | if (isset($Block['closed'])) 720 | { 721 | return; 722 | } 723 | 724 | if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open 725 | { 726 | $Block['depth'] ++; 727 | } 728 | 729 | if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close 730 | { 731 | if ($Block['depth'] > 0) 732 | { 733 | $Block['depth'] --; 734 | } 735 | else 736 | { 737 | $Block['closed'] = true; 738 | } 739 | } 740 | 741 | if (isset($Block['interrupted'])) 742 | { 743 | $Block['markup'] .= "\n"; 744 | 745 | unset($Block['interrupted']); 746 | } 747 | 748 | $Block['markup'] .= "\n".$Line['body']; 749 | 750 | return $Block; 751 | } 752 | 753 | # 754 | # Reference 755 | 756 | protected function blockReference($Line) 757 | { 758 | if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) 759 | { 760 | $id = strtolower($matches[1]); 761 | 762 | $Data = array( 763 | 'url' => $matches[2], 764 | 'title' => null, 765 | ); 766 | 767 | if (isset($matches[3])) 768 | { 769 | $Data['title'] = $matches[3]; 770 | } 771 | 772 | $this->DefinitionData['Reference'][$id] = $Data; 773 | 774 | $Block = array( 775 | 'hidden' => true, 776 | ); 777 | 778 | return $Block; 779 | } 780 | } 781 | 782 | # 783 | # Table 784 | 785 | protected function blockTable($Line, array $Block = null) 786 | { 787 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 788 | { 789 | return; 790 | } 791 | 792 | if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') 793 | { 794 | $alignments = array(); 795 | 796 | $divider = $Line['text']; 797 | 798 | $divider = trim($divider); 799 | $divider = trim($divider, '|'); 800 | 801 | $dividerCells = explode('|', $divider); 802 | 803 | foreach ($dividerCells as $dividerCell) 804 | { 805 | $dividerCell = trim($dividerCell); 806 | 807 | if ($dividerCell === '') 808 | { 809 | continue; 810 | } 811 | 812 | $alignment = null; 813 | 814 | if ($dividerCell[0] === ':') 815 | { 816 | $alignment = 'left'; 817 | } 818 | 819 | if (substr($dividerCell, - 1) === ':') 820 | { 821 | $alignment = $alignment === 'left' ? 'center' : 'right'; 822 | } 823 | 824 | $alignments []= $alignment; 825 | } 826 | 827 | # ~ 828 | 829 | $HeaderElements = array(); 830 | 831 | $header = $Block['element']['text']; 832 | 833 | $header = trim($header); 834 | $header = trim($header, '|'); 835 | 836 | $headerCells = explode('|', $header); 837 | 838 | foreach ($headerCells as $index => $headerCell) 839 | { 840 | $headerCell = trim($headerCell); 841 | 842 | $HeaderElement = array( 843 | 'name' => 'th', 844 | 'text' => $headerCell, 845 | 'handler' => 'line', 846 | ); 847 | 848 | if (isset($alignments[$index])) 849 | { 850 | $alignment = $alignments[$index]; 851 | 852 | $HeaderElement['attributes'] = array( 853 | 'style' => 'text-align: '.$alignment.';', 854 | ); 855 | } 856 | 857 | $HeaderElements []= $HeaderElement; 858 | } 859 | 860 | # ~ 861 | 862 | $Block = array( 863 | 'alignments' => $alignments, 864 | 'identified' => true, 865 | 'element' => array( 866 | 'name' => 'table', 867 | 'handler' => 'elements', 868 | ), 869 | ); 870 | 871 | $Block['element']['text'] []= array( 872 | 'name' => 'thead', 873 | 'handler' => 'elements', 874 | ); 875 | 876 | $Block['element']['text'] []= array( 877 | 'name' => 'tbody', 878 | 'handler' => 'elements', 879 | 'text' => array(), 880 | ); 881 | 882 | $Block['element']['text'][0]['text'] []= array( 883 | 'name' => 'tr', 884 | 'handler' => 'elements', 885 | 'text' => $HeaderElements, 886 | ); 887 | 888 | return $Block; 889 | } 890 | } 891 | 892 | protected function blockTableContinue($Line, array $Block) 893 | { 894 | if (isset($Block['interrupted'])) 895 | { 896 | return; 897 | } 898 | 899 | if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) 900 | { 901 | $Elements = array(); 902 | 903 | $row = $Line['text']; 904 | 905 | $row = trim($row); 906 | $row = trim($row, '|'); 907 | 908 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); 909 | 910 | foreach ($matches[0] as $index => $cell) 911 | { 912 | $cell = trim($cell); 913 | 914 | $Element = array( 915 | 'name' => 'td', 916 | 'handler' => 'line', 917 | 'text' => $cell, 918 | ); 919 | 920 | if (isset($Block['alignments'][$index])) 921 | { 922 | $Element['attributes'] = array( 923 | 'style' => 'text-align: '.$Block['alignments'][$index].';', 924 | ); 925 | } 926 | 927 | $Elements []= $Element; 928 | } 929 | 930 | $Element = array( 931 | 'name' => 'tr', 932 | 'handler' => 'elements', 933 | 'text' => $Elements, 934 | ); 935 | 936 | $Block['element']['text'][1]['text'] []= $Element; 937 | 938 | return $Block; 939 | } 940 | } 941 | 942 | # 943 | # ~ 944 | # 945 | 946 | protected function paragraph($Line) 947 | { 948 | $Block = array( 949 | 'element' => array( 950 | 'name' => 'p', 951 | 'text' => $Line['text'], 952 | 'handler' => 'line', 953 | ), 954 | ); 955 | 956 | return $Block; 957 | } 958 | 959 | # 960 | # Inline Elements 961 | # 962 | 963 | protected $InlineTypes = array( 964 | '"' => array('SpecialCharacter'), 965 | '!' => array('Image'), 966 | '&' => array('SpecialCharacter'), 967 | '*' => array('Emphasis'), 968 | ':' => array('Url'), 969 | '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), 970 | '>' => array('SpecialCharacter'), 971 | '[' => array('Link'), 972 | '_' => array('Emphasis'), 973 | '`' => array('Code'), 974 | '~' => array('Strikethrough'), 975 | '\\' => array('EscapeSequence'), 976 | ); 977 | 978 | # ~ 979 | 980 | protected $inlineMarkerList = '!"*_&[:<>`~\\'; 981 | 982 | # 983 | # ~ 984 | # 985 | 986 | public function line($text) 987 | { 988 | $markup = ''; 989 | 990 | $unexaminedText = $text; 991 | 992 | $markerPosition = 0; 993 | 994 | while ($excerpt = strpbrk($unexaminedText, $this->inlineMarkerList)) 995 | { 996 | $marker = $excerpt[0]; 997 | 998 | $markerPosition += strpos($unexaminedText, $marker); 999 | 1000 | $Excerpt = array('text' => $excerpt, 'context' => $text); 1001 | 1002 | foreach ($this->InlineTypes[$marker] as $inlineType) 1003 | { 1004 | $Inline = $this->{'inline'.$inlineType}($Excerpt); 1005 | 1006 | if ( ! isset($Inline)) 1007 | { 1008 | continue; 1009 | } 1010 | 1011 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) # position is ahead of marker 1012 | { 1013 | continue; 1014 | } 1015 | 1016 | if ( ! isset($Inline['position'])) 1017 | { 1018 | $Inline['position'] = $markerPosition; 1019 | } 1020 | 1021 | $unmarkedText = substr($text, 0, $Inline['position']); 1022 | 1023 | $markup .= $this->unmarkedText($unmarkedText); 1024 | 1025 | $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); 1026 | 1027 | $text = substr($text, $Inline['position'] + $Inline['extent']); 1028 | 1029 | $unexaminedText = $text; 1030 | 1031 | $markerPosition = 0; 1032 | 1033 | continue 2; 1034 | } 1035 | 1036 | $unexaminedText = substr($excerpt, 1); 1037 | 1038 | $markerPosition ++; 1039 | } 1040 | 1041 | $markup .= $this->unmarkedText($text); 1042 | 1043 | return $markup; 1044 | } 1045 | 1046 | # 1047 | # ~ 1048 | # 1049 | 1050 | protected function inlineCode($Excerpt) 1051 | { 1052 | $marker = $Excerpt['text'][0]; 1053 | 1054 | if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), 1062 | 'element' => array( 1063 | 'name' => 'code', 1064 | 'text' => $text, 1065 | ), 1066 | ); 1067 | } 1068 | } 1069 | 1070 | protected function inlineEmailTag($Excerpt) 1071 | { 1072 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) 1073 | { 1074 | $url = $matches[1]; 1075 | 1076 | if ( ! isset($matches[2])) 1077 | { 1078 | $url = 'mailto:' . $url; 1079 | } 1080 | 1081 | return array( 1082 | 'extent' => strlen($matches[0]), 1083 | 'element' => array( 1084 | 'name' => 'a', 1085 | 'text' => $matches[1], 1086 | 'attributes' => array( 1087 | 'href' => $url, 1088 | ), 1089 | ), 1090 | ); 1091 | } 1092 | } 1093 | 1094 | protected function inlineEmphasis($Excerpt) 1095 | { 1096 | if ( ! isset($Excerpt['text'][1])) 1097 | { 1098 | return; 1099 | } 1100 | 1101 | $marker = $Excerpt['text'][0]; 1102 | 1103 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1104 | { 1105 | $emphasis = 'strong'; 1106 | } 1107 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1108 | { 1109 | $emphasis = 'em'; 1110 | } 1111 | else 1112 | { 1113 | return; 1114 | } 1115 | 1116 | return array( 1117 | 'extent' => strlen($matches[0]), 1118 | 'element' => array( 1119 | 'name' => $emphasis, 1120 | 'handler' => 'line', 1121 | 'text' => $matches[1], 1122 | ), 1123 | ); 1124 | } 1125 | 1126 | protected function inlineEscapeSequence($Excerpt) 1127 | { 1128 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1129 | { 1130 | return array( 1131 | 'markup' => $Excerpt['text'][1], 1132 | 'extent' => 2, 1133 | ); 1134 | } 1135 | } 1136 | 1137 | protected function inlineImage($Excerpt) 1138 | { 1139 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1140 | { 1141 | return; 1142 | } 1143 | 1144 | $Excerpt['text']= substr($Excerpt['text'], 1); 1145 | 1146 | $Link = $this->inlineLink($Excerpt); 1147 | 1148 | if ($Link === null) 1149 | { 1150 | return; 1151 | } 1152 | 1153 | $Inline = array( 1154 | 'extent' => $Link['extent'] + 1, 1155 | 'element' => array( 1156 | 'name' => 'img', 1157 | 'attributes' => array( 1158 | 'src' => $Link['element']['attributes']['href'], 1159 | 'alt' => $Link['element']['text'], 1160 | ), 1161 | ), 1162 | ); 1163 | 1164 | $Inline['element']['attributes'] += $Link['element']['attributes']; 1165 | 1166 | unset($Inline['element']['attributes']['href']); 1167 | 1168 | return $Inline; 1169 | } 1170 | 1171 | protected function inlineLink($Excerpt) 1172 | { 1173 | $Element = array( 1174 | 'name' => 'a', 1175 | 'handler' => 'line', 1176 | 'text' => null, 1177 | 'attributes' => array( 1178 | 'href' => null, 1179 | 'title' => null, 1180 | ), 1181 | ); 1182 | 1183 | $extent = 0; 1184 | 1185 | $remainder = $Excerpt['text']; 1186 | 1187 | if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) 1188 | { 1189 | $Element['text'] = $matches[1]; 1190 | 1191 | $extent += strlen($matches[0]); 1192 | 1193 | $remainder = substr($remainder, $extent); 1194 | } 1195 | else 1196 | { 1197 | return; 1198 | } 1199 | 1200 | if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) 1201 | { 1202 | $Element['attributes']['href'] = $matches[1]; 1203 | 1204 | if (isset($matches[2])) 1205 | { 1206 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1207 | } 1208 | 1209 | $extent += strlen($matches[0]); 1210 | } 1211 | else 1212 | { 1213 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1214 | { 1215 | $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; 1216 | $definition = strtolower($definition); 1217 | 1218 | $extent += strlen($matches[0]); 1219 | } 1220 | else 1221 | { 1222 | $definition = strtolower($Element['text']); 1223 | } 1224 | 1225 | if ( ! isset($this->DefinitionData['Reference'][$definition])) 1226 | { 1227 | return; 1228 | } 1229 | 1230 | $Definition = $this->DefinitionData['Reference'][$definition]; 1231 | 1232 | $Element['attributes']['href'] = $Definition['url']; 1233 | $Element['attributes']['title'] = $Definition['title']; 1234 | } 1235 | 1236 | $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); 1237 | 1238 | return array( 1239 | 'extent' => $extent, 1240 | 'element' => $Element, 1241 | ); 1242 | } 1243 | 1244 | protected function inlineMarkup($Excerpt) 1245 | { 1246 | if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) 1247 | { 1248 | return; 1249 | } 1250 | 1251 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) 1252 | { 1253 | return array( 1254 | 'markup' => $matches[0], 1255 | 'extent' => strlen($matches[0]), 1256 | ); 1257 | } 1258 | 1259 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) 1260 | { 1261 | return array( 1262 | 'markup' => $matches[0], 1263 | 'extent' => strlen($matches[0]), 1264 | ); 1265 | } 1266 | 1267 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) 1268 | { 1269 | return array( 1270 | 'markup' => $matches[0], 1271 | 'extent' => strlen($matches[0]), 1272 | ); 1273 | } 1274 | } 1275 | 1276 | protected function inlineSpecialCharacter($Excerpt) 1277 | { 1278 | if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) 1279 | { 1280 | return array( 1281 | 'markup' => '&', 1282 | 'extent' => 1, 1283 | ); 1284 | } 1285 | 1286 | $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); 1287 | 1288 | if (isset($SpecialCharacter[$Excerpt['text'][0]])) 1289 | { 1290 | return array( 1291 | 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', 1292 | 'extent' => 1, 1293 | ); 1294 | } 1295 | } 1296 | 1297 | protected function inlineStrikethrough($Excerpt) 1298 | { 1299 | if ( ! isset($Excerpt['text'][1])) 1300 | { 1301 | return; 1302 | } 1303 | 1304 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1305 | { 1306 | return array( 1307 | 'extent' => strlen($matches[0]), 1308 | 'element' => array( 1309 | 'name' => 'del', 1310 | 'text' => $matches[1], 1311 | 'handler' => 'line', 1312 | ), 1313 | ); 1314 | } 1315 | } 1316 | 1317 | protected function inlineUrl($Excerpt) 1318 | { 1319 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1320 | { 1321 | return; 1322 | } 1323 | 1324 | if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) 1325 | { 1326 | $Inline = array( 1327 | 'extent' => strlen($matches[0][0]), 1328 | 'position' => $matches[0][1], 1329 | 'element' => array( 1330 | 'name' => 'a', 1331 | 'text' => $matches[0][0], 1332 | 'attributes' => array( 1333 | 'href' => $matches[0][0], 1334 | ), 1335 | ), 1336 | ); 1337 | 1338 | return $Inline; 1339 | } 1340 | } 1341 | 1342 | protected function inlineUrlTag($Excerpt) 1343 | { 1344 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) 1345 | { 1346 | $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); 1347 | 1348 | return array( 1349 | 'extent' => strlen($matches[0]), 1350 | 'element' => array( 1351 | 'name' => 'a', 1352 | 'text' => $url, 1353 | 'attributes' => array( 1354 | 'href' => $url, 1355 | ), 1356 | ), 1357 | ); 1358 | } 1359 | } 1360 | 1361 | # ~ 1362 | 1363 | protected function unmarkedText($text) 1364 | { 1365 | if ($this->breaksEnabled) 1366 | { 1367 | $text = preg_replace('/[ ]*\n/', "
\n", $text); 1368 | } 1369 | else 1370 | { 1371 | $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); 1372 | $text = str_replace(" \n", "\n", $text); 1373 | } 1374 | 1375 | return $text; 1376 | } 1377 | 1378 | # 1379 | # Handlers 1380 | # 1381 | 1382 | protected function element(array $Element) 1383 | { 1384 | $markup = '<'.$Element['name']; 1385 | 1386 | if (isset($Element['attributes'])) 1387 | { 1388 | foreach ($Element['attributes'] as $name => $value) 1389 | { 1390 | if ($value === null) 1391 | { 1392 | continue; 1393 | } 1394 | 1395 | $markup .= ' '.$name.'="'.$value.'"'; 1396 | } 1397 | } 1398 | 1399 | if (isset($Element['text'])) 1400 | { 1401 | $markup .= '>'; 1402 | 1403 | if (isset($Element['handler'])) 1404 | { 1405 | $markup .= $this->{$Element['handler']}($Element['text']); 1406 | } 1407 | else 1408 | { 1409 | $markup .= $Element['text']; 1410 | } 1411 | 1412 | $markup .= ''; 1413 | } 1414 | else 1415 | { 1416 | $markup .= ' />'; 1417 | } 1418 | 1419 | return $markup; 1420 | } 1421 | 1422 | protected function elements(array $Elements) 1423 | { 1424 | $markup = ''; 1425 | 1426 | foreach ($Elements as $Element) 1427 | { 1428 | $markup .= "\n" . $this->element($Element); 1429 | } 1430 | 1431 | $markup .= "\n"; 1432 | 1433 | return $markup; 1434 | } 1435 | 1436 | # ~ 1437 | 1438 | protected function li($lines) 1439 | { 1440 | $markup = $this->lines($lines); 1441 | 1442 | $trimmedMarkup = trim($markup); 1443 | 1444 | if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') 1445 | { 1446 | $markup = $trimmedMarkup; 1447 | $markup = substr($markup, 3); 1448 | 1449 | $position = strpos($markup, "

"); 1450 | 1451 | $markup = substr_replace($markup, '', $position, 4); 1452 | } 1453 | 1454 | return $markup; 1455 | } 1456 | 1457 | # 1458 | # Deprecated Methods 1459 | # 1460 | 1461 | function parse($text) 1462 | { 1463 | $markup = $this->text($text); 1464 | 1465 | return $markup; 1466 | } 1467 | 1468 | # 1469 | # Static Methods 1470 | # 1471 | 1472 | static function instance($name = 'default') 1473 | { 1474 | if (isset(self::$instances[$name])) 1475 | { 1476 | return self::$instances[$name]; 1477 | } 1478 | 1479 | $instance = new self(); 1480 | 1481 | self::$instances[$name] = $instance; 1482 | 1483 | return $instance; 1484 | } 1485 | 1486 | private static $instances = array(); 1487 | 1488 | # 1489 | # Fields 1490 | # 1491 | 1492 | protected $DefinitionData; 1493 | 1494 | # 1495 | # Read-Only 1496 | 1497 | protected $specialCharacters = array( 1498 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', 1499 | ); 1500 | 1501 | protected $StrongRegex = array( 1502 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', 1503 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', 1504 | ); 1505 | 1506 | protected $EmRegex = array( 1507 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1508 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1509 | ); 1510 | 1511 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; 1512 | 1513 | protected $voidElements = array( 1514 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1515 | ); 1516 | 1517 | protected $textLevelElements = array( 1518 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1519 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1520 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1521 | 'q', 'rt', 'ins', 'font', 'strong', 1522 | 's', 'tt', 'sub', 'mark', 1523 | 'u', 'xm', 'sup', 'nobr', 1524 | 'var', 'ruby', 1525 | 'wbr', 'span', 1526 | 'time', 1527 | ); 1528 | } 1529 | -------------------------------------------------------------------------------- /php/index.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | scroll-scope.js 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 |
59 | 60 |
61 | 62 | 66 |
67 |
'; 68 | $Parsedown = new Parsedown(); 69 | echo $Parsedown->text(str_replace($buttonsBefore, $buttons."\n".$buttonsBefore, file_get_contents('README.md'))); 70 | ?> 71 | 72 |
73 | 74 | 75 | 76 |

Demos

77 | 78 |
79 | 80 |
81 |

Default behavior

82 |
83 |
84 |

data-scroll-scope

85 |
86 |
87 | 88 |
89 | 90 |
91 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

92 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

93 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

94 |
95 | 96 |
97 | 98 |
99 | 100 |
101 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

102 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

103 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis.

104 |
105 | 106 |
107 |
108 |
109 | 110 |

When scrolling the container on the right, scrolling should stop when you get to the end and your document should stay still. For some browsers, this is also the default behavior.

111 | 112 |

Also keep in mind that especially mobile browsers allow the user to keep scrolling the parent if the child is at its respective boundary when initiating the scroll. If you want to block scrolling even in these cases, use force (see modal dialog example below).

113 | 114 | 115 | 116 |

Real-life use case: dropdown

117 | 118 |

The document will stay still even when you scroll the results container to the end.

119 | 120 |
121 |
122 | 123 |

124 | 125 | 132 | 133 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

134 | 135 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

136 | 137 |
138 | 139 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

140 | 141 |
142 | 143 |
144 |
145 | 146 | 147 | 148 |

Real-life use case: code block

149 | 150 |

151 | 152 |
/*A page with lots of code blocks could set a max-height for them*/
153 | pre {
154 | 	overflow: auto;
155 | 	max-height: 16em;
156 | }
157 | 
158 | /*Lots of code*/
159 | code {
160 | 	display: inline-block;
161 | 	color: #555;
162 | 	background-color: #f6f6f6;
163 | 	border-radius: 3px;
164 | 	padding-left: 0.3em;
165 | 	padding-right: 0.3em;
166 | }
167 | pre code {
168 | 	display: block;
169 | 	padding: 1em;
170 | 	overflow: auto;
171 | }
172 | 173 | 174 | 175 |

Real-life use case: modal dialog

176 | 177 | 203 | 204 |

Modal dialog implementations tend to scroll the document. Even Bootstrap's modal dialog overlay lets the scroll event through on mobile Safari!

205 | 206 |

Open the demo dialog

207 | 208 |

In this quite trivial custom dialog implementation, we scope the scrolling in both the overall container and the content area so the document maintains its position. We also want to use force to disable parent scrolling even when the areas do not overflow.

209 | 210 |

Note! When scroll events are blocked with force, mobile Safari also blocks click events for that element. To close the modal on overlay click, we must attach the click event handler to an element that does not use data-scroll-scope.

211 | 212 |

The source looks like this:

213 | 214 |
<div class="modal" data-scroll-scope="force">
215 | 	<div class="modal-content" data-scroll-scope="force">
216 | 		...
217 | 	</div>
218 | 	<div class="modal-overlay" data-action="toggle-modal"></div>
219 | </div>
220 | 221 |
// Quick custom toggle
222 | $(document).on('click', '[data-action="toggle-modal"]', function (event) {
223 | 	event.preventDefault();
224 | 	$('.modal').toggleClass('closed');
225 | });
226 | 227 |
.modal {
228 | 	position: fixed;
229 | 	z-index: 100;
230 | 	width: 100%;
231 | 	height: 100%;
232 | 	left: 0;
233 | 	top: 0;
234 | 	overflow: hidden;
235 | }
236 | 	.modal.closed {
237 | 		display: none;
238 | 	}
239 | .modal-overlay {
240 | 	position: fixed;
241 | 	z-index: 1;
242 | 	width: 100%;
243 | 	height: 100%;
244 | 	top: 0;
245 | 	left: 0;
246 | 	background-color: rgba(0, 0, 0, 0.5);
247 | }
248 | .modal-content {
249 | 	position: fixed;
250 | 	z-index: 2;
251 | 	overflow: auto;
252 | 	top: 5%;
253 | 	left: 5%;
254 | 	width: 90%;
255 | 	height: 30em;
256 | 	max-height: 90%;
257 | 	background-color: #fff;
258 | }
259 | 260 | 261 | 262 |

Nested containers

263 | 264 |
265 |
266 |
267 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

268 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

269 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

270 |
271 |
272 |
273 | 274 | 275 | 276 |

Impractical number of nested containers

277 | 278 |
279 |
280 |
281 |
282 |
283 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

284 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

285 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

286 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia aliquam, nemo molestiae consequatur officiis magni eos aliquid incidunt perspiciatis. Laudantium dolorum reprehenderit corporis dignissimos eaque, possimus quam, sequi ab soluta.

287 |
288 |
289 |
290 |
291 |
292 | 293 | 294 | 295 | 308 | 309 | 310 | 311 |
312 |
313 |
314 | 315 | 316 | 317 | 318 | 319 | 354 | 355 | 356 | -------------------------------------------------------------------------------- /scroll-scope.js: -------------------------------------------------------------------------------- 1 | /*! scroll-scope.js 0.1.0, MIT 2 | https://github.com/Eiskis/scroll-scope 3 | */ 4 | if (jQuery) { 5 | ;(function ($) { 6 | 'use strict'; 7 | 8 | 9 | 10 | // Main object 11 | var ScrollScope = function (options) { 12 | var self = this; 13 | 14 | 15 | 16 | // We bind the on handler to this element, but 17 | // it's not the one that is being scrolled 18 | self.mainContainer = null; 19 | 20 | // This will be evaluated later 21 | self.selector = null; 22 | 23 | // Some options 24 | self.settings = $.extend({ 25 | elements: '[data-scroll-scope]', 26 | forcedElements: '[data-scroll-scope="force"]', 27 | events: 'DOMMouseScroll mousewheel scroll touchstart touchmove' 28 | 29 | // Only extend if one of the keys are included 30 | }, ( 31 | options && 32 | ( 33 | options.elements || 34 | options.forcedElements || 35 | options.events 36 | ) 37 | ? options 38 | : {} 39 | ) 40 | ); 41 | 42 | 43 | 44 | // Magic 45 | 46 | // Fetch targeted elements on the page 47 | self.getTargetedElements = function () { 48 | return self.mainContainer.find(self.selector); 49 | }; 50 | 51 | // Get working selector that targets all items 52 | self.getSelector = function (selectors) { 53 | var legits = []; 54 | for (var i = 0; i < selectors.length; i++) { 55 | if (selectors[i] && selectors[i].length) { 56 | legits.push(selectors[i]); 57 | } 58 | } 59 | return legits.length ? legits.join(', ') : null; 60 | }; 61 | 62 | // Turns selector into jQuery object if needed 63 | self.normalizeJqueryObject = function (container) { 64 | if (container) { 65 | return (container instanceof jQuery) ? container : $(container); 66 | } 67 | return null; 68 | }; 69 | 70 | // Cancel an event for good 71 | self.killScrolling = function (event, force) { 72 | 73 | // Preventing touchmove disables click events on mobile Safari, so we require user to force 74 | if ( 75 | force || 76 | ( 77 | event.type !== 'touchmove' && 78 | event.type !== 'touchstart' 79 | ) 80 | ) { 81 | event.preventDefault(); 82 | event.stopPropagation(); 83 | event.returnValue = false; 84 | return false; 85 | } 86 | 87 | }; 88 | 89 | 90 | 91 | // Prevents parent element from scrolling when a child element is scrolled to its boundaries 92 | self.onScroll = function (event) { 93 | 94 | // Event has been evaluated on lower level and deemed legit 95 | if (event.isLegitScroll) { 96 | return true; 97 | } 98 | 99 | // Start handling 100 | var element = $(this); 101 | var force = (self.settings.forcedElements && element.is(self.settings.forcedElements)); 102 | var scrollHeight = this.scrollHeight; 103 | var apparentHeight = element.outerHeight(); 104 | var scrollingLeft = scrollHeight - apparentHeight - this.scrollTop; 105 | 106 | // Let targeted elements scroll parent when they're not scrollable at all 107 | if (scrollHeight <= apparentHeight) { 108 | 109 | // Unless we're using force 110 | if (force && event.type !== 'touchstart') { 111 | return self.killScrolling(event, force); 112 | } 113 | 114 | return true; 115 | } 116 | 117 | // Normalize fetching delta 118 | var delta = event.originalEvent.wheelDelta; 119 | 120 | // Mobile doesn't let us kill scrolling in some situations, but 121 | // if we cheat for just 1px the native scoping works with bounce 122 | if ( 123 | force && 124 | typeof delta === 'undefined' && 125 | (event.type === 'touchstart') 126 | ) { 127 | 128 | // When we're on top, move down one pixel 129 | if (this.scrollTop <= 0) { 130 | element.scrollTop(1); 131 | 132 | // When we're at the bottom, move up one pixel 133 | } else if (scrollingLeft <= 0) { 134 | element.scrollTop(scrollHeight - apparentHeight - 1); 135 | } 136 | } 137 | 138 | // Firefox doesn't return wheel delta, but we don't need it since Firefox works without our hacks 139 | // if (typeof delta === 'undefined') { 140 | // delta = event.originalEvent.detail; 141 | // } 142 | 143 | // Intervene only if we know we're actually moving 144 | var goingUp = delta > 0; 145 | 146 | // Scrolling down, but this will take us past the bottom 147 | if (!goingUp && -delta > scrollingLeft) { 148 | element.scrollTop(this.scrollHeight); 149 | return self.killScrolling(event, force); 150 | 151 | // Scrolling up, but this will take us past the top 152 | } else if (goingUp && delta > this.scrollTop) { 153 | element.scrollTop(0); 154 | return self.killScrolling(event, force); 155 | } 156 | 157 | // Nothing intervened, I guess we're good 158 | event.isLegitScroll = true; 159 | return true; 160 | }; 161 | 162 | 163 | 164 | // Remove listener from parent 165 | self.unbind = function () { 166 | 167 | // Detach event handler 168 | if (self.mainContainer) { 169 | self.mainContainer.off(self.settings.events, self.selector, self.onScroll); 170 | } 171 | 172 | return self; 173 | }; 174 | 175 | // Bind listener to parent 176 | self.bind = function (container) { 177 | container = self.normalizeJqueryObject(container); 178 | 179 | // Existing container, let's clean up 180 | if (container) { 181 | self.unbind(); 182 | } 183 | 184 | // Store container 185 | self.mainContainer = container; 186 | 187 | // Attach event handler 188 | self.mainContainer.on(self.settings.events, self.selector, self.onScroll); 189 | 190 | return self; 191 | }; 192 | 193 | 194 | 195 | // Normalize selectors 196 | self.settings.elements = self.getSelector([self.settings.elements]); 197 | self.settings.forcedElements = self.getSelector([self.settings.forcedElements]); 198 | self.selector = self.getSelector([self.settings.elements, self.settings.forcedElements]); 199 | 200 | }; 201 | 202 | 203 | // AMD: Register as anonymous module 204 | if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 205 | define(function() { 206 | return ScrollScope; 207 | }); 208 | 209 | // Generic export 210 | } else if (typeof module !== 'undefined' && module.exports) { 211 | // module.exports = ScrollScope.bind; 212 | // module.exports.ScrollScope = ScrollScope; 213 | module.exports = ScrollScope; 214 | 215 | } else { 216 | 217 | // Export to window for advanced usage 218 | window.ScrollScope = ScrollScope; 219 | 220 | // jQuery plugin 221 | (function ($) { 222 | $.fn.scrollScope = function (options) { 223 | 224 | // Create and bind new instance 225 | var obj = new ScrollScope(options); 226 | obj.bind(this); 227 | 228 | // Allow jQuery chaining 229 | return obj.mainContainer; 230 | 231 | }; 232 | }($)); 233 | } 234 | }(jQuery)); 235 | } 236 | -------------------------------------------------------------------------------- /scroll-scope.min.js: -------------------------------------------------------------------------------- 1 | /*! scroll-scope.js 0.1.0, MIT 2 | https://github.com/Eiskis/scroll-scope 3 | */ 4 | jQuery&&!function(e){"use strict";var t=function(t){var n=this;n.mainContainer=null,n.selector=null,n.settings=e.extend({elements:"[data-scroll-scope]",forcedElements:'[data-scroll-scope="force"]',events:"DOMMouseScroll mousewheel scroll touchstart touchmove"},t&&(t.elements||t.forcedElements||t.events)?t:{}),n.getTargetedElements=function(){return n.mainContainer.find(n.selector)},n.getSelector=function(e){for(var t=[],n=0;n=r)return l&&"touchstart"!==t.type?n.killScrolling(t,l):!0;var c=t.originalEvent.wheelDelta;l&&"undefined"==typeof c&&"touchstart"===t.type&&(this.scrollTop<=0?o.scrollTop(1):0>=s&&o.scrollTop(r-i-1));var u=c>0;return!u&&-c>s?(o.scrollTop(this.scrollHeight),n.killScrolling(t,l)):u&&c>this.scrollTop?(o.scrollTop(0),n.killScrolling(t,l)):(t.isLegitScroll=!0,!0)},n.unbind=function(){return n.mainContainer&&n.mainContainer.off(n.settings.events,n.selector,n.onScroll),n},n.bind=function(e){return e=n.normalizeJqueryObject(e),e&&n.unbind(),n.mainContainer=e,n.mainContainer.on(n.settings.events,n.selector,n.onScroll),n},n.settings.elements=n.getSelector([n.settings.elements]),n.settings.forcedElements=n.getSelector([n.settings.forcedElements]),n.selector=n.getSelector([n.settings.elements,n.settings.forcedElements])};"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){return t}):"undefined"!=typeof module&&module.exports?module.exports=t:(window.ScrollScope=t,function(e){e.fn.scrollScope=function(e){var n=new t(e);return n.bind(this),n.mainContainer}}(e))}(jQuery); -------------------------------------------------------------------------------- /splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/scroll-scope/8fbe1e38dcca035e361717902adeac523a60f9d8/splash.png --------------------------------------------------------------------------------