├── .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 [](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 |
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 |