├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── component.json ├── css ├── .DS_Store ├── animations.less ├── base.less ├── responsive.less ├── styles.css └── themes │ └── default.less ├── demo.html ├── gulpfile.js ├── js ├── comment.js ├── helpers │ └── mobile-check.js ├── main.js ├── section.js └── vendor │ └── lodash-custom.js ├── package.json ├── release ├── side-comments.css ├── side-comments.js ├── side-comments.min.css ├── side-comments.min.js └── themes │ ├── default-theme.css │ └── default-theme.min.css ├── support ├── css │ └── basics.css ├── images │ ├── cattelyn_stark.png │ ├── clay_davis.png │ ├── donald_draper.png │ ├── jon_snow.png │ └── user.png ├── js │ └── jquery.js └── test_data.js ├── templates ├── comment.html ├── form.html └── section.html ├── test ├── index.html ├── test_main.js └── vendor │ ├── chai.js │ ├── lodash.js │ ├── mocha.css │ └── mocha.js └── themes └── default.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | components 4 | .DS_STORE 5 | 6 | /.DS_Store 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Eric Anderson 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SideComments.js 2 | ###### Current Version 0.0.3 3 | 4 | SideComments.js is a UI component to give you [Medium.com](http://medium.com/) style comment management on the front-end. It allows users to comment directly on sections of content rather than the boring comment stream on the bottom of the page that we're so used to. 5 | 6 | **Note:** This component only handles the display / user interface of how your comments are presented. It does _not_ provide any utilities to help manage storing or retreiving your comment data from your server, how you do that is entirely up to you. Check out the integrations section for resources related to back-end integration. 7 | 8 | ## Demo 9 | 10 | Check out a sweet demo of SideComments here: [https://aroc.github.io/side-comments-demo](https://aroc.github.io/side-comments-demo) 11 | 12 | ## Get Started 13 | **How to start using SideComments.js on your website immediately.** 14 | 15 | ### 1. Download SideComments.js 16 | 17 | Download SideComments immediately: 18 | [](https://github.com/aroc/side-comments/archive/master.zip) 19 | 20 | Install with [Component](https://github.com/component/component): 21 | `component install aroc/side-comments` 22 | 23 | or include side-comments in your `component.json` file's `dependencies: {}` object. 24 | 25 | ### 2. Include SideComments.js in your project. 26 | 27 | **Note: jQuery is required** 28 | You must include jQuery in your project in order for SideComments.js to work. This component uses jQuery to manage DOM manipulation and will not work without it. 29 | 30 | You'll need to include the following single JavaScript file and two CSS files to get SideComments.js working. 31 | - `release/side-comments.js` 32 | - `release/side-comments.css` 33 | - `release/themes/default-theme.css` 34 | 35 | You can choose **not** to include `default-theme.css`, but you'll need to style SideComments youself if you choose not to include it, as `side-comments.css` handles only the basic layout styling and not making it all pretty and looking like Medium.com. 36 | 37 | ### 3. Set up your HTML. 38 | 39 | You need to have a wrapper element to point SideComments at and two things on each commentable section; the class `commentable-section` and the data attribute `data-section-id`, which holds the unique ID of that commentable-section for this page. 40 | 41 | ``` 42 |
43 |

44 | This is a section that can be commented on. 45 |

46 |

47 | This is a another section that can be commented on. 48 |

49 |

50 | This is yet another section that can be commented on. 51 |

52 |
53 | ``` 54 | 55 | ### 4. Initialize a new SideComments object. 56 | 57 | ``` 58 | // First require it. 59 | var SideComments = require('side-comments'); 60 | 61 | // Then, create a new SideComments instance, passing in the wrapper element and the optional the current user and any existing comments. 62 | sideComments = new SideComments('#commentable-area', currentUser, existingComments); 63 | ``` 64 | 65 | The current user is an object and is expected to be in the following format: 66 | 67 | ``` 68 | { 69 | id: 1, 70 | avatarUrl: "http://f.cl.ly/items/0s1a0q1y2Z2k2I193k1y/default-user.png", 71 | name: "You" 72 | } 73 | ``` 74 | 75 | The existing comments argument is expected to be an array of sections with a nested array of comments. It needs to look like the following: 76 | 77 | ``` 78 | [ 79 | { 80 | "sectionId": "1", 81 | "comments": [ 82 | { 83 | "authorAvatarUrl": "http://f.cl.ly/items/1W303Y360b260u3v1P0T/jon_snow_small.png", 84 | "authorName": "Jon Sno", 85 | "comment": "I'm Ned Stark's bastard. Related: I know nothing." 86 | }, 87 | { 88 | "authorAvatarUrl": "http://f.cl.ly/items/2o1a3d2f051L0V0q1p19/donald_draper.png", 89 | "authorName": "Donald Draper", 90 | "comment": "I need a scotch." 91 | } 92 | ] 93 | }, 94 | { 95 | "sectionId": "3", 96 | "comments": [ 97 | { 98 | "authorAvatarUrl": "http://f.cl.ly/items/0l1j230k080S0N1P0M3e/clay-davis.png", 99 | "authorName": "Senator Clay Davis", 100 | "comment": "These Side Comments are incredible. Sssshhhiiiiieeeee." 101 | } 102 | ] 103 | } 104 | ]; 105 | ``` 106 | 107 | ### 5. Listen to post and delete events. 108 | 109 | Finally, in order to know when a comment has been posted or deleted, just bind to your SideComments' object events and then do whatever you want with them, (likely save and delete from your database). 110 | 111 | ``` 112 | // Listen to "commentPosted", and send a request to your backend to save the comment. 113 | // More about this event in the "docs" section. 114 | sideComments.on('commentPosted', function( comment ) { 115 | $.ajax({ 116 | url: '/comments', 117 | type: 'POST', 118 | data: comment, 119 | success: function( savedComment ) { 120 | // Once the comment is saved, you can insert the comment into the comment stream with "insertComment(comment)". 121 | sideComments.insertComment(comment); 122 | } 123 | }); 124 | }); 125 | 126 | // Listen to "commentDeleted" and send a request to your backend to delete the comment. 127 | // More about this event in the "docs" section. 128 | sideComments.on('commentDeleted', function( commentId ) { 129 | $.ajax({ 130 | url: '/comments/' + commentId, 131 | type: 'DELETE', 132 | success: function( success ) { 133 | // Do something. 134 | } 135 | }); 136 | }); 137 | ``` 138 | 139 | ## Docs [![Inline docs](http://inch-ci.org/github/aroc/side-comments.svg?branch=master)](http://inch-ci.org/github/aroc/side-comments) 140 | 141 | **Overview of all events and method you can leverage in SideComments.js** 142 | 143 | ### SideComments Constructor 144 | The constructor takes one required and two optional arguments: 145 | 146 | - `$el` (String): The element which contains all the `.commentable-section` elements. 147 | 148 | - `currentUser` (Object): The user representation new comments will be posted under. As it's optional, you can just pass `null` if there is no current user at the time and set one at a later time with the `setCurrentUser` method, which is documented below. The current user object needs to look like this: [https://gist.github.com/aroc/02a0f8badf219da12667](https://gist.github.com/aroc/02a0f8badf219da12667) 149 | 150 | - `existingComments` (Array): An array of existing comments that you want inserted at initialization time. You can also insert comments yourself at later time with the `insertComment` method, outlined below. The structure of the objects in this array needs to look like this: [https://gist.github.com/aroc/54a2669783231a0d2215](https://gist.github.com/aroc/54a2669783231a0d2215) 151 | 152 | ### Methods 153 | 154 | #### deselectSection(sectionId) 155 | 156 | De-select a section and make it inactive, hiding the side comments. If the side comments are already hidden, this method will have no effect. 157 | 158 | ``` 159 | sideComments.deselectSection(12); 160 | ``` 161 | 162 | #### setCurrentUser(currentUser) 163 | 164 | Sets the currentUser to be used for all new comments. 165 | 166 | ``` 167 | var currentUser = { 168 | "id": 1, 169 | "avatarUrl": "users/avatars/user1.png", 170 | "name": "Jim Jones" 171 | }; 172 | sideComments.setCurrentUser(currentUser); 173 | ``` 174 | 175 | #### removeCurrentUser() 176 | 177 | Removes the currentUser. Without a currentUser, comments amy not be posted. Instead, the `addCommentAttempted` event gets triggered when a user clicks the "Add Comment" button. 178 | 179 | #### insertComment(comment) 180 | 181 | Inserts a comment into the markup. It will insert into the section specified by the comment object. 182 | 183 | ``` 184 | var comment = { 185 | sectionId: 12, 186 | comment: "Hey there!", 187 | authorAvatarUrl: "users/avatars/test1.png", 188 | authorName: "Jim Jones", 189 | authorId: 16 190 | }; 191 | sideComments.insertComment(comment); 192 | ``` 193 | 194 | #### removeComment(sectionId, commentId) 195 | 196 | Removes a comment from the SideComments object and from the markup. 197 | 198 | #### commentsAreVisible() 199 | 200 | Returns true if the comments are visbile, false if they are not. 201 | 202 | #### destroy() 203 | 204 | Removing the sideComments object, cleaning up any event bindings and removing any markup from the DOM. 205 | 206 | 207 | ### Events 208 | 209 | #### commentPosted 210 | Values passed: `comment (Object)` 211 | Fired after a user fills out the comment form and clicks "Post". 212 | 213 | #### addCommentAttempted 214 | Values passed: None. 215 | Fired when a sideComments object doesn't have a current user and the "Add Comment" button is clicked. 216 | 217 | #### commentDeleted 218 | Values passed: `comment (Object)` 219 | Fired after a user has clicked "Delete" on one of their comments and has confirmed with the dialog that they do want to delete it. 220 | 221 | 222 | ## Integrating with your back-end 223 | 224 | SideComments has no opinion on how you should integrate with your back-end or what technology stack you should use on your back-end. However, here are some resources that may help you depending on your platform: 225 | 226 | ### WordPress 227 | 228 | - WP-Side-Comments by [@richardtape](https://github.com/richardtape): A WP plugin that wraps this project in a plugin (currently very early stage) [https://github.com/richardtape/wp-side-comments](https://github.com/richardtape/wp-side-comments) 229 | 230 | - WPSideComments by [@strategio](https://github.com/strategio): Another WP plugin that wraps this project (early stage) [http://wordpress.org/plugins/wp-side-comments/](http://wordpress.org/plugins/wp-side-comments/) 231 | 232 | 233 | - [@dcondrey](https://github.com/dcondrey) has an exmaple of how you might be able to enqueue the SideComments scripts for WordPress: [https://github.com/aroc/side-comments/pull/14](https://github.com/aroc/side-comments/pull/14) 234 | 235 | 236 | ### Hull [hull.io](http://hull.io/) 237 | 238 | A component to integrate SideComments with Hull, which gives you a full back-end to power your comments. 239 | [http://hull-components.github.io/side-comments/](http://hull-components.github.io/side-comments/) 240 | 241 | 242 | ## License 243 | 244 | The MIT License (MIT) 245 | 246 | Copyright (c) 2014 Eric Anderson 247 | 248 | Permission is hereby granted, free of charge, to any person obtaining a copy 249 | of this software and associated documentation files (the "Software"), to deal 250 | in the Software without restriction, including without limitation the rights 251 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 252 | copies of the Software, and to permit persons to whom the Software is 253 | furnished to do so, subject to the following conditions: 254 | 255 | The above copyright notice and this permission notice shall be included in 256 | all copies or substantial portions of the Software. 257 | 258 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 259 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 260 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 261 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 262 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 263 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 264 | THE SOFTWARE. 265 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "side-comments", 3 | "homepage": "https://github.com/aroc/side-comments", 4 | "authors": [ 5 | "Eric Anderson " 6 | ], 7 | "description": "An interface component to give your site/app Medium.com style commenting.", 8 | "main": "release/side-comments.js", 9 | "keywords": [ 10 | "commenting", 11 | "medium", 12 | "medium.com", 13 | "side", 14 | "side-comments", 15 | "discussion" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "side-comments", 3 | "repo": "aroc/side-comments", 4 | "dependencies": { 5 | "component/emitter": "*" 6 | }, 7 | "scripts": [ 8 | "js/main.js", 9 | "js/section.js", 10 | "js/vendor/lodash-custom.js", 11 | "js/helpers/mobile-check.js" 12 | ], 13 | "main": "js/main.js", 14 | "templates": [ 15 | "templates/section.html", 16 | "templates/form.html", 17 | "templates/comment.html" 18 | ] 19 | } -------------------------------------------------------------------------------- /css/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aroc/side-comments/a321da9d5fb9df2e65f321f3daeda57081899c91/css/.DS_Store -------------------------------------------------------------------------------- /css/animations.less: -------------------------------------------------------------------------------- 1 | @keyframes fadein { 2 | from { opacity: 0; } 3 | to { opacity: 1; } 4 | } -------------------------------------------------------------------------------- /css/base.less: -------------------------------------------------------------------------------- 1 | @import "animations"; 2 | 3 | .commentable-section { 4 | position: relative; 5 | 6 | &:hover .side-comment .marker { 7 | display: block; 8 | } 9 | } 10 | 11 | .side-comment { 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | position: absolute; 17 | top: 0; 18 | right: 0; 19 | width: 20px; 20 | min-height: 100%; 21 | height: 100%; 22 | 23 | .hide { 24 | display: none; 25 | } 26 | 27 | .marker { 28 | display: none; 29 | position: absolute; 30 | top: 0; 31 | right: 0; 32 | cursor: pointer; 33 | } 34 | 35 | .marker span { 36 | display: none; 37 | } 38 | 39 | &.active .marker, &.has-comments .marker, &.has-comments ul.comments { 40 | display: block; 41 | } 42 | 43 | .add-comment { 44 | display: none; 45 | } 46 | 47 | &.has-comments .add-comment, &.no-current-user .add-comment { 48 | display: block; 49 | } 50 | 51 | &.no-current-user .add-comment { 52 | margin-top: 20px; 53 | } 54 | 55 | &.has-comments { 56 | 57 | .marker:before { 58 | content: ""; 59 | } 60 | 61 | .marker span { 62 | display: block; 63 | } 64 | 65 | .add-comment.hide { 66 | display: none; 67 | } 68 | 69 | .comment-form, .reply-form { 70 | display: none; 71 | } 72 | 73 | } 74 | 75 | .comments-wrapper { 76 | display: none; 77 | position: absolute; 78 | top: 0; 79 | left: 40px; 80 | } 81 | 82 | .comments { 83 | list-style: none; 84 | padding: 0; 85 | margin: 0; 86 | display: none; 87 | width: 100%; 88 | 89 | li { 90 | width: 100%; 91 | overflow: hidden; 92 | } 93 | } 94 | 95 | .comment, .comment-box, .actions { 96 | margin: 0; 97 | } 98 | 99 | .actions, .delete { 100 | margin-left: 42px; 101 | } 102 | 103 | .add-comment.active { 104 | display: block; 105 | } 106 | 107 | .comment-form, .reply-form { 108 | overflow: hidden; 109 | 110 | &.active { 111 | display: block; 112 | } 113 | } 114 | 115 | &.active .comments-wrapper { 116 | display: block; 117 | } 118 | 119 | } 120 | 121 | @import "responsive"; -------------------------------------------------------------------------------- /css/responsive.less: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px) { 2 | body { 3 | -webkit-overflow-scrolling: touch; 4 | overflow-x: hidden; 5 | } 6 | } -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes fadein { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | @keyframes fadein { 10 | from { 11 | opacity: 0; 12 | } 13 | to { 14 | opacity: 1; 15 | } 16 | } 17 | .commentable-section { 18 | position: relative; 19 | } 20 | .commentable-section:hover .side-comment .marker { 21 | display: block; 22 | } 23 | .side-comment { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | width: 20px; 28 | min-height: 100%; 29 | height: 100%; 30 | } 31 | .side-comment * { 32 | -moz-box-sizing: border-box; 33 | box-sizing: border-box; 34 | } 35 | .side-comment .hide { 36 | display: none; 37 | } 38 | .side-comment .marker { 39 | display: none; 40 | position: absolute; 41 | top: 0; 42 | right: 0; 43 | cursor: pointer; 44 | } 45 | .side-comment .marker span { 46 | display: none; 47 | } 48 | .side-comment.active .marker, 49 | .side-comment.has-comments .marker, 50 | .side-comment.has-comments ul.comments { 51 | display: block; 52 | } 53 | .side-comment .add-comment { 54 | display: none; 55 | } 56 | .side-comment.has-comments .add-comment, 57 | .side-comment.no-current-user .add-comment { 58 | display: block; 59 | } 60 | .side-comment.no-current-user .add-comment { 61 | margin-top: 20px; 62 | } 63 | .side-comment.has-comments .marker:before { 64 | content: ""; 65 | } 66 | .side-comment.has-comments .marker span { 67 | display: block; 68 | } 69 | .side-comment.has-comments .add-comment.hide { 70 | display: none; 71 | } 72 | .side-comment.has-comments .comment-form, 73 | .side-comment.has-comments .reply-form { 74 | display: none; 75 | } 76 | .side-comment .comments-wrapper { 77 | display: none; 78 | position: absolute; 79 | top: 0; 80 | left: 40px; 81 | } 82 | .side-comment .comments { 83 | list-style: none; 84 | padding: 0; 85 | margin: 0; 86 | display: none; 87 | width: 100%; 88 | } 89 | .side-comment .comments li { 90 | width: 100%; 91 | overflow: hidden; 92 | } 93 | .side-comment .comment, 94 | .side-comment .comment-box, 95 | .side-comment .actions { 96 | margin: 0; 97 | } 98 | .side-comment .actions, 99 | .side-comment .delete { 100 | margin-left: 42px; 101 | } 102 | .side-comment .add-comment.active { 103 | display: block; 104 | } 105 | .side-comment .comment-form, 106 | .side-comment .reply-form { 107 | overflow: hidden; 108 | } 109 | .side-comment .comment-form.active, 110 | .side-comment .reply-form.active { 111 | display: block; 112 | } 113 | .side-comment.active .comments-wrapper { 114 | display: block; 115 | } 116 | @media (max-width: 768px) { 117 | body { 118 | -webkit-overflow-scrolling: touch; 119 | overflow-x: hidden; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /css/themes/default.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | @comments-wrapper-width: 280px; 3 | @avatar-width: 32px; 4 | @marker-width: 20px; 5 | 6 | .commentable-container { 7 | transition: all 0.22s ease; 8 | } 9 | 10 | .side-comments-open { 11 | transform: translate(-(@comments-wrapper-width + @marker-width), 0); 12 | } 13 | 14 | .commentable-section { 15 | box-sizing: border-box; 16 | padding-right: 30px; 17 | } 18 | 19 | .side-comment { 20 | padding-bottom: 20px; 21 | 22 | .marker { 23 | width: @marker-width; 24 | height: 18px; 25 | background: #DEDEDC; 26 | border-radius: 2px; 27 | text-decoration: none; 28 | } 29 | 30 | .marker:before, .marker span { 31 | content: "+"; 32 | position: absolute; 33 | width: 20px; 34 | height: 18px; 35 | line-height: 16px; 36 | font-size: 14px; 37 | color: #FFF; 38 | text-align: center; 39 | } 40 | 41 | .marker span { 42 | line-height: 20px; 43 | font-size: 12px; 44 | } 45 | 46 | .marker:after { 47 | content: ""; 48 | display: block; 49 | position: absolute; 50 | bottom: -7px; 51 | left: 5px; 52 | width: 0; 53 | border-width: 7px 8px 0 0; 54 | border-style: solid; 55 | border-color: #DEDEDC transparent; 56 | } 57 | 58 | .marker:hover, &.active .marker { 59 | background: #4FAF62; 60 | } 61 | 62 | .marker:hover:after, &.active .marker:after { 63 | border-color: #4FAF62 transparent; 64 | } 65 | 66 | .comments-wrapper { 67 | top: -22px; 68 | width: @comments-wrapper-width; 69 | padding-bottom: 120px; 70 | } 71 | 72 | &.active .comments-wrapper { 73 | animation: fadein 0.2s; 74 | } 75 | 76 | &.has-comments .comments-wrapper { 77 | top: -22px; 78 | } 79 | 80 | ul.comments li, .comment-form { 81 | border: 1px solid #F2F2F0; 82 | border-left: 0; 83 | border-right: 0; 84 | padding: 15px 0; 85 | margin-top: -1px; 86 | } 87 | 88 | .reply-form { 89 | &:extend(.comment-form); 90 | padding-left: 42px; 91 | padding-top: 10px; 92 | } 93 | 94 | .comment, .comment-box { 95 | font-size: 14px; 96 | line-height: 18px; 97 | } 98 | 99 | .author-avatar { 100 | float: left; 101 | width: @avatar-width; 102 | height: @avatar-width; 103 | margin-right: 10px; 104 | 105 | img { 106 | width: 100%; 107 | height: 100%; 108 | } 109 | 110 | } 111 | 112 | .right-of-avatar { 113 | float: left; 114 | width: @comments-wrapper-width - (@avatar-width + 10); 115 | } 116 | 117 | .author-name { 118 | font-size: 15px; 119 | line-height: 16px; 120 | margin: 0 0 2px 0; 121 | font-weight: 700; 122 | text-decoration: none; 123 | color: #222; 124 | } 125 | 126 | a.author-name:hover { 127 | color: #444; 128 | } 129 | 130 | .action-link { 131 | color: #B3B3B1; 132 | font-size: 13px; 133 | text-decoration: none; 134 | 135 | &:hover { 136 | text-decoration: none; 137 | } 138 | } 139 | 140 | .action-link.post { 141 | .post { 142 | color: #89C794; 143 | 144 | &:hover { 145 | color: #468C54 146 | } 147 | } 148 | } 149 | 150 | .action-link.cancel, .action-link.delete { 151 | &:hover { 152 | color: #57AD68; 153 | } 154 | } 155 | 156 | .add-comment { 157 | color: #B3B3B1; 158 | font-size: 14px; 159 | line-height: 22px; 160 | font-weight: 300; 161 | padding: 0px 8px; 162 | letter-spacing: 0.05em; 163 | text-decoration: none; 164 | margin-top: 10px; 165 | 166 | &:before { 167 | content: "+"; 168 | border: 2px solid #DEDEDC; 169 | border-radius: 100px; 170 | width: 23px; 171 | height: 23px; 172 | color: #DEDEDC; 173 | display: block; 174 | text-align: center; 175 | font-size: 16px; 176 | font-weight: 400; 177 | line-height: 18px; 178 | float: left; 179 | margin-right: 15px; 180 | letter-spacing: 0; 181 | box-sizing: border-box; 182 | } 183 | 184 | &:hover { 185 | text-decoration: none; 186 | } 187 | 188 | &:hover { 189 | color: #4FAF62; 190 | 191 | &:before { 192 | border-color: #4FAF62; 193 | color: #4FAF62; 194 | } 195 | 196 | } 197 | 198 | } 199 | 200 | .comment-box { 201 | outline: none; 202 | border: 0; 203 | box-shadow: none; 204 | padding: 0; 205 | } 206 | 207 | .actions { 208 | margin-top: 5px; 209 | 210 | a { 211 | float: left; 212 | } 213 | 214 | .cancel:before { 215 | content: '\00B7'; 216 | color: #B3B3B1; 217 | padding: 0 5px; 218 | } 219 | } 220 | 221 | .replies { 222 | 223 | .right-of-avatar { 224 | width: @comments-wrapper-width - ( (@avatar-width + 10) * 2); 225 | } 226 | 227 | .delete { 228 | display: inline-block; 229 | float: left; 230 | margin: 0; 231 | } 232 | } 233 | } 234 | 235 | @comments-wrapper-width: 200px; 236 | 237 | @media (max-width: 768px) { 238 | .side-comments-open { 239 | transform: translate(-(@comments-wrapper-width + @marker-width), 0); 240 | } 241 | 242 | .side-comment { 243 | 244 | .comments-wrapper { 245 | width: @comments-wrapper-width; 246 | } 247 | 248 | .right-of-avatar { 249 | width: @comments-wrapper-width - (@avatar-width + 10); 250 | } 251 | 252 | .marker { 253 | display: block; 254 | } 255 | 256 | } 257 | } -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SideComments.js Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

13 | SideComments.js In Action 14 |

15 |

16 | Each paragraph tag has the "commentable-section" class, making it a section which can be commented on after you've initialized a new SideComments object and pointed it at the parent element, which is "#commentable-container" for this demo. 17 |

18 |

19 | Clicking on the markers on the right will show the SideComments. Sections without any comments only show their marker on hover. 20 |

21 |

22 | This is the default theme that comes with SideComments.js. You can easily theme SideComments to your liking by not including "default-theme.css" and just styling it all yourself. 23 |

24 |
25 | 26 | 27 | 28 | 41 | 42 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var uglify = require('gulp-uglify'); 4 | var minifycss = require('gulp-minify-css'); 5 | var less = require('gulp-less'); 6 | var prefix = require('gulp-autoprefixer'); 7 | var rename = require('gulp-rename'); 8 | 9 | var paths = { 10 | scripts: ['build/*.js'], 11 | themes: ['css/themes/*.less'] 12 | }; 13 | 14 | gulp.task('scripts', function() { 15 | // Minify and copy all JavaScript (except vendor scripts) 16 | return gulp.src(paths.scripts) 17 | .pipe(rename('side-comments.js')) 18 | .pipe(gulp.dest("./release")) 19 | .pipe(uglify()) 20 | .pipe(rename('side-comments.min.js')) 21 | .pipe(gulp.dest("./release")); 22 | }); 23 | 24 | gulp.task('base-styles', function () { 25 | return gulp.src('css/base.less') 26 | .pipe(less()) 27 | .pipe(prefix({ cascade: true })) 28 | .pipe(rename('styles.css')) 29 | .pipe(gulp.dest("./css")) 30 | .pipe(rename('side-comments.css')) 31 | .pipe(gulp.dest("./release")) 32 | .pipe(minifycss()) 33 | .pipe(rename('side-comments.min.css')) 34 | .pipe(gulp.dest("./release/")); 35 | }); 36 | 37 | gulp.task('theme-styles', function () { 38 | return gulp.src(paths.themes) 39 | .pipe(less()) 40 | .pipe(prefix({ cascade: true })) 41 | .pipe(rename('default-theme.css')) 42 | .pipe(gulp.dest("./release/themes")) 43 | .pipe(minifycss()) 44 | .pipe(rename('default-theme.min.css')) 45 | .pipe(gulp.dest("./release/themes")); 46 | }); 47 | 48 | // Rerun the task when a file changes 49 | gulp.task('watch', function() { 50 | gulp.watch(paths.scripts, ['scripts']); 51 | gulp.watch(['css/*.less', 'css/themes/*.less'], ['base-styles', 'theme-styles']); 52 | }); 53 | 54 | // The default task (called when you run `gulp` from cli) 55 | gulp.task('default', ['scripts', 'base-styles', 'theme-styles', 'watch']); -------------------------------------------------------------------------------- /js/comment.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var CommentTemplate = require('../templates/comment.html'); 3 | 4 | /** 5 | * Creates a new Comment 6 | * @param {[type]} section [description] 7 | * @param {[type]} attributes [description] 8 | */ 9 | function Comment( section, attributes ){ 10 | this.section = section; 11 | this.attributes = attributes; 12 | } 13 | 14 | Comment.prototype.render = function() { 15 | 16 | }; -------------------------------------------------------------------------------- /js/helpers/mobile-check.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | var check = false; 3 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera); 4 | return check; 5 | } -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | var _ = require('./vendor/lodash-custom.js'); 2 | var Section = require('./section.js'); 3 | var Emitter = require('emitter'); 4 | var $ = jQuery; 5 | 6 | /** 7 | * Creates a new SideComments instance. 8 | * @param {Object} el The selector for the element for 9 | * which side comments need to be initialized 10 | * @param {Object} currentUser An object defining the current user. Used 11 | * for posting new comments and deciding 12 | * whether existing ones can be deleted 13 | * or not. 14 | * @param {Array} existingComments An array of existing comments, in 15 | * the proper structure. 16 | * 17 | * TODO: **GIVE EXAMPLE OF STRUCTURE HERE*** 18 | */ 19 | function SideComments( el, currentUser, existingComments ) { 20 | this.$el = $(el); 21 | this.$body = $('body'); 22 | this.eventPipe = new Emitter; 23 | 24 | this.currentUser = _.clone(currentUser) || null; 25 | this.existingComments = _.cloneDeep(existingComments) || []; 26 | this.sections = []; 27 | this.activeSection = null; 28 | 29 | // Event bindings 30 | this.eventPipe.on('showComments', _.bind(this.showComments, this)); 31 | this.eventPipe.on('hideComments', _.bind(this.hideComments, this)); 32 | this.eventPipe.on('sectionSelected', _.bind(this.sectionSelected, this)); 33 | this.eventPipe.on('sectionDeselected', _.bind(this.sectionDeselected, this)); 34 | this.eventPipe.on('commentPosted', _.bind(this.commentPosted, this)); 35 | this.eventPipe.on('commentDeleted', _.bind(this.commentDeleted, this)); 36 | this.eventPipe.on('addCommentAttempted', _.bind(this.addCommentAttempted, this)); 37 | this.$body.on('click', _.bind(this.bodyClick, this)); 38 | this.initialize(this.existingComments); 39 | } 40 | 41 | // Mix in Emitter 42 | Emitter(SideComments.prototype); 43 | 44 | /** 45 | * Adds the comments beside each commentable section. 46 | */ 47 | SideComments.prototype.initialize = function( existingComments ) { 48 | _.each(this.$el.find('.commentable-section'), function( section ){ 49 | var $section = $(section); 50 | var sectionId = $section.data('section-id').toString(); 51 | var sectionComments = _.find(this.existingComments, { sectionId: sectionId }); 52 | 53 | this.sections.push(new Section(this.eventPipe, $section, this.currentUser, sectionComments)); 54 | }, this); 55 | }; 56 | 57 | /** 58 | * Shows the side comments. 59 | */ 60 | SideComments.prototype.showComments = function() { 61 | this.$el.addClass('side-comments-open'); 62 | }; 63 | 64 | /** 65 | * Hide the comments. 66 | */ 67 | SideComments.prototype.hideComments = function() { 68 | if (this.activeSection) { 69 | this.activeSection.deselect(); 70 | this.activeSection = null; 71 | } 72 | 73 | this.$el.removeClass('side-comments-open'); 74 | }; 75 | 76 | /** 77 | * Callback after a section has been selected. 78 | * @param {Object} section The Section object to be selected. 79 | */ 80 | SideComments.prototype.sectionSelected = function( section ) { 81 | this.showComments(); 82 | 83 | if (this.activeSection) { 84 | this.activeSection.deselect(); 85 | } 86 | 87 | this.activeSection = section; 88 | }; 89 | 90 | /** 91 | * Callback after a section has been deselected. 92 | * @param {Object} section The Section object to be selected. 93 | */ 94 | SideComments.prototype.sectionDeselected = function( section ) { 95 | this.hideComments(); 96 | this.activeSection = null; 97 | }; 98 | 99 | /** 100 | * Fired when the commentPosted event is triggered. 101 | * @param {Object} comment The comment object to be posted. 102 | */ 103 | SideComments.prototype.commentPosted = function( comment ) { 104 | this.emit('commentPosted', comment); 105 | }; 106 | 107 | /** 108 | * Fired when the commentDeleted event is triggered. 109 | * @param {Object} comment The commentId of the deleted comment. 110 | */ 111 | SideComments.prototype.commentDeleted = function( comment ) { 112 | this.emit('commentDeleted', comment); 113 | }; 114 | 115 | /** 116 | * Fire an event to to signal that a comment as attempted to be added without 117 | * a currentUser. 118 | */ 119 | SideComments.prototype.addCommentAttempted = function() { 120 | this.emit('addCommentAttempted'); 121 | }; 122 | 123 | /** 124 | * Inserts the given comment into the right section. 125 | * @param {Object} comment A comment to be inserted. 126 | */ 127 | SideComments.prototype.insertComment = function( comment ) { 128 | var section = _.find(this.sections, { id: comment.sectionId }); 129 | section.insertComment(comment); 130 | }; 131 | 132 | /** 133 | * Inserts the given comment into the right section as a reply. 134 | * @param {Object} comment A comment to be inserted. 135 | */ 136 | SideComments.prototype.replyComment = function( comment ) { 137 | var section = _.find(this.sections, { id: comment.sectionId}); 138 | section.insertComment(comment); 139 | }; 140 | 141 | /** 142 | * Removes the given comment from the right section. 143 | * @param sectionId The ID of the section where the comment exists. 144 | * @param commentId The ID of the comment to be removed. 145 | * @param parentId The ID of the parent comment of the reply to be removed. Optional 146 | */ 147 | SideComments.prototype.removeComment = function( sectionId, commentId, parentId ) { 148 | var section = _.find(this.sections, { id: sectionId }); 149 | section.removeComment(commentId, parentId); 150 | }; 151 | 152 | /** 153 | * Delete the comment specified by the given sectionID and commentID. 154 | * @param sectionId The section the comment belongs to. 155 | * @param commentId The comment's ID 156 | * @param parentId The parent comment's ID. Optional 157 | */ 158 | SideComments.prototype.deleteComment = function( sectionId, commentId, parentId ) { 159 | var section = _.find(this.sections, { id: sectionId }); 160 | section.deleteComment(commentId, parentId); 161 | }; 162 | 163 | /** 164 | * Checks if comments are visible or not. 165 | * @return {Boolean} Whether or not the comments are visible. 166 | */ 167 | SideComments.prototype.commentsAreVisible = function() { 168 | return this.$el.hasClass('side-comments-open'); 169 | }; 170 | 171 | /** 172 | * Callback for body clicks. We hide the comments if someone clicks outside of the comments section. 173 | * @param {Object} event The event object. 174 | */ 175 | SideComments.prototype.bodyClick = function( event ) { 176 | var $target = $(event.target); 177 | 178 | // We do a check on $('body') existing here because if the $target has 179 | // no parent body then it's because it belongs to a deleted comment and 180 | // we should NOT hide the SideComments. 181 | if ($target.closest('.side-comment').length < 1 && $target.closest('body').length > 0) { 182 | if (this.activeSection) { 183 | this.activeSection.deselect(); 184 | } 185 | this.hideComments(); 186 | } 187 | }; 188 | 189 | /** 190 | * Set the currentUser and update the UI as necessary. 191 | * @param {Object} currentUser The currentUser to be used. 192 | */ 193 | SideComments.prototype.setCurrentUser = function( currentUser ) { 194 | this.hideComments(); 195 | this.currentUser = currentUser; 196 | _.each(this.sections, _.bind(function( section ) { 197 | section.currentUser = this.currentUser; 198 | section.render(); 199 | }, this)); 200 | }; 201 | 202 | /** 203 | * Remove the currentUser and update the UI as necessary. 204 | */ 205 | SideComments.prototype.removeCurrentUser = function() { 206 | this.setCurrentUser(null); 207 | }; 208 | 209 | /** 210 | * Destroys the instance of SideComments, including unbinding from DOM events. 211 | */ 212 | SideComments.prototype.destroy = function() { 213 | this.hideComments(); 214 | this.$el.off(); 215 | }; 216 | 217 | module.exports = SideComments; 218 | -------------------------------------------------------------------------------- /js/section.js: -------------------------------------------------------------------------------- 1 | var _ = require('./vendor/lodash-custom.js'); 2 | var Template = require('../templates/section.html'); 3 | var CommentTemplate = require('../templates/comment.html'); 4 | var FormTemplate = require('../templates/form.html'); 5 | var mobileCheck = require('./helpers/mobile-check.js'); 6 | var $ = jQuery; 7 | 8 | /** 9 | * Creates a new Section object, which is responsible for managing a 10 | * single comment section. 11 | * @param {Object} eventPipe The Emitter object used for passing around events. 12 | * @param {Array} comments The array of comments for this section. Optional. 13 | */ 14 | function Section( eventPipe, $el, currentUser, comments ) { 15 | this.eventPipe = eventPipe; 16 | this.$el = $el; 17 | this.comments = comments ? comments.comments : []; 18 | this.currentUser = currentUser || null; 19 | this.clickEventName = mobileCheck() ? 'touchstart' : 'click'; 20 | 21 | this.id = $el.data('section-id'); 22 | 23 | this.$el.on(this.clickEventName, '.side-comment .marker', _.bind(this.markerClick, this)); 24 | this.$el.on(this.clickEventName, '.side-comment .add-comment', _.bind(this.addCommentClick, this)); 25 | this.$el.on(this.clickEventName, '.side-comment .reply-comment', _.bind(this.replyCommentClick, this)); 26 | this.$el.on(this.clickEventName, '.side-comment .post', _.bind(this.postCommentClick, this)); 27 | this.$el.on(this.clickEventName, '.side-comment .cancel', _.bind(this.cancelCommentClick, this)); 28 | this.$el.on(this.clickEventName, '.side-comment .delete', _.bind(this.deleteCommentClick, this)); 29 | this.render(); 30 | } 31 | 32 | /** 33 | * Click callback event on markers. 34 | * @param {Object} event The event object. 35 | */ 36 | Section.prototype.markerClick = function( event ) { 37 | event.preventDefault(); 38 | this.select(); 39 | }; 40 | 41 | /** 42 | * Callback for the comment button click event. 43 | * @param {Object} event The event object. 44 | */ 45 | Section.prototype.addCommentClick = function( event ) { 46 | event.preventDefault(); 47 | if (this.currentUser) { 48 | this.showCommentForm(); 49 | } else { 50 | this.eventPipe.emit('addCommentAttempted'); 51 | } 52 | }; 53 | 54 | /** 55 | * Show the comment form for this section. 56 | */ 57 | Section.prototype.showCommentForm = function() { 58 | if (this.comments.length > 0) { 59 | this.hideCommentForm(); 60 | this.$el.find('.add-comment').addClass('hide'); 61 | this.$el.find('.comment-form').addClass('active'); 62 | } 63 | 64 | this.focusCommentBox(); 65 | }; 66 | 67 | /** 68 | * Callback for the reply button click event. 69 | * @param {Object} event The event object. 70 | */ 71 | Section.prototype.replyCommentClick = function( event ) { 72 | event.preventDefault(); 73 | if (this.currentUser) { 74 | this.showReplyForm(event.currentTarget); 75 | } else { 76 | this.eventPipe.emit('addCommentAttempted'); 77 | } 78 | }; 79 | 80 | /** 81 | * Show the reply form for this section. 82 | */ 83 | Section.prototype.showReplyForm = function( replyButton ) { 84 | if (this.comments.length > 0) { 85 | this.hideCommentForm(); 86 | this.$el.find(replyButton).addClass('hide'); 87 | $form = $(_.find($.makeArray(this.$el.find('.reply-form')), function (el) {return el.dataset.parent === replyButton.dataset.comment})); 88 | $form.addClass('active'); 89 | } 90 | 91 | this.focusCommentBox(); 92 | }; 93 | 94 | /** 95 | * Hides the comment form for this section. 96 | */ 97 | Section.prototype.hideCommentForm = function() { 98 | if (this.comments.length > 0) { 99 | this.$el.find('a[class*="-comment"]').removeClass('hide'); 100 | this.$el.find('div[class*="-form"]').removeClass('active'); 101 | } 102 | 103 | this.$el.find('.comment-box').empty(); 104 | }; 105 | 106 | /** 107 | * Focus on the comment box in the comment form. 108 | */ 109 | Section.prototype.focusCommentBox = function() { 110 | // NOTE: !!HACK!! Using a timeout here because the autofocus causes a weird 111 | // "jump" in the form. It renders wider than it should be on screens under 768px 112 | // and then jumps to a smaller size. 113 | setTimeout(_.bind(function(){ 114 | this.$el.find('.comment-box').get(0).focus(); 115 | }, this), 300); 116 | }; 117 | 118 | /** 119 | * Cancel comment callback. 120 | * @param {Object} event The event object. 121 | */ 122 | Section.prototype.cancelCommentClick = function( event ) { 123 | event.preventDefault(); 124 | this.cancelComment(); 125 | }; 126 | 127 | /** 128 | * Cancel adding of a comment. 129 | */ 130 | Section.prototype.cancelComment = function() { 131 | if (this.comments.length > 0) { 132 | this.hideCommentForm(); 133 | } else { 134 | this.deselect(); 135 | this.eventPipe.emit('hideComments'); 136 | } 137 | }; 138 | 139 | /** 140 | * Post comment callback. 141 | * @param {Object} event The event object. 142 | */ 143 | Section.prototype.postCommentClick = function( event ) { 144 | event.preventDefault(); 145 | this.postComment(); 146 | }; 147 | 148 | /** 149 | * Post a comment to this section. 150 | */ 151 | Section.prototype.postComment = function() { 152 | if ( this.$el.find('.comments > li').length > 0 ){ 153 | var $commentForm = this.$el.find('div[class*="-form"].active'); 154 | } else { 155 | var $commentForm = this.$el.find('div[class*="-form"]'); 156 | } 157 | 158 | var $commentBox = $commentForm.find('.comment-box'), 159 | commentBody = $commentBox.val(), 160 | comment = { 161 | sectionId: this.id, 162 | comment: commentBody, 163 | authorAvatarUrl: this.currentUser.avatarUrl, 164 | authorName: this.currentUser.name, 165 | authorId: this.currentUser.id, 166 | authorUrl: this.currentUser.authorUrl || null, 167 | replies: [] 168 | }; 169 | 170 | if ( Number($commentForm.data('parent')) ) { 171 | comment.parentId = Number($commentForm.data('parent')); 172 | } 173 | 174 | $commentBox.val(''); // Clear the comment. 175 | this.eventPipe.emit('commentPosted', comment); 176 | }; 177 | 178 | /** 179 | * Insert a comment into this sections comment list. 180 | * @param {Object} comment A comment object. 181 | */ 182 | Section.prototype.insertComment = function( comment ) { 183 | 184 | var newCommentHtml = _.template(CommentTemplate, { 185 | comment: comment, 186 | currentUser: this.currentUser, 187 | formTemplate: FormTemplate, 188 | self: CommentTemplate 189 | }); 190 | 191 | if ( comment.parentId !== undefined ) { 192 | _.find(this.comments, { id: comment.parentId }).replies.push(comment); 193 | $parent = $(_.find($.makeArray(this.$el.find('.comments > li')), function ( el ) { return el.dataset.commentId == comment.parentId })); 194 | $parent.find('.replies').append(newCommentHtml); 195 | } else { 196 | this.comments.push(comment); 197 | this.$el.find('.comments').append(newCommentHtml); 198 | } 199 | 200 | this.$el.find('.side-comment').addClass('has-comments'); 201 | this.updateCommentCount(); 202 | this.hideCommentForm(); 203 | }; 204 | 205 | /** 206 | * Increments the comment count for a given section. 207 | */ 208 | Section.prototype.updateCommentCount = function() { 209 | this.$el.find('.marker span').text(this.comments.length); 210 | }; 211 | 212 | /** 213 | * Event handler for delete comment clicks. 214 | * @param {Object} event The event object. 215 | */ 216 | Section.prototype.deleteCommentClick = function( event ) { 217 | event.preventDefault(); 218 | var commentId = $(event.target).closest('li').data('comment-id'), 219 | parentId = $(event.target).data('parent-id'); 220 | 221 | if (window.confirm("Are you sure you want to delete this comment?")) { 222 | this.deleteComment(commentId, parentId); 223 | } 224 | }; 225 | 226 | /** 227 | * Finds the comment and emits an event with the comment to be deleted. 228 | * @param commentId ID of the comment to be deleted 229 | * @param parentId ID of the parent comment of the reply to be deleted. Optional 230 | */ 231 | Section.prototype.deleteComment = function( commentId, parentId ) { 232 | if ( parentId != null ) { 233 | var parent = _.find(this.comments, { id: parentId }), 234 | comment = _.find(parent.replies, { id: commentId }); 235 | } else { 236 | var comment = _.find(this.comments, { id: commentId }); 237 | } 238 | 239 | comment.sectionId = this.id; 240 | this.eventPipe.emit('commentDeleted', comment); 241 | }; 242 | 243 | /** 244 | * Removes the comment from the list of comments and the comment array. 245 | * @param commentId ID of the comment to be removed from this section 246 | * @param parentId ID of the parent comment of the reply to be removed from this section. Optional 247 | */ 248 | Section.prototype.removeComment = function( commentId, parentId ) { 249 | 250 | if ( parentId != null ) { 251 | var comment = _.find(this.comments, { id: parentId }); 252 | comment.replies = _.reject( comment.replies, { id: commentId }); 253 | this.$el.find('.side-comment .comments > li[data-comment-id="'+parentId+'"] .replies li[data-comment-id="'+commentId+'"]').remove(); 254 | } else { 255 | var comment = _.find(this.comments, { id: commentId }); 256 | 257 | if ( comment.replies.length > 0 ) { 258 | this.replaceCommentWithReplies( comment ); 259 | } else { 260 | this.comments = _.reject(this.comments, { id: commentId }); 261 | this.$el.find('.side-comment .comments li[data-comment-id="'+commentId+'"]').remove(); 262 | this.updateCommentCount(); 263 | } 264 | } 265 | 266 | if (this.comments.length < 1) { 267 | this.$el.find('.side-comment').removeClass('has-comments'); 268 | } 269 | }; 270 | 271 | /** 272 | * Replace a comment with replies 273 | * 274 | * 275 | */ 276 | Section.prototype.replaceCommentWithReplies = function ( comment ) { 277 | var $commentEl = this.$el.find('.side-comment .comments > li[data-comment-id="'+ comment.id +'"] > .comment'); 278 | 279 | comment.deleted = true; 280 | $commentEl.html('Comment deleted by the author'); 281 | } 282 | 283 | /** 284 | * Mark this section as selected. Delsect if this section is already selected. 285 | */ 286 | Section.prototype.select = function() { 287 | if (this.isSelected()) { 288 | this.deselect(); 289 | this.eventPipe.emit('sectionDeselected', this); 290 | } else { 291 | this.$el.find('.side-comment').addClass('active'); 292 | 293 | if (this.comments.length === 0 && this.currentUser) { 294 | this.focusCommentBox(); 295 | } 296 | 297 | this.eventPipe.emit('sectionSelected', this); 298 | } 299 | }; 300 | 301 | /** 302 | * Deselect this section. 303 | */ 304 | Section.prototype.deselect = function() { 305 | this.$el.find('.side-comment').removeClass('active'); 306 | this.hideCommentForm(); 307 | }; 308 | 309 | Section.prototype.isSelected = function() { 310 | return this.$el.find('.side-comment').hasClass('active'); 311 | }; 312 | 313 | /** 314 | * Get the class to be used on the side comment section wrapper. 315 | * @return {String} The class names to use. 316 | */ 317 | Section.prototype.sectionClasses = function() { 318 | var classes = ''; 319 | 320 | if (this.comments.length > 0) { 321 | classes = classes + ' has-comments'; 322 | } 323 | if (!this.currentUser) { 324 | classes = classes + ' no-current-user' 325 | } 326 | 327 | return classes; 328 | }; 329 | 330 | /** 331 | * Render this section into the DOM. 332 | */ 333 | Section.prototype.render = function() { 334 | this.$el.find('.side-comment').remove(); 335 | $(_.template(Template, { 336 | commentTemplate: CommentTemplate, 337 | comments: this.comments, 338 | sectionClasses: this.sectionClasses(), 339 | formTemplate: FormTemplate, 340 | currentUser: this.currentUser 341 | })).appendTo(this.$el); 342 | }; 343 | 344 | /** 345 | * Desttroy this Section object. Generally meaning unbind events. 346 | */ 347 | Section.prototype.destroy = function() { 348 | this.$el.off(); 349 | } 350 | 351 | module.exports = Section; -------------------------------------------------------------------------------- /js/vendor/lodash-custom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Lo-Dash 2.4.1 (Custom Build) 4 | * Build: `lodash exports="node,commonjs" include="each,bind,find,template,reject,clone,cloneDeep"` 5 | * Copyright 2012-2013 The Dojo Foundation 6 | * Based on Underscore.js 1.5.2 7 | * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | * Available under MIT license 9 | */ 10 | ;(function() { 11 | 12 | /** Used as a safe reference for `undefined` in pre ES5 environments */ 13 | var undefined; 14 | 15 | /** Used to pool arrays and objects used internally */ 16 | var arrayPool = []; 17 | 18 | /** Used internally to indicate various things */ 19 | var indicatorObject = {}; 20 | 21 | /** Used as the max size of the `arrayPool` and `objectPool` */ 22 | var maxPoolSize = 40; 23 | 24 | /** Used to match empty string literals in compiled template source */ 25 | var reEmptyStringLeading = /\b__p \+= '';/g, 26 | reEmptyStringMiddle = /\b(__p \+=) '' \+/g, 27 | reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; 28 | 29 | /** 30 | * Used to match ES6 template delimiters 31 | * http://people.mozilla.org/~jorendorff/es6-draft.html#sec-literals-string-literals 32 | */ 33 | var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; 34 | 35 | /** Used to match regexp flags from their coerced string values */ 36 | var reFlags = /\w*$/; 37 | 38 | /** Used to detected named functions */ 39 | var reFuncName = /^\s*function[ \n\r\t]+\w/; 40 | 41 | /** Used to match "interpolate" template delimiters */ 42 | var reInterpolate = /<%=([\s\S]+?)%>/g; 43 | 44 | /** Used to ensure capturing order of template delimiters */ 45 | var reNoMatch = /($^)/; 46 | 47 | /** Used to detect functions containing a `this` reference */ 48 | var reThis = /\bthis\b/; 49 | 50 | /** Used to match unescaped characters in compiled string literals */ 51 | var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; 52 | 53 | /** Used to fix the JScript [[DontEnum]] bug */ 54 | var shadowedProps = [ 55 | 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 56 | 'toLocaleString', 'toString', 'valueOf' 57 | ]; 58 | 59 | /** Used to make template sourceURLs easier to identify */ 60 | var templateCounter = 0; 61 | 62 | /** `Object#toString` result shortcuts */ 63 | var argsClass = '[object Arguments]', 64 | arrayClass = '[object Array]', 65 | boolClass = '[object Boolean]', 66 | dateClass = '[object Date]', 67 | errorClass = '[object Error]', 68 | funcClass = '[object Function]', 69 | numberClass = '[object Number]', 70 | objectClass = '[object Object]', 71 | regexpClass = '[object RegExp]', 72 | stringClass = '[object String]'; 73 | 74 | /** Used to identify object classifications that `_.clone` supports */ 75 | var cloneableClasses = {}; 76 | cloneableClasses[funcClass] = false; 77 | cloneableClasses[argsClass] = cloneableClasses[arrayClass] = 78 | cloneableClasses[boolClass] = cloneableClasses[dateClass] = 79 | cloneableClasses[numberClass] = cloneableClasses[objectClass] = 80 | cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; 81 | 82 | /** Used as the property descriptor for `__bindData__` */ 83 | var descriptor = { 84 | 'configurable': false, 85 | 'enumerable': false, 86 | 'value': null, 87 | 'writable': false 88 | }; 89 | 90 | /** Used as the data object for `iteratorTemplate` */ 91 | var iteratorData = { 92 | 'args': '', 93 | 'array': null, 94 | 'bottom': '', 95 | 'firstArg': '', 96 | 'init': '', 97 | 'keys': null, 98 | 'loop': '', 99 | 'shadowedProps': null, 100 | 'support': null, 101 | 'top': '', 102 | 'useHas': false 103 | }; 104 | 105 | /** Used to determine if values are of the language type Object */ 106 | var objectTypes = { 107 | 'boolean': false, 108 | 'function': true, 109 | 'object': true, 110 | 'number': false, 111 | 'string': false, 112 | 'undefined': false 113 | }; 114 | 115 | /** Used to escape characters for inclusion in compiled string literals */ 116 | var stringEscapes = { 117 | '\\': '\\', 118 | "'": "'", 119 | '\n': 'n', 120 | '\r': 'r', 121 | '\t': 't', 122 | '\u2028': 'u2028', 123 | '\u2029': 'u2029' 124 | }; 125 | 126 | /** Used as a reference to the global object */ 127 | var root = (objectTypes[typeof window] && window) || this; 128 | 129 | /** Detect free variable `exports` */ 130 | var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; 131 | 132 | /** Detect free variable `module` */ 133 | var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; 134 | 135 | /** Detect the popular CommonJS extension `module.exports` */ 136 | var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; 137 | 138 | /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ 139 | var freeGlobal = objectTypes[typeof global] && global; 140 | if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { 141 | root = freeGlobal; 142 | } 143 | 144 | /*--------------------------------------------------------------------------*/ 145 | 146 | /** 147 | * Used by `template` to escape characters for inclusion in compiled 148 | * string literals. 149 | * 150 | * @private 151 | * @param {string} match The matched character to escape. 152 | * @returns {string} Returns the escaped character. 153 | */ 154 | function escapeStringChar(match) { 155 | return '\\' + stringEscapes[match]; 156 | } 157 | 158 | /** 159 | * Gets an array from the array pool or creates a new one if the pool is empty. 160 | * 161 | * @private 162 | * @returns {Array} The array from the pool. 163 | */ 164 | function getArray() { 165 | return arrayPool.pop() || []; 166 | } 167 | 168 | /** 169 | * Checks if `value` is a DOM node in IE < 9. 170 | * 171 | * @private 172 | * @param {*} value The value to check. 173 | * @returns {boolean} Returns `true` if the `value` is a DOM node, else `false`. 174 | */ 175 | function isNode(value) { 176 | // IE < 9 presents DOM nodes as `Object` objects except they have `toString` 177 | // methods that are `typeof` "string" and still can coerce nodes to strings 178 | return typeof value.toString != 'function' && typeof (value + '') == 'string'; 179 | } 180 | 181 | /** 182 | * Releases the given array back to the array pool. 183 | * 184 | * @private 185 | * @param {Array} [array] The array to release. 186 | */ 187 | function releaseArray(array) { 188 | array.length = 0; 189 | if (arrayPool.length < maxPoolSize) { 190 | arrayPool.push(array); 191 | } 192 | } 193 | 194 | /** 195 | * Slices the `collection` from the `start` index up to, but not including, 196 | * the `end` index. 197 | * 198 | * Note: This function is used instead of `Array#slice` to support node lists 199 | * in IE < 9 and to ensure dense arrays are returned. 200 | * 201 | * @private 202 | * @param {Array|Object|string} collection The collection to slice. 203 | * @param {number} start The start index. 204 | * @param {number} end The end index. 205 | * @returns {Array} Returns the new array. 206 | */ 207 | function slice(array, start, end) { 208 | start || (start = 0); 209 | if (typeof end == 'undefined') { 210 | end = array ? array.length : 0; 211 | } 212 | var index = -1, 213 | length = end - start || 0, 214 | result = Array(length < 0 ? 0 : length); 215 | 216 | while (++index < length) { 217 | result[index] = array[start + index]; 218 | } 219 | return result; 220 | } 221 | 222 | /*--------------------------------------------------------------------------*/ 223 | 224 | /** 225 | * Used for `Array` method references. 226 | * 227 | * Normally `Array.prototype` would suffice, however, using an array literal 228 | * avoids issues in Narwhal. 229 | */ 230 | var arrayRef = []; 231 | 232 | /** Used for native method references */ 233 | var errorProto = Error.prototype, 234 | objectProto = Object.prototype, 235 | stringProto = String.prototype; 236 | 237 | /** Used to resolve the internal [[Class]] of values */ 238 | var toString = objectProto.toString; 239 | 240 | /** Used to detect if a method is native */ 241 | var reNative = RegExp('^' + 242 | String(toString) 243 | .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 244 | .replace(/toString| for [^\]]+/g, '.*?') + '$' 245 | ); 246 | 247 | /** Native method shortcuts */ 248 | var fnToString = Function.prototype.toString, 249 | hasOwnProperty = objectProto.hasOwnProperty, 250 | push = arrayRef.push, 251 | propertyIsEnumerable = objectProto.propertyIsEnumerable, 252 | unshift = arrayRef.unshift; 253 | 254 | /** Used to set meta data on functions */ 255 | var defineProperty = (function() { 256 | // IE 8 only accepts DOM elements 257 | try { 258 | var o = {}, 259 | func = isNative(func = Object.defineProperty) && func, 260 | result = func(o, o, o) && func; 261 | } catch(e) { } 262 | return result; 263 | }()); 264 | 265 | /* Native method shortcuts for methods with the same name as other `lodash` methods */ 266 | var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, 267 | nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, 268 | nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys; 269 | 270 | /** Used to lookup a built-in constructor by [[Class]] */ 271 | var ctorByClass = {}; 272 | ctorByClass[arrayClass] = Array; 273 | ctorByClass[boolClass] = Boolean; 274 | ctorByClass[dateClass] = Date; 275 | ctorByClass[funcClass] = Function; 276 | ctorByClass[objectClass] = Object; 277 | ctorByClass[numberClass] = Number; 278 | ctorByClass[regexpClass] = RegExp; 279 | ctorByClass[stringClass] = String; 280 | 281 | /** Used to avoid iterating non-enumerable properties in IE < 9 */ 282 | var nonEnumProps = {}; 283 | nonEnumProps[arrayClass] = nonEnumProps[dateClass] = nonEnumProps[numberClass] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; 284 | nonEnumProps[boolClass] = nonEnumProps[stringClass] = { 'constructor': true, 'toString': true, 'valueOf': true }; 285 | nonEnumProps[errorClass] = nonEnumProps[funcClass] = nonEnumProps[regexpClass] = { 'constructor': true, 'toString': true }; 286 | nonEnumProps[objectClass] = { 'constructor': true }; 287 | 288 | (function() { 289 | var length = shadowedProps.length; 290 | while (length--) { 291 | var key = shadowedProps[length]; 292 | for (var className in nonEnumProps) { 293 | if (hasOwnProperty.call(nonEnumProps, className) && !hasOwnProperty.call(nonEnumProps[className], key)) { 294 | nonEnumProps[className][key] = false; 295 | } 296 | } 297 | } 298 | }()); 299 | 300 | /*--------------------------------------------------------------------------*/ 301 | 302 | /** 303 | * Creates a `lodash` object which wraps the given value to enable intuitive 304 | * method chaining. 305 | * 306 | * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: 307 | * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, 308 | * and `unshift` 309 | * 310 | * Chaining is supported in custom builds as long as the `value` method is 311 | * implicitly or explicitly included in the build. 312 | * 313 | * The chainable wrapper functions are: 314 | * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, 315 | * `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`, 316 | * `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, 317 | * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, 318 | * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, 319 | * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, 320 | * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, 321 | * `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, 322 | * `sortBy`, `splice`, `tap`, `throttle`, `times`, `toArray`, `transform`, 323 | * `union`, `uniq`, `unshift`, `unzip`, `values`, `where`, `without`, `wrap`, 324 | * and `zip` 325 | * 326 | * The non-chainable wrapper functions are: 327 | * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, 328 | * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, 329 | * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, 330 | * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, 331 | * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, 332 | * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, 333 | * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, 334 | * `template`, `unescape`, `uniqueId`, and `value` 335 | * 336 | * The wrapper functions `first` and `last` return wrapped values when `n` is 337 | * provided, otherwise they return unwrapped values. 338 | * 339 | * Explicit chaining can be enabled by using the `_.chain` method. 340 | * 341 | * @name _ 342 | * @constructor 343 | * @category Chaining 344 | * @param {*} value The value to wrap in a `lodash` instance. 345 | * @returns {Object} Returns a `lodash` instance. 346 | * @example 347 | * 348 | * var wrapped = _([1, 2, 3]); 349 | * 350 | * // returns an unwrapped value 351 | * wrapped.reduce(function(sum, num) { 352 | * return sum + num; 353 | * }); 354 | * // => 6 355 | * 356 | * // returns a wrapped value 357 | * var squares = wrapped.map(function(num) { 358 | * return num * num; 359 | * }); 360 | * 361 | * _.isArray(squares); 362 | * // => false 363 | * 364 | * _.isArray(squares.value()); 365 | * // => true 366 | */ 367 | function lodash() { 368 | // no operation performed 369 | } 370 | 371 | /** 372 | * An object used to flag environments features. 373 | * 374 | * @static 375 | * @memberOf _ 376 | * @type Object 377 | */ 378 | var support = lodash.support = {}; 379 | 380 | (function() { 381 | var ctor = function() { this.x = 1; }, 382 | object = { '0': 1, 'length': 1 }, 383 | props = []; 384 | 385 | ctor.prototype = { 'valueOf': 1, 'y': 1 }; 386 | for (var key in new ctor) { props.push(key); } 387 | for (key in arguments) { } 388 | 389 | /** 390 | * Detect if an `arguments` object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). 391 | * 392 | * @memberOf _.support 393 | * @type boolean 394 | */ 395 | support.argsClass = toString.call(arguments) == argsClass; 396 | 397 | /** 398 | * Detect if `arguments` objects are `Object` objects (all but Narwhal and Opera < 10.5). 399 | * 400 | * @memberOf _.support 401 | * @type boolean 402 | */ 403 | support.argsObject = arguments.constructor == Object && !(arguments instanceof Array); 404 | 405 | /** 406 | * Detect if `name` or `message` properties of `Error.prototype` are 407 | * enumerable by default. (IE < 9, Safari < 5.1) 408 | * 409 | * @memberOf _.support 410 | * @type boolean 411 | */ 412 | support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || propertyIsEnumerable.call(errorProto, 'name'); 413 | 414 | /** 415 | * Detect if `prototype` properties are enumerable by default. 416 | * 417 | * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 418 | * (if the prototype or a property on the prototype has been set) 419 | * incorrectly sets a function's `prototype` property [[Enumerable]] 420 | * value to `true`. 421 | * 422 | * @memberOf _.support 423 | * @type boolean 424 | */ 425 | support.enumPrototypes = propertyIsEnumerable.call(ctor, 'prototype'); 426 | 427 | /** 428 | * Detect if functions can be decompiled by `Function#toString` 429 | * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). 430 | * 431 | * @memberOf _.support 432 | * @type boolean 433 | */ 434 | support.funcDecomp = !isNative(root.WinRTError) && reThis.test(function() { return this; }); 435 | 436 | /** 437 | * Detect if `Function#name` is supported (all but IE). 438 | * 439 | * @memberOf _.support 440 | * @type boolean 441 | */ 442 | support.funcNames = typeof Function.name == 'string'; 443 | 444 | /** 445 | * Detect if `arguments` object indexes are non-enumerable 446 | * (Firefox < 4, IE < 9, PhantomJS, Safari < 5.1). 447 | * 448 | * @memberOf _.support 449 | * @type boolean 450 | */ 451 | support.nonEnumArgs = key != 0; 452 | 453 | /** 454 | * Detect if properties shadowing those on `Object.prototype` are non-enumerable. 455 | * 456 | * In IE < 9 an objects own properties, shadowing non-enumerable ones, are 457 | * made non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). 458 | * 459 | * @memberOf _.support 460 | * @type boolean 461 | */ 462 | support.nonEnumShadows = !/valueOf/.test(props); 463 | 464 | /** 465 | * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. 466 | * 467 | * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` 468 | * and `splice()` functions that fail to remove the last element, `value[0]`, 469 | * of array-like objects even though the `length` property is set to `0`. 470 | * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` 471 | * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. 472 | * 473 | * @memberOf _.support 474 | * @type boolean 475 | */ 476 | support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); 477 | 478 | /** 479 | * Detect lack of support for accessing string characters by index. 480 | * 481 | * IE < 8 can't access characters by index and IE 8 can only access 482 | * characters by index on string literals. 483 | * 484 | * @memberOf _.support 485 | * @type boolean 486 | */ 487 | support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; 488 | 489 | /** 490 | * Detect if a DOM node's [[Class]] is resolvable (all but IE < 9) 491 | * and that the JS engine errors when attempting to coerce an object to 492 | * a string without a `toString` function. 493 | * 494 | * @memberOf _.support 495 | * @type boolean 496 | */ 497 | try { 498 | support.nodeClass = !(toString.call(document) == objectClass && !({ 'toString': 0 } + '')); 499 | } catch(e) { 500 | support.nodeClass = true; 501 | } 502 | }(1)); 503 | 504 | /** 505 | * By default, the template delimiters used by Lo-Dash are similar to those in 506 | * embedded Ruby (ERB). Change the following template settings to use alternative 507 | * delimiters. 508 | * 509 | * @static 510 | * @memberOf _ 511 | * @type Object 512 | */ 513 | lodash.templateSettings = { 514 | 515 | /** 516 | * Used to detect `data` property values to be HTML-escaped. 517 | * 518 | * @memberOf _.templateSettings 519 | * @type RegExp 520 | */ 521 | 'escape': /<%-([\s\S]+?)%>/g, 522 | 523 | /** 524 | * Used to detect code to be evaluated. 525 | * 526 | * @memberOf _.templateSettings 527 | * @type RegExp 528 | */ 529 | 'evaluate': /<%([\s\S]+?)%>/g, 530 | 531 | /** 532 | * Used to detect `data` property values to inject. 533 | * 534 | * @memberOf _.templateSettings 535 | * @type RegExp 536 | */ 537 | 'interpolate': reInterpolate, 538 | 539 | /** 540 | * Used to reference the data object in the template text. 541 | * 542 | * @memberOf _.templateSettings 543 | * @type string 544 | */ 545 | 'variable': '', 546 | 547 | /** 548 | * Used to import variables into the compiled template. 549 | * 550 | * @memberOf _.templateSettings 551 | * @type Object 552 | */ 553 | 'imports': { 554 | 555 | /** 556 | * A reference to the `lodash` function. 557 | * 558 | * @memberOf _.templateSettings.imports 559 | * @type Function 560 | */ 561 | '_': lodash 562 | } 563 | }; 564 | 565 | /*--------------------------------------------------------------------------*/ 566 | 567 | /** 568 | * The template used to create iterator functions. 569 | * 570 | * @private 571 | * @param {Object} data The data object used to populate the text. 572 | * @returns {string} Returns the interpolated text. 573 | */ 574 | var iteratorTemplate = function(obj) { 575 | 576 | var __p = 'var index, iterable = ' + 577 | (obj.firstArg) + 578 | ', result = ' + 579 | (obj.init) + 580 | ';\nif (!iterable) return result;\n' + 581 | (obj.top) + 582 | ';'; 583 | if (obj.array) { 584 | __p += '\nvar length = iterable.length; index = -1;\nif (' + 585 | (obj.array) + 586 | ') { '; 587 | if (support.unindexedChars) { 588 | __p += '\n if (isString(iterable)) {\n iterable = iterable.split(\'\')\n } '; 589 | } 590 | __p += '\n while (++index < length) {\n ' + 591 | (obj.loop) + 592 | ';\n }\n}\nelse { '; 593 | } else if (support.nonEnumArgs) { 594 | __p += '\n var length = iterable.length; index = -1;\n if (length && isArguments(iterable)) {\n while (++index < length) {\n index += \'\';\n ' + 595 | (obj.loop) + 596 | ';\n }\n } else { '; 597 | } 598 | 599 | if (support.enumPrototypes) { 600 | __p += '\n var skipProto = typeof iterable == \'function\';\n '; 601 | } 602 | 603 | if (support.enumErrorProps) { 604 | __p += '\n var skipErrorProps = iterable === errorProto || iterable instanceof Error;\n '; 605 | } 606 | 607 | var conditions = []; if (support.enumPrototypes) { conditions.push('!(skipProto && index == "prototype")'); } if (support.enumErrorProps) { conditions.push('!(skipErrorProps && (index == "message" || index == "name"))'); } 608 | 609 | if (obj.useHas && obj.keys) { 610 | __p += '\n var ownIndex = -1,\n ownProps = objectTypes[typeof iterable] && keys(iterable),\n length = ownProps ? ownProps.length : 0;\n\n while (++ownIndex < length) {\n index = ownProps[ownIndex];\n'; 611 | if (conditions.length) { 612 | __p += ' if (' + 613 | (conditions.join(' && ')) + 614 | ') {\n '; 615 | } 616 | __p += 617 | (obj.loop) + 618 | '; '; 619 | if (conditions.length) { 620 | __p += '\n }'; 621 | } 622 | __p += '\n } '; 623 | } else { 624 | __p += '\n for (index in iterable) {\n'; 625 | if (obj.useHas) { conditions.push("hasOwnProperty.call(iterable, index)"); } if (conditions.length) { 626 | __p += ' if (' + 627 | (conditions.join(' && ')) + 628 | ') {\n '; 629 | } 630 | __p += 631 | (obj.loop) + 632 | '; '; 633 | if (conditions.length) { 634 | __p += '\n }'; 635 | } 636 | __p += '\n } '; 637 | if (support.nonEnumShadows) { 638 | __p += '\n\n if (iterable !== objectProto) {\n var ctor = iterable.constructor,\n isProto = iterable === (ctor && ctor.prototype),\n className = iterable === stringProto ? stringClass : iterable === errorProto ? errorClass : toString.call(iterable),\n nonEnum = nonEnumProps[className];\n '; 639 | for (k = 0; k < 7; k++) { 640 | __p += '\n index = \'' + 641 | (obj.shadowedProps[k]) + 642 | '\';\n if ((!(isProto && nonEnum[index]) && hasOwnProperty.call(iterable, index))'; 643 | if (!obj.useHas) { 644 | __p += ' || (!nonEnum[index] && iterable[index] !== objectProto[index])'; 645 | } 646 | __p += ') {\n ' + 647 | (obj.loop) + 648 | ';\n } '; 649 | } 650 | __p += '\n } '; 651 | } 652 | 653 | } 654 | 655 | if (obj.array || support.nonEnumArgs) { 656 | __p += '\n}'; 657 | } 658 | __p += 659 | (obj.bottom) + 660 | ';\nreturn result'; 661 | 662 | return __p 663 | }; 664 | 665 | /*--------------------------------------------------------------------------*/ 666 | 667 | /** 668 | * The base implementation of `_.bind` that creates the bound function and 669 | * sets its meta data. 670 | * 671 | * @private 672 | * @param {Array} bindData The bind data array. 673 | * @returns {Function} Returns the new bound function. 674 | */ 675 | function baseBind(bindData) { 676 | var func = bindData[0], 677 | partialArgs = bindData[2], 678 | thisArg = bindData[4]; 679 | 680 | function bound() { 681 | // `Function#bind` spec 682 | // http://es5.github.io/#x15.3.4.5 683 | if (partialArgs) { 684 | // avoid `arguments` object deoptimizations by using `slice` instead 685 | // of `Array.prototype.slice.call` and not assigning `arguments` to a 686 | // variable as a ternary expression 687 | var args = slice(partialArgs); 688 | push.apply(args, arguments); 689 | } 690 | // mimic the constructor's `return` behavior 691 | // http://es5.github.io/#x13.2.2 692 | if (this instanceof bound) { 693 | // ensure `new bound` is an instance of `func` 694 | var thisBinding = baseCreate(func.prototype), 695 | result = func.apply(thisBinding, args || arguments); 696 | return isObject(result) ? result : thisBinding; 697 | } 698 | return func.apply(thisArg, args || arguments); 699 | } 700 | setBindData(bound, bindData); 701 | return bound; 702 | } 703 | 704 | /** 705 | * The base implementation of `_.clone` without argument juggling or support 706 | * for `thisArg` binding. 707 | * 708 | * @private 709 | * @param {*} value The value to clone. 710 | * @param {boolean} [isDeep=false] Specify a deep clone. 711 | * @param {Function} [callback] The function to customize cloning values. 712 | * @param {Array} [stackA=[]] Tracks traversed source objects. 713 | * @param {Array} [stackB=[]] Associates clones with source counterparts. 714 | * @returns {*} Returns the cloned value. 715 | */ 716 | function baseClone(value, isDeep, callback, stackA, stackB) { 717 | if (callback) { 718 | var result = callback(value); 719 | if (typeof result != 'undefined') { 720 | return result; 721 | } 722 | } 723 | // inspect [[Class]] 724 | var isObj = isObject(value); 725 | if (isObj) { 726 | var className = toString.call(value); 727 | if (!cloneableClasses[className] || (!support.nodeClass && isNode(value))) { 728 | return value; 729 | } 730 | var ctor = ctorByClass[className]; 731 | switch (className) { 732 | case boolClass: 733 | case dateClass: 734 | return new ctor(+value); 735 | 736 | case numberClass: 737 | case stringClass: 738 | return new ctor(value); 739 | 740 | case regexpClass: 741 | result = ctor(value.source, reFlags.exec(value)); 742 | result.lastIndex = value.lastIndex; 743 | return result; 744 | } 745 | } else { 746 | return value; 747 | } 748 | var isArr = isArray(value); 749 | if (isDeep) { 750 | // check for circular references and return corresponding clone 751 | var initedStack = !stackA; 752 | stackA || (stackA = getArray()); 753 | stackB || (stackB = getArray()); 754 | 755 | var length = stackA.length; 756 | while (length--) { 757 | if (stackA[length] == value) { 758 | return stackB[length]; 759 | } 760 | } 761 | result = isArr ? ctor(value.length) : {}; 762 | } 763 | else { 764 | result = isArr ? slice(value) : assign({}, value); 765 | } 766 | // add array properties assigned by `RegExp#exec` 767 | if (isArr) { 768 | if (hasOwnProperty.call(value, 'index')) { 769 | result.index = value.index; 770 | } 771 | if (hasOwnProperty.call(value, 'input')) { 772 | result.input = value.input; 773 | } 774 | } 775 | // exit for shallow clone 776 | if (!isDeep) { 777 | return result; 778 | } 779 | // add the source value to the stack of traversed objects 780 | // and associate it with its clone 781 | stackA.push(value); 782 | stackB.push(result); 783 | 784 | // recursively populate clone (susceptible to call stack limits) 785 | (isArr ? baseEach : forOwn)(value, function(objValue, key) { 786 | result[key] = baseClone(objValue, isDeep, callback, stackA, stackB); 787 | }); 788 | 789 | if (initedStack) { 790 | releaseArray(stackA); 791 | releaseArray(stackB); 792 | } 793 | return result; 794 | } 795 | 796 | /** 797 | * The base implementation of `_.create` without support for assigning 798 | * properties to the created object. 799 | * 800 | * @private 801 | * @param {Object} prototype The object to inherit from. 802 | * @returns {Object} Returns the new object. 803 | */ 804 | function baseCreate(prototype, properties) { 805 | return isObject(prototype) ? nativeCreate(prototype) : {}; 806 | } 807 | // fallback for browsers without `Object.create` 808 | if (!nativeCreate) { 809 | baseCreate = (function() { 810 | function Object() {} 811 | return function(prototype) { 812 | if (isObject(prototype)) { 813 | Object.prototype = prototype; 814 | var result = new Object; 815 | Object.prototype = null; 816 | } 817 | return result || root.Object(); 818 | }; 819 | }()); 820 | } 821 | 822 | /** 823 | * The base implementation of `_.createCallback` without support for creating 824 | * "_.pluck" or "_.where" style callbacks. 825 | * 826 | * @private 827 | * @param {*} [func=identity] The value to convert to a callback. 828 | * @param {*} [thisArg] The `this` binding of the created callback. 829 | * @param {number} [argCount] The number of arguments the callback accepts. 830 | * @returns {Function} Returns a callback function. 831 | */ 832 | function baseCreateCallback(func, thisArg, argCount) { 833 | if (typeof func != 'function') { 834 | return identity; 835 | } 836 | // exit early for no `thisArg` or already bound by `Function#bind` 837 | if (typeof thisArg == 'undefined' || !('prototype' in func)) { 838 | return func; 839 | } 840 | var bindData = func.__bindData__; 841 | if (typeof bindData == 'undefined') { 842 | if (support.funcNames) { 843 | bindData = !func.name; 844 | } 845 | bindData = bindData || !support.funcDecomp; 846 | if (!bindData) { 847 | var source = fnToString.call(func); 848 | if (!support.funcNames) { 849 | bindData = !reFuncName.test(source); 850 | } 851 | if (!bindData) { 852 | // checks if `func` references the `this` keyword and stores the result 853 | bindData = reThis.test(source); 854 | setBindData(func, bindData); 855 | } 856 | } 857 | } 858 | // exit early if there are no `this` references or `func` is bound 859 | if (bindData === false || (bindData !== true && bindData[1] & 1)) { 860 | return func; 861 | } 862 | switch (argCount) { 863 | case 1: return function(value) { 864 | return func.call(thisArg, value); 865 | }; 866 | case 2: return function(a, b) { 867 | return func.call(thisArg, a, b); 868 | }; 869 | case 3: return function(value, index, collection) { 870 | return func.call(thisArg, value, index, collection); 871 | }; 872 | case 4: return function(accumulator, value, index, collection) { 873 | return func.call(thisArg, accumulator, value, index, collection); 874 | }; 875 | } 876 | return bind(func, thisArg); 877 | } 878 | 879 | /** 880 | * The base implementation of `createWrapper` that creates the wrapper and 881 | * sets its meta data. 882 | * 883 | * @private 884 | * @param {Array} bindData The bind data array. 885 | * @returns {Function} Returns the new function. 886 | */ 887 | function baseCreateWrapper(bindData) { 888 | var func = bindData[0], 889 | bitmask = bindData[1], 890 | partialArgs = bindData[2], 891 | partialRightArgs = bindData[3], 892 | thisArg = bindData[4], 893 | arity = bindData[5]; 894 | 895 | var isBind = bitmask & 1, 896 | isBindKey = bitmask & 2, 897 | isCurry = bitmask & 4, 898 | isCurryBound = bitmask & 8, 899 | key = func; 900 | 901 | function bound() { 902 | var thisBinding = isBind ? thisArg : this; 903 | if (partialArgs) { 904 | var args = slice(partialArgs); 905 | push.apply(args, arguments); 906 | } 907 | if (partialRightArgs || isCurry) { 908 | args || (args = slice(arguments)); 909 | if (partialRightArgs) { 910 | push.apply(args, partialRightArgs); 911 | } 912 | if (isCurry && args.length < arity) { 913 | bitmask |= 16 & ~32; 914 | return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); 915 | } 916 | } 917 | args || (args = arguments); 918 | if (isBindKey) { 919 | func = thisBinding[key]; 920 | } 921 | if (this instanceof bound) { 922 | thisBinding = baseCreate(func.prototype); 923 | var result = func.apply(thisBinding, args); 924 | return isObject(result) ? result : thisBinding; 925 | } 926 | return func.apply(thisBinding, args); 927 | } 928 | setBindData(bound, bindData); 929 | return bound; 930 | } 931 | 932 | /** 933 | * The base implementation of `_.isEqual`, without support for `thisArg` binding, 934 | * that allows partial "_.where" style comparisons. 935 | * 936 | * @private 937 | * @param {*} a The value to compare. 938 | * @param {*} b The other value to compare. 939 | * @param {Function} [callback] The function to customize comparing values. 940 | * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. 941 | * @param {Array} [stackA=[]] Tracks traversed `a` objects. 942 | * @param {Array} [stackB=[]] Tracks traversed `b` objects. 943 | * @returns {boolean} Returns `true` if the values are equivalent, else `false`. 944 | */ 945 | function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { 946 | // used to indicate that when comparing objects, `a` has at least the properties of `b` 947 | if (callback) { 948 | var result = callback(a, b); 949 | if (typeof result != 'undefined') { 950 | return !!result; 951 | } 952 | } 953 | // exit early for identical values 954 | if (a === b) { 955 | // treat `+0` vs. `-0` as not equal 956 | return a !== 0 || (1 / a == 1 / b); 957 | } 958 | var type = typeof a, 959 | otherType = typeof b; 960 | 961 | // exit early for unlike primitive values 962 | if (a === a && 963 | !(a && objectTypes[type]) && 964 | !(b && objectTypes[otherType])) { 965 | return false; 966 | } 967 | // exit early for `null` and `undefined` avoiding ES3's Function#call behavior 968 | // http://es5.github.io/#x15.3.4.4 969 | if (a == null || b == null) { 970 | return a === b; 971 | } 972 | // compare [[Class]] names 973 | var className = toString.call(a), 974 | otherClass = toString.call(b); 975 | 976 | if (className == argsClass) { 977 | className = objectClass; 978 | } 979 | if (otherClass == argsClass) { 980 | otherClass = objectClass; 981 | } 982 | if (className != otherClass) { 983 | return false; 984 | } 985 | switch (className) { 986 | case boolClass: 987 | case dateClass: 988 | // coerce dates and booleans to numbers, dates to milliseconds and booleans 989 | // to `1` or `0` treating invalid dates coerced to `NaN` as not equal 990 | return +a == +b; 991 | 992 | case numberClass: 993 | // treat `NaN` vs. `NaN` as equal 994 | return (a != +a) 995 | ? b != +b 996 | // but treat `+0` vs. `-0` as not equal 997 | : (a == 0 ? (1 / a == 1 / b) : a == +b); 998 | 999 | case regexpClass: 1000 | case stringClass: 1001 | // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) 1002 | // treat string primitives and their corresponding object instances as equal 1003 | return a == String(b); 1004 | } 1005 | var isArr = className == arrayClass; 1006 | if (!isArr) { 1007 | // unwrap any `lodash` wrapped values 1008 | var aWrapped = hasOwnProperty.call(a, '__wrapped__'), 1009 | bWrapped = hasOwnProperty.call(b, '__wrapped__'); 1010 | 1011 | if (aWrapped || bWrapped) { 1012 | return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); 1013 | } 1014 | // exit for functions and DOM nodes 1015 | if (className != objectClass || (!support.nodeClass && (isNode(a) || isNode(b)))) { 1016 | return false; 1017 | } 1018 | // in older versions of Opera, `arguments` objects have `Array` constructors 1019 | var ctorA = !support.argsObject && isArguments(a) ? Object : a.constructor, 1020 | ctorB = !support.argsObject && isArguments(b) ? Object : b.constructor; 1021 | 1022 | // non `Object` object instances with different constructors are not equal 1023 | if (ctorA != ctorB && 1024 | !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && 1025 | ('constructor' in a && 'constructor' in b) 1026 | ) { 1027 | return false; 1028 | } 1029 | } 1030 | // assume cyclic structures are equal 1031 | // the algorithm for detecting cyclic structures is adapted from ES 5.1 1032 | // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) 1033 | var initedStack = !stackA; 1034 | stackA || (stackA = getArray()); 1035 | stackB || (stackB = getArray()); 1036 | 1037 | var length = stackA.length; 1038 | while (length--) { 1039 | if (stackA[length] == a) { 1040 | return stackB[length] == b; 1041 | } 1042 | } 1043 | var size = 0; 1044 | result = true; 1045 | 1046 | // add `a` and `b` to the stack of traversed objects 1047 | stackA.push(a); 1048 | stackB.push(b); 1049 | 1050 | // recursively compare objects and arrays (susceptible to call stack limits) 1051 | if (isArr) { 1052 | // compare lengths to determine if a deep comparison is necessary 1053 | length = a.length; 1054 | size = b.length; 1055 | result = size == length; 1056 | 1057 | if (result || isWhere) { 1058 | // deep compare the contents, ignoring non-numeric properties 1059 | while (size--) { 1060 | var index = length, 1061 | value = b[size]; 1062 | 1063 | if (isWhere) { 1064 | while (index--) { 1065 | if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { 1066 | break; 1067 | } 1068 | } 1069 | } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { 1070 | break; 1071 | } 1072 | } 1073 | } 1074 | } 1075 | else { 1076 | // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` 1077 | // which, in this case, is more costly 1078 | forIn(b, function(value, key, b) { 1079 | if (hasOwnProperty.call(b, key)) { 1080 | // count the number of properties. 1081 | size++; 1082 | // deep compare each property value. 1083 | return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); 1084 | } 1085 | }); 1086 | 1087 | if (result && !isWhere) { 1088 | // ensure both objects have the same number of properties 1089 | forIn(a, function(value, key, a) { 1090 | if (hasOwnProperty.call(a, key)) { 1091 | // `size` will be `-1` if `a` has more properties than `b` 1092 | return (result = --size > -1); 1093 | } 1094 | }); 1095 | } 1096 | } 1097 | stackA.pop(); 1098 | stackB.pop(); 1099 | 1100 | if (initedStack) { 1101 | releaseArray(stackA); 1102 | releaseArray(stackB); 1103 | } 1104 | return result; 1105 | } 1106 | 1107 | /** 1108 | * Creates a function that, when called, either curries or invokes `func` 1109 | * with an optional `this` binding and partially applied arguments. 1110 | * 1111 | * @private 1112 | * @param {Function|string} func The function or method name to reference. 1113 | * @param {number} bitmask The bitmask of method flags to compose. 1114 | * The bitmask may be composed of the following flags: 1115 | * 1 - `_.bind` 1116 | * 2 - `_.bindKey` 1117 | * 4 - `_.curry` 1118 | * 8 - `_.curry` (bound) 1119 | * 16 - `_.partial` 1120 | * 32 - `_.partialRight` 1121 | * @param {Array} [partialArgs] An array of arguments to prepend to those 1122 | * provided to the new function. 1123 | * @param {Array} [partialRightArgs] An array of arguments to append to those 1124 | * provided to the new function. 1125 | * @param {*} [thisArg] The `this` binding of `func`. 1126 | * @param {number} [arity] The arity of `func`. 1127 | * @returns {Function} Returns the new function. 1128 | */ 1129 | function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { 1130 | var isBind = bitmask & 1, 1131 | isBindKey = bitmask & 2, 1132 | isCurry = bitmask & 4, 1133 | isCurryBound = bitmask & 8, 1134 | isPartial = bitmask & 16, 1135 | isPartialRight = bitmask & 32; 1136 | 1137 | if (!isBindKey && !isFunction(func)) { 1138 | throw new TypeError; 1139 | } 1140 | if (isPartial && !partialArgs.length) { 1141 | bitmask &= ~16; 1142 | isPartial = partialArgs = false; 1143 | } 1144 | if (isPartialRight && !partialRightArgs.length) { 1145 | bitmask &= ~32; 1146 | isPartialRight = partialRightArgs = false; 1147 | } 1148 | var bindData = func && func.__bindData__; 1149 | if (bindData && bindData !== true) { 1150 | // clone `bindData` 1151 | bindData = slice(bindData); 1152 | if (bindData[2]) { 1153 | bindData[2] = slice(bindData[2]); 1154 | } 1155 | if (bindData[3]) { 1156 | bindData[3] = slice(bindData[3]); 1157 | } 1158 | // set `thisBinding` is not previously bound 1159 | if (isBind && !(bindData[1] & 1)) { 1160 | bindData[4] = thisArg; 1161 | } 1162 | // set if previously bound but not currently (subsequent curried functions) 1163 | if (!isBind && bindData[1] & 1) { 1164 | bitmask |= 8; 1165 | } 1166 | // set curried arity if not yet set 1167 | if (isCurry && !(bindData[1] & 4)) { 1168 | bindData[5] = arity; 1169 | } 1170 | // append partial left arguments 1171 | if (isPartial) { 1172 | push.apply(bindData[2] || (bindData[2] = []), partialArgs); 1173 | } 1174 | // append partial right arguments 1175 | if (isPartialRight) { 1176 | unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); 1177 | } 1178 | // merge flags 1179 | bindData[1] |= bitmask; 1180 | return createWrapper.apply(null, bindData); 1181 | } 1182 | // fast path for `_.bind` 1183 | var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; 1184 | return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); 1185 | } 1186 | 1187 | /** 1188 | * Creates compiled iteration functions. 1189 | * 1190 | * @private 1191 | * @param {...Object} [options] The compile options object(s). 1192 | * @param {string} [options.array] Code to determine if the iterable is an array or array-like. 1193 | * @param {boolean} [options.useHas] Specify using `hasOwnProperty` checks in the object loop. 1194 | * @param {Function} [options.keys] A reference to `_.keys` for use in own property iteration. 1195 | * @param {string} [options.args] A comma separated string of iteration function arguments. 1196 | * @param {string} [options.top] Code to execute before the iteration branches. 1197 | * @param {string} [options.loop] Code to execute in the object loop. 1198 | * @param {string} [options.bottom] Code to execute after the iteration branches. 1199 | * @returns {Function} Returns the compiled function. 1200 | */ 1201 | function createIterator() { 1202 | // data properties 1203 | iteratorData.shadowedProps = shadowedProps; 1204 | 1205 | // iterator options 1206 | iteratorData.array = iteratorData.bottom = iteratorData.loop = iteratorData.top = ''; 1207 | iteratorData.init = 'iterable'; 1208 | iteratorData.useHas = true; 1209 | 1210 | // merge options into a template data object 1211 | for (var object, index = 0; object = arguments[index]; index++) { 1212 | for (var key in object) { 1213 | iteratorData[key] = object[key]; 1214 | } 1215 | } 1216 | var args = iteratorData.args; 1217 | iteratorData.firstArg = /^[^,]+/.exec(args)[0]; 1218 | 1219 | // create the function factory 1220 | var factory = Function( 1221 | 'baseCreateCallback, errorClass, errorProto, hasOwnProperty, ' + 1222 | 'indicatorObject, isArguments, isArray, isString, keys, objectProto, ' + 1223 | 'objectTypes, nonEnumProps, stringClass, stringProto, toString', 1224 | 'return function(' + args + ') {\n' + iteratorTemplate(iteratorData) + '\n}' 1225 | ); 1226 | 1227 | // return the compiled function 1228 | return factory( 1229 | baseCreateCallback, errorClass, errorProto, hasOwnProperty, 1230 | indicatorObject, isArguments, isArray, isString, iteratorData.keys, objectProto, 1231 | objectTypes, nonEnumProps, stringClass, stringProto, toString 1232 | ); 1233 | } 1234 | 1235 | /** 1236 | * Used by `escape` to convert characters to HTML entities. 1237 | * 1238 | * @private 1239 | * @param {string} match The matched character to escape. 1240 | * @returns {string} Returns the escaped character. 1241 | */ 1242 | function escapeHtmlChar(match) { 1243 | return htmlEscapes[match]; 1244 | } 1245 | 1246 | /** 1247 | * Checks if `value` is a native function. 1248 | * 1249 | * @private 1250 | * @param {*} value The value to check. 1251 | * @returns {boolean} Returns `true` if the `value` is a native function, else `false`. 1252 | */ 1253 | function isNative(value) { 1254 | return typeof value == 'function' && reNative.test(value); 1255 | } 1256 | 1257 | /** 1258 | * Sets `this` binding data on a given function. 1259 | * 1260 | * @private 1261 | * @param {Function} func The function to set data on. 1262 | * @param {Array} value The data array to set. 1263 | */ 1264 | var setBindData = !defineProperty ? noop : function(func, value) { 1265 | descriptor.value = value; 1266 | defineProperty(func, '__bindData__', descriptor); 1267 | }; 1268 | 1269 | /*--------------------------------------------------------------------------*/ 1270 | 1271 | /** 1272 | * Checks if `value` is an `arguments` object. 1273 | * 1274 | * @static 1275 | * @memberOf _ 1276 | * @category Objects 1277 | * @param {*} value The value to check. 1278 | * @returns {boolean} Returns `true` if the `value` is an `arguments` object, else `false`. 1279 | * @example 1280 | * 1281 | * (function() { return _.isArguments(arguments); })(1, 2, 3); 1282 | * // => true 1283 | * 1284 | * _.isArguments([1, 2, 3]); 1285 | * // => false 1286 | */ 1287 | function isArguments(value) { 1288 | return value && typeof value == 'object' && typeof value.length == 'number' && 1289 | toString.call(value) == argsClass || false; 1290 | } 1291 | // fallback for browsers that can't detect `arguments` objects by [[Class]] 1292 | if (!support.argsClass) { 1293 | isArguments = function(value) { 1294 | return value && typeof value == 'object' && typeof value.length == 'number' && 1295 | hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee') || false; 1296 | }; 1297 | } 1298 | 1299 | /** 1300 | * Checks if `value` is an array. 1301 | * 1302 | * @static 1303 | * @memberOf _ 1304 | * @type Function 1305 | * @category Objects 1306 | * @param {*} value The value to check. 1307 | * @returns {boolean} Returns `true` if the `value` is an array, else `false`. 1308 | * @example 1309 | * 1310 | * (function() { return _.isArray(arguments); })(); 1311 | * // => false 1312 | * 1313 | * _.isArray([1, 2, 3]); 1314 | * // => true 1315 | */ 1316 | var isArray = nativeIsArray || function(value) { 1317 | return value && typeof value == 'object' && typeof value.length == 'number' && 1318 | toString.call(value) == arrayClass || false; 1319 | }; 1320 | 1321 | /** 1322 | * A fallback implementation of `Object.keys` which produces an array of the 1323 | * given object's own enumerable property names. 1324 | * 1325 | * @private 1326 | * @type Function 1327 | * @param {Object} object The object to inspect. 1328 | * @returns {Array} Returns an array of property names. 1329 | */ 1330 | var shimKeys = createIterator({ 1331 | 'args': 'object', 1332 | 'init': '[]', 1333 | 'top': 'if (!(objectTypes[typeof object])) return result', 1334 | 'loop': 'result.push(index)' 1335 | }); 1336 | 1337 | /** 1338 | * Creates an array composed of the own enumerable property names of an object. 1339 | * 1340 | * @static 1341 | * @memberOf _ 1342 | * @category Objects 1343 | * @param {Object} object The object to inspect. 1344 | * @returns {Array} Returns an array of property names. 1345 | * @example 1346 | * 1347 | * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); 1348 | * // => ['one', 'two', 'three'] (property order is not guaranteed across environments) 1349 | */ 1350 | var keys = !nativeKeys ? shimKeys : function(object) { 1351 | if (!isObject(object)) { 1352 | return []; 1353 | } 1354 | if ((support.enumPrototypes && typeof object == 'function') || 1355 | (support.nonEnumArgs && object.length && isArguments(object))) { 1356 | return shimKeys(object); 1357 | } 1358 | return nativeKeys(object); 1359 | }; 1360 | 1361 | /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */ 1362 | var eachIteratorOptions = { 1363 | 'args': 'collection, callback, thisArg', 1364 | 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)", 1365 | 'array': "typeof length == 'number'", 1366 | 'keys': keys, 1367 | 'loop': 'if (callback(iterable[index], index, collection) === false) return result' 1368 | }; 1369 | 1370 | /** Reusable iterator options for `assign` and `defaults` */ 1371 | var defaultsIteratorOptions = { 1372 | 'args': 'object, source, guard', 1373 | 'top': 1374 | 'var args = arguments,\n' + 1375 | ' argsIndex = 0,\n' + 1376 | " argsLength = typeof guard == 'number' ? 2 : args.length;\n" + 1377 | 'while (++argsIndex < argsLength) {\n' + 1378 | ' iterable = args[argsIndex];\n' + 1379 | ' if (iterable && objectTypes[typeof iterable]) {', 1380 | 'keys': keys, 1381 | 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]", 1382 | 'bottom': ' }\n}' 1383 | }; 1384 | 1385 | /** Reusable iterator options for `forIn` and `forOwn` */ 1386 | var forOwnIteratorOptions = { 1387 | 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top, 1388 | 'array': false 1389 | }; 1390 | 1391 | /** 1392 | * Used to convert characters to HTML entities: 1393 | * 1394 | * Though the `>` character is escaped for symmetry, characters like `>` and `/` 1395 | * don't require escaping in HTML and have no special meaning unless they're part 1396 | * of a tag or an unquoted attribute value. 1397 | * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") 1398 | */ 1399 | var htmlEscapes = { 1400 | '&': '&', 1401 | '<': '<', 1402 | '>': '>', 1403 | '"': '"', 1404 | "'": ''' 1405 | }; 1406 | 1407 | /** Used to match HTML entities and HTML characters */ 1408 | var reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); 1409 | 1410 | /** 1411 | * A function compiled to iterate `arguments` objects, arrays, objects, and 1412 | * strings consistenly across environments, executing the callback for each 1413 | * element in the collection. The callback is bound to `thisArg` and invoked 1414 | * with three arguments; (value, index|key, collection). Callbacks may exit 1415 | * iteration early by explicitly returning `false`. 1416 | * 1417 | * @private 1418 | * @type Function 1419 | * @param {Array|Object|string} collection The collection to iterate over. 1420 | * @param {Function} [callback=identity] The function called per iteration. 1421 | * @param {*} [thisArg] The `this` binding of `callback`. 1422 | * @returns {Array|Object|string} Returns `collection`. 1423 | */ 1424 | var baseEach = createIterator(eachIteratorOptions); 1425 | 1426 | /*--------------------------------------------------------------------------*/ 1427 | 1428 | /** 1429 | * Assigns own enumerable properties of source object(s) to the destination 1430 | * object. Subsequent sources will overwrite property assignments of previous 1431 | * sources. If a callback is provided it will be executed to produce the 1432 | * assigned values. The callback is bound to `thisArg` and invoked with two 1433 | * arguments; (objectValue, sourceValue). 1434 | * 1435 | * @static 1436 | * @memberOf _ 1437 | * @type Function 1438 | * @alias extend 1439 | * @category Objects 1440 | * @param {Object} object The destination object. 1441 | * @param {...Object} [source] The source objects. 1442 | * @param {Function} [callback] The function to customize assigning values. 1443 | * @param {*} [thisArg] The `this` binding of `callback`. 1444 | * @returns {Object} Returns the destination object. 1445 | * @example 1446 | * 1447 | * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); 1448 | * // => { 'name': 'fred', 'employer': 'slate' } 1449 | * 1450 | * var defaults = _.partialRight(_.assign, function(a, b) { 1451 | * return typeof a == 'undefined' ? b : a; 1452 | * }); 1453 | * 1454 | * var object = { 'name': 'barney' }; 1455 | * defaults(object, { 'name': 'fred', 'employer': 'slate' }); 1456 | * // => { 'name': 'barney', 'employer': 'slate' } 1457 | */ 1458 | var assign = createIterator(defaultsIteratorOptions, { 1459 | 'top': 1460 | defaultsIteratorOptions.top.replace(';', 1461 | ';\n' + 1462 | "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" + 1463 | ' var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n' + 1464 | "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" + 1465 | ' callback = args[--argsLength];\n' + 1466 | '}' 1467 | ), 1468 | 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]' 1469 | }); 1470 | 1471 | /** 1472 | * Creates a clone of `value`. If `isDeep` is `true` nested objects will also 1473 | * be cloned, otherwise they will be assigned by reference. If a callback 1474 | * is provided it will be executed to produce the cloned values. If the 1475 | * callback returns `undefined` cloning will be handled by the method instead. 1476 | * The callback is bound to `thisArg` and invoked with one argument; (value). 1477 | * 1478 | * @static 1479 | * @memberOf _ 1480 | * @category Objects 1481 | * @param {*} value The value to clone. 1482 | * @param {boolean} [isDeep=false] Specify a deep clone. 1483 | * @param {Function} [callback] The function to customize cloning values. 1484 | * @param {*} [thisArg] The `this` binding of `callback`. 1485 | * @returns {*} Returns the cloned value. 1486 | * @example 1487 | * 1488 | * var characters = [ 1489 | * { 'name': 'barney', 'age': 36 }, 1490 | * { 'name': 'fred', 'age': 40 } 1491 | * ]; 1492 | * 1493 | * var shallow = _.clone(characters); 1494 | * shallow[0] === characters[0]; 1495 | * // => true 1496 | * 1497 | * var deep = _.clone(characters, true); 1498 | * deep[0] === characters[0]; 1499 | * // => false 1500 | * 1501 | * _.mixin({ 1502 | * 'clone': _.partialRight(_.clone, function(value) { 1503 | * return _.isElement(value) ? value.cloneNode(false) : undefined; 1504 | * }) 1505 | * }); 1506 | * 1507 | * var clone = _.clone(document.body); 1508 | * clone.childNodes.length; 1509 | * // => 0 1510 | */ 1511 | function clone(value, isDeep, callback, thisArg) { 1512 | // allows working with "Collections" methods without using their `index` 1513 | // and `collection` arguments for `isDeep` and `callback` 1514 | if (typeof isDeep != 'boolean' && isDeep != null) { 1515 | thisArg = callback; 1516 | callback = isDeep; 1517 | isDeep = false; 1518 | } 1519 | return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); 1520 | } 1521 | 1522 | /** 1523 | * Creates a deep clone of `value`. If a callback is provided it will be 1524 | * executed to produce the cloned values. If the callback returns `undefined` 1525 | * cloning will be handled by the method instead. The callback is bound to 1526 | * `thisArg` and invoked with one argument; (value). 1527 | * 1528 | * Note: This method is loosely based on the structured clone algorithm. Functions 1529 | * and DOM nodes are **not** cloned. The enumerable properties of `arguments` objects and 1530 | * objects created by constructors other than `Object` are cloned to plain `Object` objects. 1531 | * See http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm. 1532 | * 1533 | * @static 1534 | * @memberOf _ 1535 | * @category Objects 1536 | * @param {*} value The value to deep clone. 1537 | * @param {Function} [callback] The function to customize cloning values. 1538 | * @param {*} [thisArg] The `this` binding of `callback`. 1539 | * @returns {*} Returns the deep cloned value. 1540 | * @example 1541 | * 1542 | * var characters = [ 1543 | * { 'name': 'barney', 'age': 36 }, 1544 | * { 'name': 'fred', 'age': 40 } 1545 | * ]; 1546 | * 1547 | * var deep = _.cloneDeep(characters); 1548 | * deep[0] === characters[0]; 1549 | * // => false 1550 | * 1551 | * var view = { 1552 | * 'label': 'docs', 1553 | * 'node': element 1554 | * }; 1555 | * 1556 | * var clone = _.cloneDeep(view, function(value) { 1557 | * return _.isElement(value) ? value.cloneNode(true) : undefined; 1558 | * }); 1559 | * 1560 | * clone.node == view.node; 1561 | * // => false 1562 | */ 1563 | function cloneDeep(value, callback, thisArg) { 1564 | return baseClone(value, true, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); 1565 | } 1566 | 1567 | /** 1568 | * Assigns own enumerable properties of source object(s) to the destination 1569 | * object for all destination properties that resolve to `undefined`. Once a 1570 | * property is set, additional defaults of the same property will be ignored. 1571 | * 1572 | * @static 1573 | * @memberOf _ 1574 | * @type Function 1575 | * @category Objects 1576 | * @param {Object} object The destination object. 1577 | * @param {...Object} [source] The source objects. 1578 | * @param- {Object} [guard] Allows working with `_.reduce` without using its 1579 | * `key` and `object` arguments as sources. 1580 | * @returns {Object} Returns the destination object. 1581 | * @example 1582 | * 1583 | * var object = { 'name': 'barney' }; 1584 | * _.defaults(object, { 'name': 'fred', 'employer': 'slate' }); 1585 | * // => { 'name': 'barney', 'employer': 'slate' } 1586 | */ 1587 | var defaults = createIterator(defaultsIteratorOptions); 1588 | 1589 | /** 1590 | * Iterates over own and inherited enumerable properties of an object, 1591 | * executing the callback for each property. The callback is bound to `thisArg` 1592 | * and invoked with three arguments; (value, key, object). Callbacks may exit 1593 | * iteration early by explicitly returning `false`. 1594 | * 1595 | * @static 1596 | * @memberOf _ 1597 | * @type Function 1598 | * @category Objects 1599 | * @param {Object} object The object to iterate over. 1600 | * @param {Function} [callback=identity] The function called per iteration. 1601 | * @param {*} [thisArg] The `this` binding of `callback`. 1602 | * @returns {Object} Returns `object`. 1603 | * @example 1604 | * 1605 | * function Shape() { 1606 | * this.x = 0; 1607 | * this.y = 0; 1608 | * } 1609 | * 1610 | * Shape.prototype.move = function(x, y) { 1611 | * this.x += x; 1612 | * this.y += y; 1613 | * }; 1614 | * 1615 | * _.forIn(new Shape, function(value, key) { 1616 | * console.log(key); 1617 | * }); 1618 | * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) 1619 | */ 1620 | var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, { 1621 | 'useHas': false 1622 | }); 1623 | 1624 | /** 1625 | * Iterates over own enumerable properties of an object, executing the callback 1626 | * for each property. The callback is bound to `thisArg` and invoked with three 1627 | * arguments; (value, key, object). Callbacks may exit iteration early by 1628 | * explicitly returning `false`. 1629 | * 1630 | * @static 1631 | * @memberOf _ 1632 | * @type Function 1633 | * @category Objects 1634 | * @param {Object} object The object to iterate over. 1635 | * @param {Function} [callback=identity] The function called per iteration. 1636 | * @param {*} [thisArg] The `this` binding of `callback`. 1637 | * @returns {Object} Returns `object`. 1638 | * @example 1639 | * 1640 | * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { 1641 | * console.log(key); 1642 | * }); 1643 | * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) 1644 | */ 1645 | var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions); 1646 | 1647 | /** 1648 | * Checks if `value` is a function. 1649 | * 1650 | * @static 1651 | * @memberOf _ 1652 | * @category Objects 1653 | * @param {*} value The value to check. 1654 | * @returns {boolean} Returns `true` if the `value` is a function, else `false`. 1655 | * @example 1656 | * 1657 | * _.isFunction(_); 1658 | * // => true 1659 | */ 1660 | function isFunction(value) { 1661 | return typeof value == 'function'; 1662 | } 1663 | // fallback for older versions of Chrome and Safari 1664 | if (isFunction(/x/)) { 1665 | isFunction = function(value) { 1666 | return typeof value == 'function' && toString.call(value) == funcClass; 1667 | }; 1668 | } 1669 | 1670 | /** 1671 | * Checks if `value` is the language type of Object. 1672 | * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 1673 | * 1674 | * @static 1675 | * @memberOf _ 1676 | * @category Objects 1677 | * @param {*} value The value to check. 1678 | * @returns {boolean} Returns `true` if the `value` is an object, else `false`. 1679 | * @example 1680 | * 1681 | * _.isObject({}); 1682 | * // => true 1683 | * 1684 | * _.isObject([1, 2, 3]); 1685 | * // => true 1686 | * 1687 | * _.isObject(1); 1688 | * // => false 1689 | */ 1690 | function isObject(value) { 1691 | // check if the value is the ECMAScript language type of Object 1692 | // http://es5.github.io/#x8 1693 | // and avoid a V8 bug 1694 | // http://code.google.com/p/v8/issues/detail?id=2291 1695 | return !!(value && objectTypes[typeof value]); 1696 | } 1697 | 1698 | /** 1699 | * Checks if `value` is a string. 1700 | * 1701 | * @static 1702 | * @memberOf _ 1703 | * @category Objects 1704 | * @param {*} value The value to check. 1705 | * @returns {boolean} Returns `true` if the `value` is a string, else `false`. 1706 | * @example 1707 | * 1708 | * _.isString('fred'); 1709 | * // => true 1710 | */ 1711 | function isString(value) { 1712 | return typeof value == 'string' || 1713 | value && typeof value == 'object' && toString.call(value) == stringClass || false; 1714 | } 1715 | 1716 | /** 1717 | * Creates an array composed of the own enumerable property values of `object`. 1718 | * 1719 | * @static 1720 | * @memberOf _ 1721 | * @category Objects 1722 | * @param {Object} object The object to inspect. 1723 | * @returns {Array} Returns an array of property values. 1724 | * @example 1725 | * 1726 | * _.values({ 'one': 1, 'two': 2, 'three': 3 }); 1727 | * // => [1, 2, 3] (property order is not guaranteed across environments) 1728 | */ 1729 | function values(object) { 1730 | var index = -1, 1731 | props = keys(object), 1732 | length = props.length, 1733 | result = Array(length); 1734 | 1735 | while (++index < length) { 1736 | result[index] = object[props[index]]; 1737 | } 1738 | return result; 1739 | } 1740 | 1741 | /*--------------------------------------------------------------------------*/ 1742 | 1743 | /** 1744 | * Iterates over elements of a collection, returning an array of all elements 1745 | * the callback returns truey for. The callback is bound to `thisArg` and 1746 | * invoked with three arguments; (value, index|key, collection). 1747 | * 1748 | * If a property name is provided for `callback` the created "_.pluck" style 1749 | * callback will return the property value of the given element. 1750 | * 1751 | * If an object is provided for `callback` the created "_.where" style callback 1752 | * will return `true` for elements that have the properties of the given object, 1753 | * else `false`. 1754 | * 1755 | * @static 1756 | * @memberOf _ 1757 | * @alias select 1758 | * @category Collections 1759 | * @param {Array|Object|string} collection The collection to iterate over. 1760 | * @param {Function|Object|string} [callback=identity] The function called 1761 | * per iteration. If a property name or object is provided it will be used 1762 | * to create a "_.pluck" or "_.where" style callback, respectively. 1763 | * @param {*} [thisArg] The `this` binding of `callback`. 1764 | * @returns {Array} Returns a new array of elements that passed the callback check. 1765 | * @example 1766 | * 1767 | * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); 1768 | * // => [2, 4, 6] 1769 | * 1770 | * var characters = [ 1771 | * { 'name': 'barney', 'age': 36, 'blocked': false }, 1772 | * { 'name': 'fred', 'age': 40, 'blocked': true } 1773 | * ]; 1774 | * 1775 | * // using "_.pluck" callback shorthand 1776 | * _.filter(characters, 'blocked'); 1777 | * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] 1778 | * 1779 | * // using "_.where" callback shorthand 1780 | * _.filter(characters, { 'age': 36 }); 1781 | * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] 1782 | */ 1783 | function filter(collection, callback, thisArg) { 1784 | var result = []; 1785 | callback = lodash.createCallback(callback, thisArg, 3); 1786 | 1787 | if (isArray(collection)) { 1788 | var index = -1, 1789 | length = collection.length; 1790 | 1791 | while (++index < length) { 1792 | var value = collection[index]; 1793 | if (callback(value, index, collection)) { 1794 | result.push(value); 1795 | } 1796 | } 1797 | } else { 1798 | baseEach(collection, function(value, index, collection) { 1799 | if (callback(value, index, collection)) { 1800 | result.push(value); 1801 | } 1802 | }); 1803 | } 1804 | return result; 1805 | } 1806 | 1807 | /** 1808 | * Iterates over elements of a collection, returning the first element that 1809 | * the callback returns truey for. The callback is bound to `thisArg` and 1810 | * invoked with three arguments; (value, index|key, collection). 1811 | * 1812 | * If a property name is provided for `callback` the created "_.pluck" style 1813 | * callback will return the property value of the given element. 1814 | * 1815 | * If an object is provided for `callback` the created "_.where" style callback 1816 | * will return `true` for elements that have the properties of the given object, 1817 | * else `false`. 1818 | * 1819 | * @static 1820 | * @memberOf _ 1821 | * @alias detect, findWhere 1822 | * @category Collections 1823 | * @param {Array|Object|string} collection The collection to iterate over. 1824 | * @param {Function|Object|string} [callback=identity] The function called 1825 | * per iteration. If a property name or object is provided it will be used 1826 | * to create a "_.pluck" or "_.where" style callback, respectively. 1827 | * @param {*} [thisArg] The `this` binding of `callback`. 1828 | * @returns {*} Returns the found element, else `undefined`. 1829 | * @example 1830 | * 1831 | * var characters = [ 1832 | * { 'name': 'barney', 'age': 36, 'blocked': false }, 1833 | * { 'name': 'fred', 'age': 40, 'blocked': true }, 1834 | * { 'name': 'pebbles', 'age': 1, 'blocked': false } 1835 | * ]; 1836 | * 1837 | * _.find(characters, function(chr) { 1838 | * return chr.age < 40; 1839 | * }); 1840 | * // => { 'name': 'barney', 'age': 36, 'blocked': false } 1841 | * 1842 | * // using "_.where" callback shorthand 1843 | * _.find(characters, { 'age': 1 }); 1844 | * // => { 'name': 'pebbles', 'age': 1, 'blocked': false } 1845 | * 1846 | * // using "_.pluck" callback shorthand 1847 | * _.find(characters, 'blocked'); 1848 | * // => { 'name': 'fred', 'age': 40, 'blocked': true } 1849 | */ 1850 | function find(collection, callback, thisArg) { 1851 | callback = lodash.createCallback(callback, thisArg, 3); 1852 | 1853 | if (isArray(collection)) { 1854 | var index = -1, 1855 | length = collection.length; 1856 | 1857 | while (++index < length) { 1858 | var value = collection[index]; 1859 | if (callback(value, index, collection)) { 1860 | return value; 1861 | } 1862 | } 1863 | } else { 1864 | var result; 1865 | baseEach(collection, function(value, index, collection) { 1866 | if (callback(value, index, collection)) { 1867 | result = value; 1868 | return false; 1869 | } 1870 | }); 1871 | return result; 1872 | } 1873 | } 1874 | 1875 | /** 1876 | * Iterates over elements of a collection, executing the callback for each 1877 | * element. The callback is bound to `thisArg` and invoked with three arguments; 1878 | * (value, index|key, collection). Callbacks may exit iteration early by 1879 | * explicitly returning `false`. 1880 | * 1881 | * Note: As with other "Collections" methods, objects with a `length` property 1882 | * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` 1883 | * may be used for object iteration. 1884 | * 1885 | * @static 1886 | * @memberOf _ 1887 | * @alias each 1888 | * @category Collections 1889 | * @param {Array|Object|string} collection The collection to iterate over. 1890 | * @param {Function} [callback=identity] The function called per iteration. 1891 | * @param {*} [thisArg] The `this` binding of `callback`. 1892 | * @returns {Array|Object|string} Returns `collection`. 1893 | * @example 1894 | * 1895 | * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); 1896 | * // => logs each number and returns '1,2,3' 1897 | * 1898 | * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); 1899 | * // => logs each number and returns the object (property order is not guaranteed across environments) 1900 | */ 1901 | function forEach(collection, callback, thisArg) { 1902 | if (callback && typeof thisArg == 'undefined' && isArray(collection)) { 1903 | var index = -1, 1904 | length = collection.length; 1905 | 1906 | while (++index < length) { 1907 | if (callback(collection[index], index, collection) === false) { 1908 | break; 1909 | } 1910 | } 1911 | } else { 1912 | baseEach(collection, callback, thisArg); 1913 | } 1914 | return collection; 1915 | } 1916 | 1917 | /** 1918 | * The opposite of `_.filter` this method returns the elements of a 1919 | * collection that the callback does **not** return truey for. 1920 | * 1921 | * If a property name is provided for `callback` the created "_.pluck" style 1922 | * callback will return the property value of the given element. 1923 | * 1924 | * If an object is provided for `callback` the created "_.where" style callback 1925 | * will return `true` for elements that have the properties of the given object, 1926 | * else `false`. 1927 | * 1928 | * @static 1929 | * @memberOf _ 1930 | * @category Collections 1931 | * @param {Array|Object|string} collection The collection to iterate over. 1932 | * @param {Function|Object|string} [callback=identity] The function called 1933 | * per iteration. If a property name or object is provided it will be used 1934 | * to create a "_.pluck" or "_.where" style callback, respectively. 1935 | * @param {*} [thisArg] The `this` binding of `callback`. 1936 | * @returns {Array} Returns a new array of elements that failed the callback check. 1937 | * @example 1938 | * 1939 | * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); 1940 | * // => [1, 3, 5] 1941 | * 1942 | * var characters = [ 1943 | * { 'name': 'barney', 'age': 36, 'blocked': false }, 1944 | * { 'name': 'fred', 'age': 40, 'blocked': true } 1945 | * ]; 1946 | * 1947 | * // using "_.pluck" callback shorthand 1948 | * _.reject(characters, 'blocked'); 1949 | * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] 1950 | * 1951 | * // using "_.where" callback shorthand 1952 | * _.reject(characters, { 'age': 36 }); 1953 | * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] 1954 | */ 1955 | function reject(collection, callback, thisArg) { 1956 | callback = lodash.createCallback(callback, thisArg, 3); 1957 | return filter(collection, function(value, index, collection) { 1958 | return !callback(value, index, collection); 1959 | }); 1960 | } 1961 | 1962 | /*--------------------------------------------------------------------------*/ 1963 | 1964 | /** 1965 | * Creates a function that, when called, invokes `func` with the `this` 1966 | * binding of `thisArg` and prepends any additional `bind` arguments to those 1967 | * provided to the bound function. 1968 | * 1969 | * @static 1970 | * @memberOf _ 1971 | * @category Functions 1972 | * @param {Function} func The function to bind. 1973 | * @param {*} [thisArg] The `this` binding of `func`. 1974 | * @param {...*} [arg] Arguments to be partially applied. 1975 | * @returns {Function} Returns the new bound function. 1976 | * @example 1977 | * 1978 | * var func = function(greeting) { 1979 | * return greeting + ' ' + this.name; 1980 | * }; 1981 | * 1982 | * func = _.bind(func, { 'name': 'fred' }, 'hi'); 1983 | * func(); 1984 | * // => 'hi fred' 1985 | */ 1986 | function bind(func, thisArg) { 1987 | return arguments.length > 2 1988 | ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) 1989 | : createWrapper(func, 1, null, null, thisArg); 1990 | } 1991 | 1992 | /*--------------------------------------------------------------------------*/ 1993 | 1994 | /** 1995 | * Produces a callback bound to an optional `thisArg`. If `func` is a property 1996 | * name the created callback will return the property value for a given element. 1997 | * If `func` is an object the created callback will return `true` for elements 1998 | * that contain the equivalent object properties, otherwise it will return `false`. 1999 | * 2000 | * @static 2001 | * @memberOf _ 2002 | * @category Utilities 2003 | * @param {*} [func=identity] The value to convert to a callback. 2004 | * @param {*} [thisArg] The `this` binding of the created callback. 2005 | * @param {number} [argCount] The number of arguments the callback accepts. 2006 | * @returns {Function} Returns a callback function. 2007 | * @example 2008 | * 2009 | * var characters = [ 2010 | * { 'name': 'barney', 'age': 36 }, 2011 | * { 'name': 'fred', 'age': 40 } 2012 | * ]; 2013 | * 2014 | * // wrap to create custom callback shorthands 2015 | * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { 2016 | * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); 2017 | * return !match ? func(callback, thisArg) : function(object) { 2018 | * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; 2019 | * }; 2020 | * }); 2021 | * 2022 | * _.filter(characters, 'age__gt38'); 2023 | * // => [{ 'name': 'fred', 'age': 40 }] 2024 | */ 2025 | function createCallback(func, thisArg, argCount) { 2026 | var type = typeof func; 2027 | if (func == null || type == 'function') { 2028 | return baseCreateCallback(func, thisArg, argCount); 2029 | } 2030 | // handle "_.pluck" style callback shorthands 2031 | if (type != 'object') { 2032 | return property(func); 2033 | } 2034 | var props = keys(func), 2035 | key = props[0], 2036 | a = func[key]; 2037 | 2038 | // handle "_.where" style callback shorthands 2039 | if (props.length == 1 && a === a && !isObject(a)) { 2040 | // fast path the common case of providing an object with a single 2041 | // property containing a primitive value 2042 | return function(object) { 2043 | var b = object[key]; 2044 | return a === b && (a !== 0 || (1 / a == 1 / b)); 2045 | }; 2046 | } 2047 | return function(object) { 2048 | var length = props.length, 2049 | result = false; 2050 | 2051 | while (length--) { 2052 | if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { 2053 | break; 2054 | } 2055 | } 2056 | return result; 2057 | }; 2058 | } 2059 | 2060 | /** 2061 | * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their 2062 | * corresponding HTML entities. 2063 | * 2064 | * @static 2065 | * @memberOf _ 2066 | * @category Utilities 2067 | * @param {string} string The string to escape. 2068 | * @returns {string} Returns the escaped string. 2069 | * @example 2070 | * 2071 | * _.escape('Fred, Wilma, & Pebbles'); 2072 | * // => 'Fred, Wilma, & Pebbles' 2073 | */ 2074 | function escape(string) { 2075 | return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); 2076 | } 2077 | 2078 | /** 2079 | * This method returns the first argument provided to it. 2080 | * 2081 | * @static 2082 | * @memberOf _ 2083 | * @category Utilities 2084 | * @param {*} value Any value. 2085 | * @returns {*} Returns `value`. 2086 | * @example 2087 | * 2088 | * var object = { 'name': 'fred' }; 2089 | * _.identity(object) === object; 2090 | * // => true 2091 | */ 2092 | function identity(value) { 2093 | return value; 2094 | } 2095 | 2096 | /** 2097 | * A no-operation function. 2098 | * 2099 | * @static 2100 | * @memberOf _ 2101 | * @category Utilities 2102 | * @example 2103 | * 2104 | * var object = { 'name': 'fred' }; 2105 | * _.noop(object) === undefined; 2106 | * // => true 2107 | */ 2108 | function noop() { 2109 | // no operation performed 2110 | } 2111 | 2112 | /** 2113 | * Creates a "_.pluck" style function, which returns the `key` value of a 2114 | * given object. 2115 | * 2116 | * @static 2117 | * @memberOf _ 2118 | * @category Utilities 2119 | * @param {string} key The name of the property to retrieve. 2120 | * @returns {Function} Returns the new function. 2121 | * @example 2122 | * 2123 | * var characters = [ 2124 | * { 'name': 'fred', 'age': 40 }, 2125 | * { 'name': 'barney', 'age': 36 } 2126 | * ]; 2127 | * 2128 | * var getName = _.property('name'); 2129 | * 2130 | * _.map(characters, getName); 2131 | * // => ['barney', 'fred'] 2132 | * 2133 | * _.sortBy(characters, getName); 2134 | * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] 2135 | */ 2136 | function property(key) { 2137 | return function(object) { 2138 | return object[key]; 2139 | }; 2140 | } 2141 | 2142 | /** 2143 | * A micro-templating method that handles arbitrary delimiters, preserves 2144 | * whitespace, and correctly escapes quotes within interpolated code. 2145 | * 2146 | * Note: In the development build, `_.template` utilizes sourceURLs for easier 2147 | * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl 2148 | * 2149 | * For more information on precompiling templates see: 2150 | * http://lodash.com/custom-builds 2151 | * 2152 | * For more information on Chrome extension sandboxes see: 2153 | * http://developer.chrome.com/stable/extensions/sandboxingEval.html 2154 | * 2155 | * @static 2156 | * @memberOf _ 2157 | * @category Utilities 2158 | * @param {string} text The template text. 2159 | * @param {Object} data The data object used to populate the text. 2160 | * @param {Object} [options] The options object. 2161 | * @param {RegExp} [options.escape] The "escape" delimiter. 2162 | * @param {RegExp} [options.evaluate] The "evaluate" delimiter. 2163 | * @param {Object} [options.imports] An object to import into the template as local variables. 2164 | * @param {RegExp} [options.interpolate] The "interpolate" delimiter. 2165 | * @param {string} [sourceURL] The sourceURL of the template's compiled source. 2166 | * @param {string} [variable] The data object variable name. 2167 | * @returns {Function|string} Returns a compiled function when no `data` object 2168 | * is given, else it returns the interpolated text. 2169 | * @example 2170 | * 2171 | * // using the "interpolate" delimiter to create a compiled template 2172 | * var compiled = _.template('hello <%= name %>'); 2173 | * compiled({ 'name': 'fred' }); 2174 | * // => 'hello fred' 2175 | * 2176 | * // using the "escape" delimiter to escape HTML in data property values 2177 | * _.template('<%- value %>', { 'value': ' 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | -------------------------------------------------------------------------------- /test/test_main.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; 2 | var SideComments = require('side-comments'); 3 | var sideComments; 4 | var fixturesHTML = $('#fixtures').html(); 5 | 6 | /***********/ 7 | /* Helpers * 8 | /***********/ 9 | 10 | var $section1; 11 | var $section2; 12 | var $section3; 13 | 14 | var newTestComment = { 15 | id: 278, 16 | authorId: 1, 17 | authorAvatarUrl: "support/images/user/png", 18 | authorName: "New Test Commenter", 19 | comment: "This is a test comment.", 20 | replies: [] 21 | }; 22 | 23 | function check( done, f ) { 24 | try { 25 | f(); 26 | done(); 27 | } catch( e ) { 28 | done( e ); 29 | } 30 | } 31 | 32 | function setSections() { 33 | $section1 = $('.side-comment').eq(0); 34 | $section2 = $('.side-comment').eq(1); 35 | $section3 = $('.side-comment').eq(2); 36 | } 37 | 38 | function testCommentForSection( sectionNumber, replyFor ) { 39 | var comment = _.clone(newTestComment); 40 | comment.sectionId = sectionNumber; 41 | if ( replyFor ) 42 | comment.parentId = replyFor; 43 | return comment; 44 | } 45 | 46 | function setupSideComments( withCurrentUser ) { 47 | var currentUserToPass = currentUser; 48 | 49 | if (withCurrentUser === undefined || withCurrentUser === true) { 50 | currentUserToPass = currentUser; 51 | } else { 52 | currentUserToPass = null; 53 | } 54 | 55 | if (sideComments) { 56 | sideComments.destroy(); 57 | sideComments = null; 58 | } 59 | $('#fixtures').html(fixturesHTML); 60 | sideComments = new SideComments('#commentable-container', currentUserToPass, existingComments); 61 | } 62 | 63 | function teardownSideComments() { 64 | sideComments.destroy(); 65 | sideComments = null; 66 | $('#fixtures').html(fixturesHTML); 67 | } 68 | 69 | describe("SideComments", function() { 70 | 71 | after(function( done ) { 72 | teardownSideComments(); 73 | done(); 74 | }); 75 | 76 | describe("Constructor", function() { 77 | 78 | beforeEach(function( done ) { 79 | setupSideComments(); 80 | setSections(); 81 | done(); 82 | }); 83 | 84 | it("should have a $el", function() { 85 | expect(sideComments.$el).to.not.be.empty; 86 | }); 87 | 88 | it("should have a $body", function() { 89 | expect(sideComments.$body).to.not.be.empty; 90 | }); 91 | 92 | it("should create the right number of Section objects", function() { 93 | expect(sideComments.sections).to.have.length.of(3); 94 | }); 95 | 96 | }); 97 | 98 | describe("High level display and interactions", function() { 99 | 100 | beforeEach(function( done ) { 101 | setupSideComments(); 102 | setSections(); 103 | done(); 104 | }); 105 | 106 | it("should have the comment sections hidden at start", function() { 107 | expect(sideComments.commentsAreVisible()).to.be.false; 108 | }); 109 | 110 | it("should know if the comment sections are hidden or not", function() { 111 | $('#commentable-container').addClass('side-comments-open'); 112 | expect(sideComments.commentsAreVisible()).to.be.true; 113 | }); 114 | 115 | it("should make comments visible when a marker is clicked", function() { 116 | $('.side-comment').first().find('.marker').trigger('click'); 117 | expect(sideComments.commentsAreVisible()).to.be.true; 118 | }); 119 | 120 | it("should display the comments when a marker is clicked", function() { 121 | var $section = $('.side-comment').first(); 122 | $section.find('.marker').trigger('click'); 123 | expect($section.hasClass('active')).to.be.true; 124 | }); 125 | 126 | it("should hide the previously active section when a different marker is clicked", function() { 127 | var $section = $('.side-comment').first(); 128 | $section.find('.marker').trigger('click'); 129 | 130 | var $nextSection = $('.side-comment').eq(1); 131 | $nextSection.find('.marker').trigger('click'); 132 | 133 | expect($section.hasClass('active')).to.be.false; 134 | }); 135 | 136 | it("should select the new section when a different marker is clicked", function() { 137 | var $section = $('.side-comment').first(); 138 | $section.find('.marker').trigger('click'); 139 | 140 | var $nextSection = $('.side-comment').eq(1); 141 | $nextSection.find('.marker').trigger('click'); 142 | 143 | expect($nextSection.hasClass('active')).to.be.true; 144 | }); 145 | 146 | it("should hide the comments when the active section is clicked", function() { 147 | var $section = $('.side-comment').first(); 148 | $section.find('.marker').trigger('click'); 149 | $section.find('.marker').trigger('click'); 150 | 151 | expect($section.hasClass('active')).to.be.false; 152 | expect(sideComments.commentsAreVisible()).to.be.false; 153 | }); 154 | 155 | it("should hide the comments when the body is clicked", function(){ 156 | $('.side-comment').first().find('.marker').trigger('click'); 157 | $('body p').first().trigger('click'); 158 | expect($('.side-comment').hasClass('active')).to.be.false; 159 | expect(sideComments.commentsAreVisible()).to.be.false; 160 | }); 161 | 162 | }); 163 | 164 | describe("Comments display and interactions", function() { 165 | 166 | beforeEach(function( done ) { 167 | setupSideComments(); 168 | setSections(); 169 | done(); 170 | }); 171 | 172 | it("should render comment markup correctly", function(){ 173 | expect($section1.find('.comments > li').first().children('.author-name').text().trim()).to.equal('Jon Sno'); 174 | }); 175 | 176 | it("should display the right number of comments in the markers for each sections", function(){ 177 | expect($section1.find('.marker span').text()).to.equal('2'); 178 | expect($section2.find('.marker span').text()).to.equal('0'); 179 | expect($section3.find('.marker span').text()).to.equal('1'); 180 | }); 181 | 182 | it("should display the right number of comments in the list for each sections", function(){ 183 | expect($section1.find('.comments > li')).to.have.length.of(2); 184 | expect($section2.find('.comments > li')).to.have.length.of(0); 185 | expect($section3.find('.comments > li')).to.have.length.of(1); 186 | }); 187 | 188 | it("should show the add button when there is one or more comments", function(){ 189 | $section1.find('.marker').trigger('click'); 190 | expect($section1.find('.add-comment').is(':visible')).to.be.true; 191 | }); 192 | 193 | it("should show the comment form when there is not any comments", function(){ 194 | $section2.find('.marker').trigger('click'); 195 | expect($section2.find('.comment-form').is(':visible')).to.be.true; 196 | }); 197 | 198 | it("should hide the add button after it's clicked", function(){ 199 | $section1.find('.marker').trigger('click'); 200 | $section1.find('.add-comment').trigger('click'); 201 | expect($section1.find('.add-comment').is(':visible')).to.be.false; 202 | }); 203 | 204 | it("should hide the comment form when the cancel button is clicked for a section with comments", function(){ 205 | $section1.find('.marker').trigger('click'); 206 | $section1.find('.add-comment').trigger('click'); 207 | $section1.find('.actions .cancel').trigger('click'); 208 | expect($section1.find('.actions').is(':visible')).to.be.false; 209 | }); 210 | 211 | it("should show the add comment button again when the cancel button is clicked for a section with comments", function(){ 212 | $section1.find('.marker').trigger('click'); 213 | $section1.find('.add-comment').trigger('click'); 214 | $section1.find('.actions .cancel').trigger('click'); 215 | expect($section1.find('.add-comment').is(':visible')).to.be.true; 216 | }); 217 | 218 | it("should hide the section the cancel button is clicked for a section without comments", function(){ 219 | $section2.find('.marker').trigger('click'); 220 | $section2.find('.actions .cancel').trigger('click'); 221 | expect($section2.find('.comments').is(':visible')).to.be.false; 222 | }); 223 | 224 | it("should hide the side comments when the cancel button is clicked for a section without comments", function(){ 225 | $section2.find('.marker').trigger('click'); 226 | $section2.find('.actions .cancel').trigger('click'); 227 | expect(sideComments.commentsAreVisible()).to.be.false; 228 | }); 229 | 230 | it("should assign the reply form to the correct comment", function () { 231 | expect($section1.find('.comments > li').first().find('.reply-form').data('parent')).to.equal($section1.find('.comments > li').first().data('comment-id')); 232 | }); 233 | 234 | it("should hide the previous reply form when clicking to reply another comment", function () { 235 | $section1.find('.marker').trigger('click'); 236 | $section1.find('.comments > li').first().find('.reply-comment').trigger('click'); 237 | var $form1 = $section1.find('.reply-form.active'); 238 | 239 | $section1.find('.comments > li').last().find('.reply-comment').trigger('click'); 240 | 241 | expect($form1.is(":visible")).to.be.false; 242 | }); 243 | 244 | it("should hide the add comments form when clicking to reply a comment", function () { 245 | $section1.find('.marker').trigger('click'); 246 | $section1.find('.add-comment').trigger('click'); 247 | 248 | $section1.find('.comments > li').first().find('.reply-comment').trigger('click'); 249 | expect($section1.find('.comment-form').is(':visible')).to.be.false 250 | }); 251 | 252 | it("should display the reply form when the reply button is clicked in a comment", function () { 253 | $section1.find('.marker').trigger('click'); 254 | $section1.find('.comments > li').first().find('.reply-comment').trigger('click'); 255 | expect($section1.find('.comments > li').first().find('.reply-form').is(':visible')).to.be.true; 256 | }); 257 | 258 | it("should not have a link for comments that do not have a authorUrl", function(){ 259 | $section1.find('.marker').trigger('click'); 260 | var $authorName = $section1.find('.author-name').eq(2); 261 | expect($authorName.prop('tagName').toLowerCase()).to.eq('p'); 262 | }); 263 | 264 | it("should have a link for comments that have a authorUrl", function(){ 265 | $section1.find('.marker').trigger('click'); 266 | var $authorName = $section1.find('.author-name').eq(0); 267 | expect($authorName.prop('tagName').toLowerCase()).to.eq('a'); 268 | }); 269 | 270 | }); 271 | 272 | describe("New Comment Posting", function(){ 273 | 274 | beforeEach(function( done ) { 275 | setupSideComments(); 276 | setSections(); 277 | done(); 278 | }); 279 | 280 | it("should emit an event when a comment is posted", function( done ){ 281 | this.timeout(0); 282 | var eventFired = false; 283 | 284 | setTimeout( function () { 285 | check( done, function() { 286 | expect(eventFired).to.be.true; 287 | } ) 288 | }, 500); 289 | 290 | sideComments.on('commentPosted', function( comment ) { 291 | eventFired = true; 292 | }); 293 | 294 | $section1.find('.marker').trigger('click'); 295 | $section1.find('.add-comment').trigger('click'); 296 | $section1.find('.comment-box').html(newTestComment.comment); 297 | $section1.find('.action-link.post').trigger('click'); 298 | }); 299 | 300 | it("should update a non-empty section's comment list length after adding", function(){ 301 | sideComments.insertComment(testCommentForSection(1)); 302 | expect($section1.find('.comments > li')).to.have.length.of(3); 303 | }); 304 | 305 | it("should update a non-empty section's comment count after adding", function(){ 306 | sideComments.insertComment(testCommentForSection(1)); 307 | expect($section1.find('.marker span').text().trim()).to.equal("3"); 308 | }); 309 | 310 | it("should update an empty section's comment list length after adding", function(){ 311 | sideComments.insertComment(testCommentForSection(2)); 312 | expect($section2.find('.comments > li')).to.have.length.of(1); 313 | }); 314 | 315 | it("should update an empty section's comment count after adding", function(){ 316 | sideComments.insertComment(testCommentForSection(2)); 317 | expect($section2.find('.marker span').text().trim()).to.equal("1"); 318 | }); 319 | 320 | it("should have a link for comments posted by a currentUser that has a authorUrl", function(){ 321 | sideComments.on('commentPosted', function( comment ) { 322 | comment.id = 99; 323 | sideComments.insertComment(comment); 324 | }); 325 | 326 | $section1.find('.marker').trigger('click'); 327 | $section1.find('.add-comment').trigger('click'); 328 | $section1.find('.comment-box').val('Test Comment'); 329 | $section1.find('.action-link.post').trigger('click'); 330 | var $lastCommentAuthor = $section1.find('.comments > li').last().find('.author-name'); 331 | 332 | expect($lastCommentAuthor.attr('href')).to.eq(currentUser.authorUrl); 333 | }); 334 | 335 | it("should be inserted with a comment body", function () { 336 | sideComments.on('commentPosted', function ( comment ) { 337 | comment.id = 123; 338 | sideComments.insertComment(comment); 339 | }); 340 | 341 | var commentBody = 'Test comment'; 342 | 343 | $section2.find('.marker').trigger('click'); 344 | $section2.find('.add-comment').trigger('.click'); 345 | $section2.find('.comment-box').val(commentBody); 346 | $section2.find('.action-link.post').trigger('click'); 347 | 348 | expect($section2.find('.comments > li').last().find('.comment').text().trim()).to.equal(commentBody) 349 | }); 350 | 351 | describe("Comment is a reply", function () { 352 | 353 | it("should emit an event when a reply is posted", function ( done ) { 354 | this.timeout(0); 355 | var eventFired = false; 356 | 357 | setTimeout( function () { 358 | check( done, function() { 359 | expect(eventFired).to.be.true; 360 | } ) 361 | }, 500); 362 | 363 | sideComments.on('commentPosted', function( comment ) { 364 | eventFired = true; 365 | }); 366 | 367 | $section1.find('.marker').trigger('click'); 368 | $section1.find('.comments > li').first().find('.reply-comment').trigger('click'); 369 | $section1.find('.comments > li').first().find('.comment-box').html(newTestComment.comment); 370 | $section1.find('.comments > li').first().find('.reply-form .post').trigger('click'); 371 | }); 372 | 373 | it("should update a non-empty reply list length after adding", function(){ 374 | sideComments.insertComment(testCommentForSection(1, 88)); 375 | expect($section1.find('.comments > li').first().find('.replies li')).to.have.length.of(2); 376 | }); 377 | 378 | it("should update an empty reply list length after adding", function () { 379 | sideComments.insertComment(testCommentForSection(3, 66)); 380 | expect($section3.find('.comments > li').first().find('.replies li')).to.have.length.of(1); 381 | }); 382 | 383 | it("should update an empty reply list of a new comment", function () { 384 | sideComments.insertComment(testCommentForSection(2)); 385 | sideComments.insertComment(testCommentForSection(2, 278)); 386 | expect($section2.find('.comments > li').first().find('.replies li')).to.have.length.of(1); 387 | }); 388 | 389 | it("should belong to the current user", function () { 390 | sideComments.on('commentPosted', function( comment ) { 391 | comment.id = 3335; 392 | sideComments.insertComment(comment); 393 | }); 394 | 395 | $section1.find('.comments > li').first().find('.reply-comment').trigger('click'); 396 | $section1.find('.comments > li').first().find('.comment-box').html(newTestComment.comment); 397 | $section1.find('.comments > li').first().find('.reply-form .action-link.post').trigger('click'); 398 | 399 | expect($section1.find('.comments > li').first().find('.replies li').last().find('.author-name').text().trim()).to.equal(currentUser.name); 400 | }); 401 | 402 | }); 403 | 404 | }); 405 | 406 | 407 | describe("Comment Deleting", function(){ 408 | 409 | beforeEach(function( done ) { 410 | setupSideComments(); 411 | setSections(); 412 | done(); 413 | }); 414 | 415 | it("should emit an event when a comment is deleted", function( done ){ 416 | this.timeout(0); 417 | var eventFired = false; 418 | 419 | setTimeout( function () { 420 | check( done, function() { 421 | expect(eventFired).to.be.true; 422 | } ) 423 | }, 500); 424 | 425 | sideComments.on('commentDeleted', function( comment ) { 426 | eventFired = true; 427 | }); 428 | 429 | sideComments.sections[0].deleteComment(88); 430 | }); 431 | 432 | it("should update a section's comment count after removing", function(){ 433 | sideComments.removeComment(1, 112); 434 | expect($section1.find('.marker span').text().trim()).to.equal("1"); 435 | }); 436 | 437 | it("should update a section's comment list after deleting", function(){ 438 | sideComments.removeComment(1, 112); 439 | expect($section1.find('.comments > li')).to.have.length.of(1); 440 | }); 441 | 442 | it("should remove a section's comment count after deleting if it's the last comment", function(){ 443 | sideComments.removeComment(3, 66); 444 | expect($section3.find('.marker').is(':visible')).to.be.false; 445 | }); 446 | 447 | it("should show a section's comment form after deleting if it's the last comment", function(){ 448 | sideComments.removeComment(3, 66); 449 | $section3.find('.marker').trigger('click'); 450 | expect($section3.find('.comment-form').is(':visible')).to.be.true; 451 | }); 452 | 453 | it("should only update a comment's body when the deleted comment has replies", function () { 454 | sideComments.removeComment(1, 88); 455 | var comment = $section1.find('.comments > li').first().find('.comment').first().text(); 456 | expect(comment).to.equal("Comment deleted by the author"); 457 | }); 458 | 459 | describe("Comment is a reply", function () { 460 | 461 | it("should emit an event when a comment is deleted", function( done ){ 462 | this.timeout(0); 463 | var eventFired = false; 464 | 465 | setTimeout( function () { 466 | check( done, function() { 467 | expect(eventFired).to.be.true; 468 | } ) 469 | }, 500); 470 | 471 | sideComments.on('commentDeleted', function( comment ) { 472 | eventFired = true; 473 | }); 474 | 475 | sideComments.sections[0].deleteComment(100, 88); 476 | }); 477 | 478 | it("should update a comment replies list after deleting", function () { 479 | sideComments.removeComment(1, 100, 88); 480 | expect($section1.find('.comments > li').first().find('.replies li')).to.have.length.of(0) 481 | }); 482 | 483 | }); 484 | 485 | }); 486 | 487 | describe("No Current User", function(){ 488 | 489 | beforeEach(function( done ) { 490 | setupSideComments( false ); 491 | setSections(); 492 | done(); 493 | }); 494 | 495 | it("should show the add comment button rather than the comment form on sections without comments", function(){ 496 | $section2.find('.marker').trigger('click'); 497 | expect($section2.find('.add-comment').is(':visible')).to.be.true; 498 | expect($section2.find('.comment-form').is(':visible')).to.be.false; 499 | }); 500 | 501 | it("should add the no-curent-user class to all sections", function(){ 502 | expect($('.side-comment.no-current-user')).to.have.length.of(3); 503 | }); 504 | 505 | it("should emit the 'addCommentAttempted' event when a user tries to add a comment", function( done ){ 506 | this.timeout(0); 507 | var eventFired = false; 508 | 509 | setTimeout( function () { 510 | check( done, function() { 511 | expect(eventFired).to.be.true; 512 | } ) 513 | }, 500); 514 | 515 | sideComments.on('addCommentAttempted', function( comment ) { 516 | eventFired = true; 517 | }); 518 | 519 | $section1.find('.marker').trigger('click'); 520 | $section1.find('.add-comment').trigger('click'); 521 | }); 522 | 523 | }); 524 | 525 | describe("Setting a Current User", function(){ 526 | 527 | beforeEach(function( done ) { 528 | setupSideComments( false ); 529 | setSections(); 530 | done(); 531 | }); 532 | 533 | it("should update the UI once a currentUser has been set", function(){ 534 | sideComments.setCurrentUser(currentUser); 535 | setSections(); 536 | 537 | $section2.find('.marker').trigger('click'); 538 | expect($section2.find('.add-comment').is(':visible')).to.be.false; 539 | expect($section2.find(".comment-form").is(':visible')).to.be.true; 540 | 541 | $section1.find('.marker').trigger('click'); 542 | $section1.find('.add-comment').trigger('click'); 543 | expect($section1.find(".comment-form").is(':visible')).to.be.true; 544 | }); 545 | 546 | }); 547 | 548 | describe("Removing a Current User", function(){ 549 | 550 | beforeEach(function( done ) { 551 | setupSideComments(); 552 | $section1 = $('.side-comment').eq(0); 553 | $section2 = $('.side-comment').eq(1); 554 | $section3 = $('.side-comment').eq(2); 555 | done(); 556 | }); 557 | 558 | it("should update the UI once a currentUser has been removed", function(){ 559 | sideComments.removeCurrentUser(); 560 | setSections(); 561 | 562 | $section2.find('.marker').trigger('click'); 563 | expect($section2.find('.add-comment').is(':visible')).to.be.true; 564 | expect($section2.find(".comment-form").is(':visible')).to.be.false; 565 | 566 | $section1.find('.marker').trigger('click'); 567 | $section1.find('.add-comment').trigger('click'); 568 | expect($section1.find(".comment-form").is(':visible')).to.be.false; 569 | }); 570 | 571 | }); 572 | 573 | }); -------------------------------------------------------------------------------- /test/vendor/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /themes/default.css: -------------------------------------------------------------------------------- 1 | .side-comment { 2 | padding-bottom: 20px; 3 | } 4 | .side-comment .comments-wrapper { 5 | top: -22px; 6 | } 7 | .side-comment.has-comments .comments-wrapper { 8 | top: -22px; 9 | } 10 | .side-comment ul.comments li, 11 | .side-comment .comment-form { 12 | border: 1px solid #F2F2F0; 13 | border-left: 0; 14 | border-right: 0; 15 | padding: 15px 0; 16 | margin-top: -1px; 17 | } 18 | .side-comment .comment, 19 | .side-comment .comment-box { 20 | font-size: 14px; 21 | line-height: 18px; 22 | } 23 | .side-comment .author-name { 24 | font-size: 15px; 25 | line-height: 16px; 26 | margin: 0 0 2px 0; 27 | font-weight: 700; 28 | } 29 | .side-comment .action-link { 30 | color: #B3B3B1; 31 | font-size: 13px; 32 | text-decoration: none; 33 | } 34 | .side-comment .action-link:hover { 35 | text-decoration: none; 36 | } 37 | .side-comment .action-link.post .post { 38 | color: #89C794; 39 | } 40 | .side-comment .action-link.post .post:hover { 41 | color: #468c54; 42 | } 43 | .side-comment .action-link.cancel:hover, 44 | .side-comment .action-link.delete:hover { 45 | color: #57AD68; 46 | } 47 | .side-comment .add-comment { 48 | color: #B3B3B1; 49 | font-size: 14px; 50 | line-height: 22px; 51 | font-weight: 300; 52 | padding: 0px 8px; 53 | letter-spacing: 0.05em; 54 | text-decoration: none; 55 | margin-top: 10px; 56 | } 57 | .side-comment .add-comment:before { 58 | content: "+"; 59 | border: 2px solid #DEDEDC; 60 | border-radius: 100px; 61 | width: 23px; 62 | height: 23px; 63 | color: #DEDEDC; 64 | display: block; 65 | text-align: center; 66 | font-size: 16px; 67 | font-weight: 400; 68 | line-height: 18px; 69 | float: left; 70 | margin-right: 15px; 71 | letter-spacing: 0; 72 | -webkit-box-sizing: border-box; 73 | -moz-box-sizing: border-box; 74 | box-sizing: border-box; 75 | } 76 | .side-comment .add-comment:hover { 77 | text-decoration: none; 78 | } 79 | .side-comment .add-comment:hover { 80 | color: #4FAF62; 81 | } 82 | .side-comment .add-comment:hover:before { 83 | border-color: #4FAF62; 84 | color: #4FAF62; 85 | } 86 | .side-comment .comment-box:empty:not(:focus):before { 87 | color: #B5B5B5; 88 | } 89 | .side-comment .actions { 90 | margin-top: 5px; 91 | } 92 | .side-comment .actions a { 93 | float: left; 94 | } 95 | .side-comment .actions .cancel:before { 96 | content: '\00B7'; 97 | color: #B3B3B1; 98 | padding: 0 5px; 99 | } 100 | --------------------------------------------------------------------------------