├── LICENSE ├── README.md └── files ├── Background Script └── incident_mock.js ├── GraphQL Schema └── Example.schema.gql ├── GraphQL Scripted Resolver ├── Resolver - Format Date.script.js ├── Resolver - Get Child Incidents.script.js ├── Resolver - Get User by id.script.js ├── Resolver - Get incident by number.script.js └── Resolver - Get incidents by filter.script.js └── Script Include ├── GraphQLExampleUtilities.script.js ├── moment.js.script.js └── underscore.js.script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marcus Reinhardt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # NO SUPPORT - NO UPDATES 2 | > Since I switched the role inside the company, I'm not longer working with ServiceNow! 3 | > I'll keep this repo as boilerplate, but I mark it as archived, because I will not be able to add updates 4 | > Thanks for reading and trying this example! 5 | 6 | --- 7 | 8 | > # External usage 9 | > 10 | > If you're planning to create an howto, video, blog entry or something else which contains parts or everything from this repo, 11 | > **PLEASE** don't forget to add the link to this repo in your credits ;) 12 | 13 | 14 | # ServiceNow GraphQL Example 15 | 16 | - [ServiceNow GraphQL Example](#servicenow-graphql-example) 17 | - [What do we need](#what-do-we-need) 18 | - [How to use GraphQL in Service Portal](#how-to-use-graphql-in-service-portal) 19 | - [(Useful) Links](#useful-links) 20 | - [What is not included](#what-is-not-included) 21 | - [What is included](#what-is-included) 22 | - [Files explained](#files-explained) 23 | - [GraphQL Schema: Example](#graphql-schema-example) 24 | - [GraphQL Scripted Resolver: Resolver - Format Date](#graphql-scripted-resolver-resolver---format-date) 25 | - [GraphQL Scripted Resolver: Resolver - Get Child Incidents](#graphql-scripted-resolver-resolver---get-child-incidents) 26 | - [GraphQL Scripted Resolver: Resolver - Get incident by number](#graphql-scripted-resolver-resolver---get-incident-by-number) 27 | - [GraphQL Scripted Resolver: Resolver - Get incidents by filter](#graphql-scripted-resolver-resolver---get-incidents-by-filter) 28 | - [GraphQL Scripted Resolver: Resolver - Get User by id](#graphql-scripted-resolver-resolver---get-user-by-id) 29 | - [Script Include: GraphQLExampleUtilities](#script-include-graphqlexampleutilities) 30 | - [Script Include: moment.js](#script-include-momentjs) 31 | - [Script Include: _](#script-include-_) 32 | - [How To](#how-to) 33 | - [Build](#build) 34 | - [Create the script includes](#create-the-script-includes) 35 | - [Enable GraphQL](#enable-graphql) 36 | - [Create a new schema](#create-a-new-schema) 37 | - [Create the schema resolver](#create-the-schema-resolver) 38 | - [Create the resolver mappings](#create-the-resolver-mappings) 39 | - [Test](#test) 40 | - [Get one incident - Simple](#get-one-incident---simple) 41 | - [Get one incident - Advanced](#get-one-incident---advanced) 42 | - [Get all incidents - Simple](#get-all-incidents---simple) 43 | - [Get all incidents - Advanced](#get-all-incidents---advanced) 44 | - [GraphQL filters](#graphql-filters) 45 | - [Examples](#examples) 46 | - [GraphQL Paging & Sorting](#graphql-paging--sorting) 47 | - [Example - Paging](#example---paging) 48 | - [Example - Sorting](#example---sorting) 49 | - [Extend the schema](#extend-the-schema) 50 | - [Visual Studio Code](#visual-studio-code) 51 | - [Mock Generator](#mock-generator) 52 | - [Conclusion](#conclusion) 53 | 54 | ## What do we need 55 | 56 | * ServiceNow Developer Instance with Release: Paris 57 | * Postman, Insomnia, A REST/GraphQL Client 58 | * Javascript Knowledge 59 | 60 | ## (Useful) Links 61 | 62 | * [SN DEV - GraphQL Introduction](https://docs.servicenow.com/bundle/paris-release-notes/page/release-notes/now-platform-app-engine/web-services-rn.html) 63 | * [Postman](https://www.postman.com/) 64 | * [Insomnia](http://insomnia.rest/) 65 | * [MomentJS](https://www.momentjs.com) 66 | * [UnderscoreJS](https://www.underscorejs.org) 67 | * [GraphQL](https://graphql.org/) 68 | 69 | ## How to use GraphQL in Service Portal 70 | 71 | The guys from [serviceportal.io](https://www.serviceportal.io) created a very good video tutorial about "What is GraphQL" and how to use it in the ServiceNow Service Portal. 72 | 73 | You can find the video and their sources here: https://serviceportal.io/graphql-in-servicenow/ 74 | 75 | ## What is not included 76 | 77 | I have excluded the mutation handling for this example. 78 | If you need an example for this, please install the Application "GraphQL Framework Demo Application" ( app id: com.glide.graphql.framework.demo ) via the internal SN App Store. 79 | 80 | ## What is included 81 | * Example to fetch one record 82 | * Example to fetch multiple records 83 | * Relationships 84 | * One-To-One (e.g. Opened By) 85 | * One-To-Many (e.g. Child Incidents) 86 | * Possibility to filter child incidents 87 | * Simple filter criteria handling 88 | * Reusable code => **DRY** 89 | * Paging 90 | * Sorting 91 | 92 | 93 | ## Files explained 94 | 95 |
96 | 97 | Click to expand 98 | 99 | ### GraphQL Schema: Example 100 | 101 | The complete GraphQL Schema ;) 102 | 103 | ### GraphQL Scripted Resolver: Resolver - Format Date 104 | 105 | Contains the code to convert a servicenow date/time string. 106 | 107 | > Behind the scenes: It calls the `getFormattedDate` method. 108 | 109 | ### GraphQL Scripted Resolver: Resolver - Get Child Incidents 110 | 111 | Contains the code to fetch multiple child incidents w/ or w/o filter criteria. 112 | 113 | > Behind the scenes: It calls the `generateQuery` and `getRecordList` with module `incident`. 114 | > There is a hardcoded filter criteria to ensure that `parent_incident` is always set. 115 | 116 | ### GraphQL Scripted Resolver: Resolver - Get incident by number 117 | 118 | Contains the code to fetch a incident based on the given number 119 | 120 | > Behind the scenes: It calls the `getRecord` method with module `incident` and the number 121 | 122 | ### GraphQL Scripted Resolver: Resolver - Get incidents by filter 123 | 124 | Contains the code to fetch multiple incidents w/ or w/o filter criteria. 125 | 126 | > Behind the scenes: It calls the `generateQuery` and `getRecordList` with module `incident`. 127 | 128 | ### GraphQL Scripted Resolver: Resolver - Get User by id 129 | 130 | Contains the code to fetch a user (e.g. opened by) in a incident. 131 | 132 | > Behind the scenes: It calls the `getRecord` method with module `user` and the sysid 133 | 134 | ### Script Include: GraphQLExampleUtilities 135 | 136 | We have one generic Script Include which contains all the logic. 137 | The Script Include handles the following: 138 | 139 | - Generating the Objects based on the GraphQL schema 140 | - Fetch one record 141 | - Fetch multiple records 142 | - Convert the given filter conditions into a valid servicenow query 143 | - Format a date value via `moment.js` 144 | 145 | 146 | ### Script Include: moment.js 147 | 148 | Moment.js in the version 2.20.1 149 | 150 | Source: https://github.com/moment/moment/releases/tag/2.20.1 151 | 152 | ### Script Include: underscore.js 153 | 154 | UnderscoreJS in the version: 1.8.2 155 | Duplicated from a existing Script Include. 156 | 157 |
158 | 159 | 160 | ---- 161 | 162 | # How To 163 | 164 | ## Build 165 | ### Create the script includes 166 | 167 | As you can see in the `files/Script Include/` directory, there are three files which you have to create: 168 | 169 | | Script Include Name | Content | 170 | |------------------------------|--------------------------------------------------------------| 171 | | `underscore.js` | Use `files/Script Include/underscore.js.script.js` | 172 | | `moment.js` | Use `files/Script Include/moment.js.script.js` | 173 | | `GraphQLExampleUtilities` | Use `files/Script Include/GraphQLExampleUtilities.script.js` | 174 | 175 | ### Enable GraphQL 176 | 177 | In the navigator, go to `System Web Services > GraphQL > Properties`. 178 | In my example, I have activated all checkboxes. 179 | 180 | ### Create a new schema 181 | 182 | In the navigator, go to `System Web Services > GraphQL > GraphQL APIs`. 183 | 184 | Click the `New` button and fill the fields with the following values: 185 | 186 | > Use can use different values if you want ;) 187 | 188 | | Field | Value | 189 | |----------------------------|-----------------------------------------------| 190 | | Name | Example | 191 | | Schema namespace | example | 192 | | Active | Yes | 193 | | Schema | Use `files/GraphQL Schema/Example.schema.qgl` | 194 | | Requires authentication | Yes | 195 | | Requires ACL authorization | No | 196 | 197 | 198 | Click the `Submit` button to create the new GraphQL API. 199 | 200 | ### Create the schema resolver 201 | 202 | In your created schema, you should see now the tab `GraphQL Scripted Resolvers`. 203 | 204 | You have to create the following records: 205 | 206 | > Please make sure, that you update the scope name. 207 | > `x_116934_graphql` will not work in your instance. 208 | 209 | | Resolver Name | Content | 210 | |------------------------------------|-----------------------------------------------| 211 | | Resolver - Format Date | Use `files/GraphQL Scripted Resolver/Resolver - Format Date.script.js` | 212 | | Resolver - Get Child Incidents | Use `files/GraphQL Scripted Resolver/Resolver - Get Child Incidents.script.js` | 213 | | Resolver - Get incident by number | Use `files/GraphQL Scripted Resolver/Resolver - Get incident by number.script.js` | 214 | | Resolver - Get incidents by filter | Use `files/GraphQL Scripted Resolver/Resolver - Get incidents by filter.script.js` | 215 | | Resolver - Get User by id | Use `files/GraphQL Scripted Resolver/Resolver - Get User by id.script.js` | 216 | 217 | ### Create the resolver mappings 218 | 219 | In your created schema, you should see now the tab `GraphQL Resolver Mappings`. 220 | 221 | You have to create the following records: 222 | 223 | | Path | Resolver | 224 | |-------------------------|------------------------------------| 225 | | Query:incident | Resolver - Get incident by number | 226 | | Query:allIncident | Resolver - Get incidents by filter | 227 | | Incident:openedAt | Resolver - Format Date | 228 | | Incident:resolvedAt | Resolver - Format Date | 229 | | Incident:closedAt | Resolver - Format Date | 230 | | Incident:childIncidents | Resolver - Get Child Incidents | 231 | | Incident:parentIncident | Resolver - Get incident by number | 232 | | Incident:openedBy | Resolver - Get User by id | 233 | | Incident:resolvedBy | Resolver - Get User by id | 234 | 235 | ## Test 236 | 237 | All requests have the same config: 238 | 239 | * Method: `POST` 240 | * Endpoint: `https://.service-now.com/api/now/graphql` 241 | * Auth 242 | * Type: Basic 243 | * Username/Password: Only you know it ;) 244 | 245 | ### Get one incident - Simple 246 | 247 | Body: 248 | 249 | > Please make sure, that you replace `x116934Graphql` with your Application namespace. 250 | > You can find your Application namespace in the `GraphQL API` record. 251 | 252 | ``` 253 | { 254 | x116934Graphql { 255 | example { 256 | incident(number: "INC0007001") { 257 | id 258 | number 259 | openedBy { 260 | id 261 | email 262 | } 263 | resolvedBy { 264 | id 265 | email 266 | } 267 | openedAt 268 | } 269 | 270 | } 271 | } 272 | } 273 | ``` 274 | 275 | The result should be something like: 276 | 277 | ```json 278 | { 279 | "data": { 280 | "x116934Graphql": { 281 | "example": { 282 | "incident": { 283 | "id": "f12ca184735123002728660c4cf6a7ef", 284 | "number": "INC0007001", 285 | "openedBy": { 286 | "id": "6816f79cc0a8016401c5a33be04be441", 287 | "email": "admin@example.com" 288 | }, 289 | "resolvedBy": null, 290 | "openedAt": "2018-10-17T12:47:10Z" 291 | } 292 | } 293 | } 294 | } 295 | } 296 | ``` 297 | 298 | ### Get one incident - Advanced 299 | 300 | Body: 301 | 302 | > Please make sure, that you replace `x116934Graphql` with your Application namespace. 303 | > You can find your Application namespace in the `GraphQL API` record. 304 | 305 | ```gql 306 | { 307 | x116934Graphql { 308 | example { 309 | incident(number: "INC0007001") { 310 | id 311 | number 312 | state 313 | impact 314 | urgency 315 | priority 316 | openedBy { 317 | id 318 | email 319 | } 320 | resolvedBy { 321 | id 322 | } 323 | formattedOpenedAt : openedAt(format: "DD.MM.Y") 324 | openedAt 325 | resolvedAt 326 | closedAt 327 | parentIncident { 328 | id 329 | number 330 | openedAt 331 | } 332 | newChilds: childIncidents(filter:{state:{eq:NEW}}) { 333 | results { 334 | id 335 | number 336 | state 337 | parentIncident { 338 | number 339 | } 340 | openedBy { 341 | email 342 | } 343 | } 344 | } 345 | allChilds: childIncidents { 346 | results { 347 | id 348 | number 349 | state 350 | } 351 | } 352 | } 353 | 354 | } 355 | } 356 | } 357 | ``` 358 | 359 | The result should be something like: 360 | 361 | ```json 362 | { 363 | "data": { 364 | "x116934Graphql": { 365 | "example": { 366 | "incident": { 367 | "id": "f12ca184735123002728660c4cf6a7ef", 368 | "number": "INC0007001", 369 | "state": "NEW", 370 | "impact": "HIGH", 371 | "urgency": "HIGH", 372 | "priority": "CRITICAL", 373 | "openedBy": { 374 | "id": "6816f79cc0a8016401c5a33be04be441", 375 | "email": "admin@example.com" 376 | }, 377 | "resolvedBy": null, 378 | "formattedOpenedAt": "17.10.2018", 379 | "openedAt": "2018-10-17T12:47:10Z", 380 | "resolvedAt": null, 381 | "closedAt": null, 382 | "parentIncident": null, 383 | "newChilds": { 384 | "results": [{ 385 | "id": "ff4c21c4735123002728660c4cf6a758", 386 | "number": "INC0007002", 387 | "state": "NEW", 388 | "parentIncident": { 389 | "number": "INC0007001" 390 | }, 391 | "openedBy": { 392 | "email": "admin@example.com" 393 | } 394 | }] 395 | }, 396 | "allChilds": { 397 | "results": [{ 398 | "id": "46c03489a9fe19810148cd5b8cbf501e", 399 | "number": "INC0000011", 400 | "state": "CLOSED" 401 | }, 402 | { 403 | "id": "e8caedcbc0a80164017df472f39eaed1", 404 | "number": "INC0000003", 405 | "state": "IN_PROGRESS" 406 | }, 407 | { 408 | "id": "ff4c21c4735123002728660c4cf6a758", 409 | "number": "INC0007002", 410 | "state": "NEW" 411 | } 412 | ] 413 | } 414 | } 415 | } 416 | } 417 | } 418 | } 419 | ``` 420 | 421 | ### Get all incidents - Simple 422 | 423 | Body: 424 | 425 | > Please make sure, that you replace `x116934Graphql` with your Application namespace. 426 | > You can find your Application namespace in the `GraphQL API` record. 427 | 428 | ```gql 429 | { 430 | x116934Graphql { 431 | example { 432 | allIncident { 433 | rowCount 434 | results { 435 | id 436 | number 437 | } 438 | } 439 | } 440 | } 441 | } 442 | ``` 443 | 444 | The result should be something like: 445 | 446 | ```json 447 | { 448 | "data": { 449 | "x116934Graphql": { 450 | "example": { 451 | "allIncident": { 452 | "rowCount": 1064, 453 | "results": [ 454 | { 455 | "id": "1c741bd70b2322007518478d83673af3", 456 | "number": "INC0000060" 457 | }, 458 | { 459 | "id": "1c832706732023002728660c4cf6a7b9", 460 | "number": "INC0009002" 461 | }, 462 | //... 463 | ] 464 | } 465 | } 466 | } 467 | } 468 | } 469 | ``` 470 | 471 | ### Get all incidents - Advanced 472 | 473 | Body: 474 | 475 | > Please make sure, that you replace `x116934Graphql` with your Application namespace. 476 | > You can find your Application namespace in the `GraphQL API` record. 477 | 478 | ```gql 479 | { 480 | x116934Graphql { 481 | example { 482 | allIncident(filter: {number: {in: ["INC0007001", "INC0007002"]}}) { 483 | rowCount 484 | results { 485 | id 486 | number 487 | parentIncident { 488 | number 489 | } 490 | } 491 | } 492 | } 493 | } 494 | } 495 | 496 | ``` 497 | 498 | The result should be something like: 499 | 500 | ```json 501 | { 502 | "data": { 503 | "x116934Graphql": { 504 | "example": { 505 | "allIncident": { 506 | "rowCount": 2, 507 | "results": [ 508 | { 509 | "id": "f12ca184735123002728660c4cf6a7ef", 510 | "number": "INC0007001", 511 | "parentIncident": null 512 | }, 513 | { 514 | "id": "ff4c21c4735123002728660c4cf6a758", 515 | "number": "INC0007002", 516 | "parentIncident": { 517 | "number": "INC0007001" 518 | } 519 | } 520 | ] 521 | } 522 | } 523 | } 524 | } 525 | } 526 | ``` 527 | 528 | ## GraphQL filters 529 | 530 | I'm a [Gridsome](https://www.gridsome.org) lover and here we have some builtin filters. 531 | I used these filters as startpoint for the implementation. 532 | 533 | Currently only the following filter operators are allowed: 534 | 535 | | GraphQL Operator | ServiceNow Operator | 536 | |------------------|---------------------| 537 | | eq | = | 538 | | ne | != | 539 | | in | IN | 540 | | nin | NOT IN | 541 | | lt | < | 542 | | lte | <= | 543 | | gt | > | 544 | | gte | >= | 545 | | between | BETWEEN | 546 | 547 | You can easily extend the query operators 548 | To do this, you have to 549 | 1. extend the `operators` definition in the `initialize` method 550 | - here you can decide between 551 | - the default value transformation (e.g. converting `[A,B]` to `A,B`) 552 | - or define your own logic which returns the needed the query part 553 | 2. create new inputs in the GraphQL schema 554 | 3. update the `IncidentQueryFilter` with the new searchable fields ( like `openedAt` ) 555 | 556 | ### Examples 557 | 558 | * Use `allIncident` to find all incidents with state `NEW`(=1) and urgency `LOW` (=3): 559 | 560 | ``` 561 | allIncident(filter:{state:{eq:NEW}, urgency: {eq:LOW}}) 562 | ``` 563 | 564 | * Use `allIncident` to find all incidents which have state `NEW` or `IN_PROGRESS` and have urgency `LOW` or `HIGH` 565 | 566 | ``` 567 | allIncident(filter:{state:{in:[NEW, IN_PROGRESS]}, urgency: {in:[LOW, HIGH]}}) 568 | ``` 569 | 570 | * Use `allIncident` to find all incidents which have state `NEW` or `IN_PROGRESS` but the urgency is not `LOW` or `HIGH` 571 | 572 | ``` 573 | allIncident(filter:{state:{in:[NEW, IN_PROGRESS]}, urgency: {nin:[LOW, HIGH]}}) 574 | ``` 575 | 576 | * Use `allIncident` to get all incidents between `INC0010000` and `INC0010010` 577 | 578 | ``` 579 | allIncident(filter:{number:{between:{from:"INC0010000", to: "INC0010010"}}}) 580 | ``` 581 | 582 | ## GraphQL Paging & Sorting 583 | 584 | Since there is no `@paginate` directive in the ServiceNow GraphQL implementation, 585 | With this example you will get a simple implementation for 586 | 587 | * Paging - uses `chooseWindow()` 588 | * Sorting - uses `orderBy()` / `orderByDesc()` 589 | 590 | 591 | ### Example - Paging 592 | 593 | GraphQL Request: 594 | 595 | ``` 596 | { 597 | x116934Graphql { 598 | example { 599 | allIncident(paginate: {perPage:10, page:2}) { 600 | rowCount 601 | pageInfo { 602 | totalPages 603 | currentPage 604 | } 605 | results { 606 | number 607 | } 608 | } 609 | } 610 | } 611 | } 612 | 613 | ``` 614 | 615 | Response: 616 | 617 | ```json 618 | { 619 | "data": { 620 | "x116934Graphql": { 621 | "example": { 622 | "allIncident": { 623 | "rowCount": 10, 624 | "pageInfo": { 625 | "totalPages": 106, 626 | "currentPage": 2 627 | }, 628 | "results": [{ 629 | "number": "INC0010401" 630 | }, 631 | { 632 | "number": "INC0010990" 633 | }, 634 | //... 635 | ] 636 | } 637 | } 638 | } 639 | } 640 | } 641 | ``` 642 | 643 | ### Example - Sorting 644 | 645 | GraphQL Request: 646 | 647 | ``` 648 | { 649 | x116934Graphql { 650 | example { 651 | allIncident(paginate: {perPage:10, page:2}, sort: {by: "number", order:DESC}) { 652 | rowCount 653 | pageInfo { 654 | totalPages 655 | currentPage 656 | } 657 | results { 658 | number 659 | } 660 | } 661 | } 662 | } 663 | } 664 | ``` 665 | 666 | Response: 667 | 668 | ```json 669 | { 670 | "data": { 671 | "x116934Graphql": { 672 | "example": { 673 | "allIncident": { 674 | "rowCount": 10, 675 | "pageInfo": { 676 | "totalPages": 106, 677 | "currentPage": 2 678 | }, 679 | "results": [{ 680 | "number": "INC0010991" 681 | }, 682 | { 683 | "number": "INC0010990" 684 | }, 685 | //... 686 | ] 687 | } 688 | } 689 | } 690 | } 691 | } 692 | ``` 693 | 694 | ## Extend the schema 695 | 696 | In case you need other fields available in your schema, you have to do the following steps. 697 | In this example, we're using the `assigned_to` field. 698 | 699 | 1. Update the schema 700 | 701 | We have to create a new field inside the `Incident` type. 702 | Since all fields are lowerCamelCase, the field will be named as `assignedTo`. 703 | 704 | ```js 705 | resolvedBy: User @source(value: "assignedTo.value") 706 | ``` 707 | 708 | We're using `User` as field type, otherwise we have no access to the user related fields. 709 | 710 | The addition `@source(value: "assignedTo.value")` is used to have access in the scripted resolver via `getSource()`. 711 | 712 | 2. Update the Script Include 713 | 714 | In the `GraphQLExampleUtilities.initialize` you have to update the `mapping` for the `incident` definition. 715 | Just add the following at the end: 716 | 717 | ```js 718 | assignedTo: { display: false, useQuery: false, field: 'assigned_to' }, 719 | ``` 720 | 721 | * The key ( `assignedTo` ) is the same as in your schema - It's important that the name is the same. Otherwise you will have a lot of errors ;) 722 | * `display` defines if `getDisplayValue` or `getValue` should be used. 723 | * `useQuery` defines if the database value should be replaced with the defined value in `queryBuilder` 724 | * `field` defines the field name in the database 725 | 726 | 3. Define a new schema mapping 727 | 728 | Like in the how to above, we need a new resolver mapping to get the related user information: 729 | 730 | | Path | Resolver | 731 | |-------------------------|------------------------------------| 732 | | Incident:assignedTo | Resolver - Get User by id | 733 | 734 | 4. You're done 735 | 736 | If you want to implement other fields like `service` or something else, please make sure 737 | 738 | * you have created a `Scripted Resolver` 739 | * you have defined a new `type` in the schema 740 | 741 | # Visual Studio Code 742 | 743 | If you're using the ServiceNow VSC Extension, here the content of my `app.config.json` for the GraphQL tables: 744 | 745 | ```json 746 | { 747 | "CustomFileTypes": { 748 | "sys_graphql_typeresolver": { 749 | "superCoverName": "Miscellaneous", 750 | "create": "no", 751 | "coverName": "GraphQL Type Resolver", 752 | "tags": { 753 | "script": "js" 754 | } 755 | }, 756 | "sys_graphql_resolver": { 757 | "superCoverName": "Miscellaneous", 758 | "create": "no", 759 | "coverName": "GraphQL Scripted Resolver", 760 | "tags": { 761 | "script": "js" 762 | } 763 | }, 764 | "sys_graphql_schema": { 765 | "superCoverName": "Miscellaneous", 766 | "create": "no", 767 | "coverName": "GraphQL Schema", 768 | "tags": { 769 | "schema": "gql" 770 | } 771 | } 772 | } 773 | } 774 | ``` 775 | 776 | # Mock Generator 777 | 778 | I created a small background script, which generates the configured amount of incidents with some basic information. 779 | 780 | You can find the script here: `files/Background Script/incident_mock.js` 781 | 782 | # Conclusion 783 | 784 | I personally love GraphQL and the benefits to have only one API instead of creating new API versions all the time. 785 | Also to have the possibility as consumer to define only the required fields helps to reduce the response size. 786 | 787 | 788 | -------------------------------------------------------------------------------- /files/Background Script/incident_mock.js: -------------------------------------------------------------------------------- 1 | gs.include('_') 2 | 3 | var urgency = [ 1, 2, 3 ] 4 | var impact = [ 1, 2, 3 ] 5 | var contactType = [ 'email', 'phone' ] 6 | var category = [ 'software', 'hardware', 'network', 'database' ] 7 | var state = [ 1, 2, 3 ] 8 | 9 | var user = [ 10 | '77ad8176731313005754660c4cf6a7de', //david miller 11 | '62826bf03710200044e0bfc8bcbe5df1', //abel tuter 12 | '71826bf03710200044e0bfc8bcbe5d3b' //aileen mottern 13 | ] 14 | 15 | /** 16 | * Generates 100 incidents 17 | * First incident has the title ... #1 18 | */ 19 | createIncident(1, 100) 20 | 21 | /** 22 | * Generates 100 incidents 23 | * first incident has the title ... #101 24 | */ 25 | //createIncident(101, 100) 26 | 27 | /** 28 | * Creates some dummy incidents based on the above defined values 29 | * 30 | * @param {Integer} start The number which should be use as start counter 31 | * @param {*} max The amount of incidents which should be create 32 | * 33 | * @return {void} 34 | */ 35 | function createIncident(start, max) { 36 | 37 | for (var count = start; count <= max; count++) { 38 | var gr = new GlideRecord('incident') 39 | gr.newRecord(); 40 | 41 | gr.setValue('caller_id', _.sample(user)) 42 | gr.setValue('category', _.sample(category)) 43 | gr.setValue('contact_type', _.sample(contactType)) 44 | gr.setValue('state', _.sample(state)) 45 | gr.setValue('impact', _.sample(impact)) 46 | gr.setValue('urgency', _.sample(urgency)) 47 | gr.setValue('short_description', 'Mock Incident #' + count) 48 | gr.setValue('description', 'This is a mock incident to test the QGL performance') 49 | 50 | var insertResult = gr.insert() 51 | } 52 | } -------------------------------------------------------------------------------- /files/GraphQL Schema/Example.schema.gql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | allIncident( 7 | filter: IncidentQueryFilter 8 | paginate: QueryPaginate 9 | sort: QuerySort 10 | ): IncidentResultList 11 | incident(number: String!): Incident 12 | } 13 | 14 | type Incident { 15 | id: ID! 16 | number: String! 17 | active: Boolean 18 | state: IncidentState @source(value: "state.value") 19 | priority: IncidentPriority @source(value: "priority.value") 20 | impact: IncidentImpact @source(value: "impact.value") 21 | urgency: IncidentUrgency @source(value: "urgency.value") 22 | description: String 23 | resolvedBy: User @source(value: "resolvedBy.value") 24 | openedBy: User @source(value: "openedBy.value") 25 | openedAt(format: String): String @source(value: "openedAt.value") 26 | resolvedAt(format: String): String @source(value: "resolvedAt.value") 27 | closedAt(format: String): String @source(value: "closedAt.value") 28 | parentIncident: Incident @source(value: "parentIncident.value") 29 | childIncidents(filter: IncidentQueryFilter): IncidentResultList 30 | @source(value: "childIncidents.value") 31 | } 32 | 33 | type User { 34 | id: String 35 | email: String 36 | } 37 | 38 | type IncidentResultList { 39 | rowCount: Int 40 | pageInfo: PageInfo 41 | results: [Incident] 42 | } 43 | 44 | type PageInfo { 45 | totalPages: Int 46 | currentPage: Int 47 | } 48 | 49 | input QueryPaginate { 50 | perPage: Int 51 | page: Int 52 | } 53 | 54 | input QuerySort { 55 | by: String! 56 | order: SortOrder 57 | } 58 | 59 | input IncidentQueryFilter { 60 | number: IncidentNumberOperatorInput 61 | state: IncidentStateOperatorInput 62 | contactType: IncidentContactTypeOperatorInput 63 | impact: IncidentImpactOperatorInput 64 | urgency: IncidentUrgencyOperatorInput 65 | priority: IncidentPriorityOperatorInput 66 | } 67 | 68 | enum SortOrder { 69 | ASC 70 | DESC 71 | } 72 | enum IncidentState { 73 | #Incident is logged but not yet triaged. 74 | NEW 75 | #Incident is assigned and is being investigated. 76 | IN_PROGRESS 77 | #The responsibility for the incident shifts temporarily to another entity to provide further information, evidence, or a resolution 78 | ON_HOLD 79 | #A satisfactory fix is provided for the incident to ensure that it does not occur again. 80 | RESOLVED 81 | #Incident is marked Closed after it is in the Resolved state for a specific duration and it is confirmed that the incident is satisfactorily resolved. 82 | CLOSED 83 | #Incident was triaged but found to be a duplicate incident, an unnecessary incident, or not an incident at all. 84 | CANCELED 85 | } 86 | 87 | enum IncidentContactType { 88 | EMAIL 89 | PHONE 90 | SELF_SERVICE 91 | WALK_IN 92 | } 93 | 94 | enum IncidentImpact { 95 | LOW 96 | MEDIUM 97 | HIGH 98 | } 99 | 100 | enum IncidentUrgency { 101 | LOW 102 | MEDIUM 103 | HIGH 104 | } 105 | 106 | enum IncidentPriority { 107 | PLANNING 108 | LOW 109 | MODERATE 110 | HIGH 111 | CRITICAL 112 | } 113 | 114 | input IDQueryOperatorInput { 115 | # Filter by property of (strict) equality. 116 | eq: ID 117 | # Filter by property not equal to provided value. 118 | ne: ID 119 | # Filter by property matching any of the provided values. 120 | in: [ID] 121 | } 122 | 123 | input StringQueryOperatorInput { 124 | # Filter by property of (strict) equality. 125 | eq: String 126 | # Filter by property not equal to provided value. 127 | ne: String 128 | # Filter by property matching any of the provided values. 129 | in: [String] 130 | } 131 | 132 | input StringBetweenQueryFilter { 133 | from: String! 134 | to: String! 135 | } 136 | 137 | input IncidentNumberOperatorInput { 138 | # Filter by property of (strict) equality. 139 | eq: String 140 | # Filter by property not equal to provided value. 141 | ne: String 142 | # Filter by property matching any of the provided values. 143 | in: [String] 144 | nin: [String] 145 | lte: String 146 | lt: String 147 | gt: String 148 | gte: String 149 | between: StringBetweenQueryFilter 150 | } 151 | 152 | input IncidentStateOperatorInput { 153 | # Filter by property of (strict) equality. 154 | eq: IncidentState 155 | # Filter by property not equal to provided value. 156 | ne: IncidentState 157 | # Filter by property matching any of the provided values. 158 | in: [IncidentState] 159 | } 160 | 161 | input IncidentContactTypeOperatorInput { 162 | # Filter by property of (strict) equality. 163 | eq: IncidentContactType 164 | # Filter by property not equal to provided value. 165 | ne: IncidentContactType 166 | # Filter by property matching any of the provided values. 167 | in: [IncidentContactType] 168 | nin: [String] 169 | } 170 | 171 | input IncidentUrgencyOperatorInput { 172 | # Filter by property of (strict) equality. 173 | eq: IncidentUrgency 174 | # Filter by property not equal to provided value. 175 | ne: IncidentUrgency 176 | # Filter by property matching any of the provided values. 177 | in: [IncidentUrgency] 178 | nin: [String] 179 | } 180 | 181 | input IncidentImpactOperatorInput { 182 | # Filter by property of (strict) equality. 183 | eq: IncidentImpact 184 | # Filter by property not equal to provided value. 185 | ne: IncidentImpact 186 | # Filter by property matching any of the provided values. 187 | in: [IncidentImpact] 188 | nin: [String] 189 | } 190 | 191 | input IncidentPriorityOperatorInput { 192 | # Filter by property of (strict) equality. 193 | eq: IncidentPriority 194 | # Filter by property not equal to provided value. 195 | ne: IncidentPriority 196 | # Filter by property matching any of the provided values. 197 | in: [IncidentPriority] 198 | nin: [String] 199 | } 200 | -------------------------------------------------------------------------------- /files/GraphQL Scripted Resolver/Resolver - Format Date.script.js: -------------------------------------------------------------------------------- 1 | (function process(/*ResolverEnvironment*/ env) { 2 | var dateFormat = env.getArguments().format || ''; 3 | var GraphQLUtils = new global.GraphQLExampleUtilities(); 4 | return GraphQLUtils.getFormattedDate(env.getSource(), dateFormat); 5 | })(env); 6 | -------------------------------------------------------------------------------- /files/GraphQL Scripted Resolver/Resolver - Get Child Incidents.script.js: -------------------------------------------------------------------------------- 1 | (function process(/*ResolverEnvironment*/ env) { 2 | var GraphQLUtils = new global.GraphQLExampleUtilities(); 3 | 4 | //set parent incident sys id as hardcoded query 5 | var filter = { parentIncident: { eq: env.getSource()} }; 6 | var query = GraphQLUtils.generateQuery('incident', filter); 7 | 8 | //if we have additional filter criterias 9 | var childFilter = (env.getArguments().filter != null) ? env.getArguments().filter : {}; 10 | var childQuery = GraphQLUtils.generateQuery('incident', childFilter); 11 | 12 | //merge both queries 13 | var listQuery = ( !childQuery ) ? query : query+'^'+childQuery; 14 | 15 | return GraphQLUtils.getRecordList('incident', listQuery); 16 | })(env); 17 | -------------------------------------------------------------------------------- /files/GraphQL Scripted Resolver/Resolver - Get User by id.script.js: -------------------------------------------------------------------------------- 1 | (function process(/*ResolverEnvironment*/ env) { 2 | var id = env.getArguments().id != null ? env.getArguments().id : env.getSource(); 3 | if(!id) return null; 4 | 5 | var GraphQLUtils = new global.GraphQLExampleUtilities(); 6 | return GraphQLUtils.getRecord('user', id); 7 | })(env); 8 | -------------------------------------------------------------------------------- /files/GraphQL Scripted Resolver/Resolver - Get incident by number.script.js: -------------------------------------------------------------------------------- 1 | (function process(/*ResolverEnvironment*/ env) { 2 | var id = env.getArguments().number != null ? env.getArguments().number : env.getSource(); 3 | if (!id) return null; 4 | 5 | var GraphQLUtils = new global.GraphQLExampleUtilities(); 6 | return GraphQLUtils.getRecord('incident', id); 7 | })(env); 8 | -------------------------------------------------------------------------------- /files/GraphQL Scripted Resolver/Resolver - Get incidents by filter.script.js: -------------------------------------------------------------------------------- 1 | (function process(/*ResolverEnvironment*/ env) { 2 | var GraphQLUtils = new global.GraphQLExampleUtilities(); 3 | var filter = (env.getArguments().filter != null) ? env.getArguments().filter : {}; 4 | var paginate = (env.getArguments().paginate != null) ? env.getArguments().paginate : false; 5 | var sort = (env.getArguments().sort != null) ? env.getArguments().sort : false; 6 | var query = GraphQLUtils.generateQuery('incident', filter); 7 | return GraphQLUtils.getRecordList('incident', query, paginate, sort); 8 | })(env); 9 | -------------------------------------------------------------------------------- /files/Script Include/GraphQLExampleUtilities.script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * REPOSITORY: https://github.com/noxify/ServiceNow-GraphQL-Example 4 | * 5 | * --------------- 6 | * 7 | * MIT License 8 | * 9 | * Copyright (c) 2020 Marcus Reinhardt 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | * SOFTWARE. 28 | */ 29 | 30 | gs.include('underscore.js'); 31 | gs.include('moment.js'); 32 | 33 | var GraphQLExampleUtilities = Class.create(); 34 | 35 | GraphQLExampleUtilities.prototype = { 36 | 37 | type: 'GraphQLExampleUtilities', 38 | 39 | initialize: function () { 40 | 41 | //Table definition for each module 42 | this.table = { 43 | incident: { 44 | tableName: 'incident', 45 | queryField: 'number' 46 | }, 47 | user: { 48 | tableName: 'sys_user', 49 | queryField: 'sys_id' 50 | } 51 | }; 52 | 53 | //Mapping which will be used as graphl response 54 | this.mapping = { 55 | incident: { 56 | id: { display: false, useQuery: false, field: 'sys_id' }, 57 | number: { display: false, useQuery: false, field: 'number' }, 58 | openedBy: { display: false, useQuery: false, field: 'opened_by' }, 59 | resolvedBy: { display: false, useQuery: false, field: 'resolved_by' }, 60 | openedAt: { display: false, useQuery: false, field: 'opened_at' }, 61 | resolvedAt: { display: false, useQuery: false, field: 'resolved_at' }, 62 | closedAt: { display: false, useQuery: false, field: 'closed_at' }, 63 | urgency: { display: false, useQuery: true, field: 'urgency' }, 64 | impact: { display: false, useQuery: true, field: 'impact' }, 65 | priority: { display: false, useQuery: true, field: 'priority' }, 66 | contactType: { display: false, useQuery: true, field: 'contact_type' }, 67 | state: { display: false, useQuery: true, field: 'state' }, 68 | parentIncident: { display: true, useQuery: false, field: 'parent_incident' }, 69 | childIncidents: { display: false, useQuery: false, field: 'sys_id' }, 70 | }, 71 | 72 | user: { 73 | id: { display: false, field: 'sys_id' }, 74 | email: { display: false, field: 'email' } 75 | } 76 | }; 77 | 78 | //Definition for the value replacement e.g. the GQL enums 79 | this.queryBuilder = { 80 | incident: { 81 | state: { 82 | NEW: 1, 83 | IN_PROGRESS: 2, 84 | ON_HOLD: 3, 85 | RESOLVED: 6, 86 | CLOSED: 7, 87 | CANCELED: 8, 88 | }, 89 | contactType: { 90 | EMAIL: 'email', 91 | PHONE: 'phone', 92 | SELF_SERVICE: 'self-service', 93 | WALK_IN: 'walk-in' 94 | }, 95 | impact: { 96 | LOW: 3, 97 | MEDIUM: 2, 98 | HIGH: 1 99 | }, 100 | urgency: { 101 | LOW: 3, 102 | MEDIUM: 2, 103 | HIGH: 1 104 | }, 105 | priority: { 106 | PLANNING: 5, 107 | LOW: 4, 108 | MODERATE: 3, 109 | HIGH: 2, 110 | CRITICAL: 1 111 | } 112 | } 113 | }; 114 | 115 | //Definition for the filter operators 116 | this.operators = { 117 | 'eq': '=', 118 | 'ne': '!=', 119 | 'in': 'IN', 120 | 'nin': 'NOT IN', 121 | 'lt': '<', 122 | 'lte': '<=', 123 | 'gt': '>', 124 | 'gte': '>=', 125 | 'between': function (field, value) { 126 | return field + 'BETWEEN' + value.from + '@' + value.to; 127 | } 128 | }; 129 | }, 130 | 131 | /** 132 | * Generates the encoded query based on the given 133 | * module and filter criterias 134 | * 135 | * @param {String} module The module name e.g. incident or user 136 | * @param {Object} filter The filter criterias 137 | * 138 | * @return {String|null} The encoded query string or null 139 | */ 140 | generateQuery: function (module, filter) { 141 | 142 | if (_.isNull(filter) || _.isUndefined(filter) || _.isEmpty(filter)) { 143 | return null; 144 | } 145 | 146 | var that = this; 147 | var query = _.map(filter, function (definition, field) { 148 | 149 | var targetField = (that.mapping[ module ][ field ]) ? that.mapping[ module ][ field ].field : field; 150 | 151 | return _.map(definition, function (value, op) { 152 | //check if the current operator is a function 153 | if (_.isFunction(that.operators[ op ])) { 154 | return that.operators[ op ](targetField, value); 155 | } else { 156 | //if not, run the default behavior 157 | value = that.convertQueryValue(module, field, value); 158 | return targetField + that.operators[ op ] + value; 159 | } 160 | }); 161 | }); 162 | 163 | query = _.flatten(query); 164 | 165 | return query.join('^'); 166 | }, 167 | 168 | /** 169 | * Converts the given query value. 170 | * It uses the definition from `queryBuilder` for the convert 171 | * 172 | * @example 173 | * convertQueryValue('incident', 'number', 'INC1234') 174 | * //returns: INC1234 175 | * 176 | * convertQueryValue('incident', 'state', ['NEW', 'IN_PROGRESS']) 177 | * //returns: 1,2 178 | * 179 | * convertQueryValue('incident', 'contactType', 'WALK_IN') 180 | * //returns: walk-in 181 | * 182 | * @param {String} module The current module 183 | * @param {String} field The current field name 184 | * @param {String|Array} value The value to convert 185 | * 186 | * @return {String} converted value 187 | */ 188 | convertQueryValue: function (module, field, value) { 189 | 190 | var that = this; 191 | var newValue = (!_.isArray(value)) ? [ value ] : value; 192 | 193 | return _.map(newValue, function (fieldValue) { 194 | try { 195 | return that.queryBuilder[ module ][ field ][ fieldValue ]; 196 | } catch (e) { 197 | return fieldValue; 198 | } 199 | }).join(','); 200 | }, 201 | 202 | /** 203 | * Gets a list of records based on the given credentials 204 | * and converts it to the expected graphql object 205 | * 206 | * @param {String} module The current module e.g. incident or user 207 | * @param {String} query The query which should be execute 208 | * @param {Object|Boolean} paginate The pagination config 209 | * @param {Object|Boolean} sort The sort config 210 | * 211 | * @return {Array} The result with the generated objects 212 | */ 213 | getRecordList: function (module, query, paginate, sort) { 214 | 215 | var moduleConfig = this.table[ module ]; 216 | 217 | //count all records 218 | var grCount = new GlideAggregate(moduleConfig.tableName); 219 | grCount.addAggregate('COUNT'); 220 | if (query) { 221 | grCount.addEncodedQuery(query); 222 | } 223 | grCount.query(); 224 | 225 | var countResult = (grCount.next()) ? grCount.getAggregate('COUNT') : 0; 226 | 227 | //fetch the records 228 | var grRecord = new GlideRecord(moduleConfig.tableName); 229 | 230 | if (query) { 231 | grRecord.addEncodedQuery(query); 232 | } 233 | 234 | //add paging if defined 235 | if (paginate) { 236 | var rowStart = (paginate.perPage * paginate.page) - paginate.perPage; 237 | var rowEnd = (paginate.perPage * paginate.page); 238 | grRecord.chooseWindow(rowStart, rowEnd); 239 | } 240 | 241 | //add sorting if defined 242 | if (sort) { 243 | if (!sort.order || sort.order == 'ASC') { 244 | grRecord.orderBy(this.mapping[ module ][ sort.by ][ 'field' ]); 245 | } else { 246 | grRecord.orderByDesc(this.mapping[ module ][ sort.by ][ 'field' ]); 247 | } 248 | } 249 | 250 | grRecord.query(); 251 | var records = []; 252 | while (grRecord.next()) { 253 | records.push(this.createResponseObject(grRecord, module, this.mapping[ module ])); 254 | } 255 | 256 | return { 257 | pageInfo: { 258 | totalPages: (paginate) ? Math.round(countResult / paginate.perPage) : 1, 259 | currentPage: (paginate) ? paginate.page : 1 260 | }, 261 | rowCount: records.length, 262 | results: records 263 | }; 264 | }, 265 | 266 | /** 267 | * Get a record based on the given credentials 268 | * and converts it to the expected graphql object 269 | * 270 | * @param {String} module The current module e.g. incident or user 271 | * @param {String} value The value which should be used for the `get` 272 | * 273 | * @return {Object} The generated object from `createResponseObject` 274 | */ 275 | getRecord: function (module, value) { 276 | var moduleConfig = this.table[ module ]; 277 | var grRecord = new GlideRecord(moduleConfig.tableName); 278 | var exists = grRecord.get(moduleConfig.queryField, value); 279 | return (exists) ? this.createResponseObject(grRecord, module, this.mapping[ module ]) : null; 280 | }, 281 | 282 | /** 283 | * Converts the given date string based on the given format 284 | * Also the given date will be returned as UTC date 285 | * 286 | * Valid formats can be found here: 287 | * https://momentjs.com/docs/#/displaying/format/ 288 | * 289 | * @param {String} value The date/time string 290 | * @param {String} format The format which should be used 291 | * 292 | * @return {String} the formatted date 293 | */ 294 | getFormattedDate: function (value, format) { 295 | if (!value) return null; 296 | var currentDate = moment(value); 297 | return currentDate.utc().format(format); 298 | }, 299 | 300 | /** 301 | * Generates the graphql response object based 302 | * on the mapping definition 303 | * 304 | * @param {GlideRecord} grRecord The current Glide Record 305 | * @param {Object} objMapping The field mapping 306 | * 307 | * @return {Object} 308 | */ 309 | createResponseObject: function (grRecord, module, objMapping) { 310 | var that = this; 311 | return _.mapObject(objMapping, function (fieldDef, propertyName) { 312 | var value = (fieldDef.display === true) ? grRecord.getDisplayValue(fieldDef.field) : grRecord.getValue(fieldDef.field); 313 | if (fieldDef.useQuery) { 314 | var selection = _.invert(that.queryBuilder[ module ][ propertyName ]); 315 | if (selection[ value ]) { 316 | value = selection[ value ]; 317 | } 318 | } 319 | return value; 320 | }); 321 | } 322 | }; 323 | -------------------------------------------------------------------------------- /files/Script Include/underscore.js.script.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.2 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Save bytes in the minified (but not gzipped) version: 18 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 19 | 20 | // Create quick reference variables for speed access to core prototypes. 21 | var 22 | push = ArrayProto.push, 23 | slice = ArrayProto.slice, 24 | toString = ObjProto.toString, 25 | hasOwnProperty = ObjProto.hasOwnProperty; 26 | 27 | // All **ECMAScript 5** native function implementations that we hope to use 28 | // are declared here. 29 | var 30 | nativeIsArray = Array.isArray, 31 | nativeKeys = Object.keys, 32 | nativeBind = FuncProto.bind, 33 | nativeCreate = Object.create; 34 | 35 | // Naked function reference for surrogate-prototype-swapping. 36 | var Ctor = function(){}; 37 | 38 | // Create a safe reference to the Underscore object for use below. 39 | var _ = function(obj) { 40 | if (obj instanceof _) return obj; 41 | if (!(this instanceof _)) return new _(obj); 42 | this._wrapped = obj; 43 | }; 44 | 45 | // Export the Underscore object for **Node.js**, with 46 | // backwards-compatibility for the old `require()` API. If we're in 47 | // the browser, add `_` as a global object. 48 | if (typeof exports !== 'undefined') { 49 | if (typeof module !== 'undefined' && module.exports) { 50 | exports = module.exports = _; 51 | } 52 | exports._ = _; 53 | } else { 54 | root._ = _; 55 | } 56 | 57 | // Current version. 58 | _.VERSION = '1.8.2'; 59 | 60 | // Internal function that returns an efficient (for current engines) version 61 | // of the passed-in callback, to be repeatedly applied in other Underscore 62 | // functions. 63 | var optimizeCb = function(func, context, argCount) { 64 | if (context === void 0) return func; 65 | switch (argCount == null ? 3 : argCount) { 66 | case 1: return function(value) { 67 | return func.call(context, value); 68 | }; 69 | case 2: return function(value, other) { 70 | return func.call(context, value, other); 71 | }; 72 | case 3: return function(value, index, collection) { 73 | return func.call(context, value, index, collection); 74 | }; 75 | case 4: return function(accumulator, value, index, collection) { 76 | return func.call(context, accumulator, value, index, collection); 77 | }; 78 | } 79 | return function() { 80 | return func.apply(context, arguments); 81 | }; 82 | }; 83 | 84 | // A mostly-internal function to generate callbacks that can be applied 85 | // to each element in a collection, returning the desired result — either 86 | // identity, an arbitrary callback, a property matcher, or a property accessor. 87 | var cb = function(value, context, argCount) { 88 | if (value == null) return _.identity; 89 | if (_.isFunction(value)) return optimizeCb(value, context, argCount); 90 | if (_.isObject(value)) return _.matcher(value); 91 | return _.property(value); 92 | }; 93 | _.iteratee = function(value, context) { 94 | return cb(value, context, Infinity); 95 | }; 96 | 97 | // An internal function for creating assigner functions. 98 | var createAssigner = function(keysFunc, undefinedOnly) { 99 | return function(obj) { 100 | var length = arguments.length; 101 | if (length < 2 || obj == null) return obj; 102 | for (var index = 1; index < length; index++) { 103 | var source = arguments[index], 104 | keys = keysFunc(source), 105 | l = keys.length; 106 | for (var i = 0; i < l; i++) { 107 | var key = keys[i]; 108 | if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; 109 | } 110 | } 111 | return obj; 112 | }; 113 | }; 114 | 115 | // An internal function for creating a new object that inherits from another. 116 | var baseCreate = function(prototype) { 117 | if (!_.isObject(prototype)) return {}; 118 | if (nativeCreate) return nativeCreate(prototype); 119 | Ctor.prototype = prototype; 120 | var result = new Ctor; 121 | Ctor.prototype = null; 122 | return result; 123 | }; 124 | 125 | // Helper for collection methods to determine whether a collection 126 | // should be iterated as an array or as an object 127 | // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength 128 | var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; 129 | var isArrayLike = function(collection) { 130 | var length = collection && collection.length; 131 | return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; 132 | }; 133 | 134 | // Collection Functions 135 | // -------------------- 136 | 137 | // The cornerstone, an `each` implementation, aka `forEach`. 138 | // Handles raw objects in addition to array-likes. Treats all 139 | // sparse array-likes as if they were dense. 140 | _.each = _.forEach = function(obj, iteratee, context) { 141 | iteratee = optimizeCb(iteratee, context); 142 | var i, length; 143 | if (isArrayLike(obj)) { 144 | for (i = 0, length = obj.length; i < length; i++) { 145 | iteratee(obj[i], i, obj); 146 | } 147 | } else { 148 | var keys = _.keys(obj); 149 | for (i = 0, length = keys.length; i < length; i++) { 150 | iteratee(obj[keys[i]], keys[i], obj); 151 | } 152 | } 153 | return obj; 154 | }; 155 | 156 | // Return the results of applying the iteratee to each element. 157 | _.map = _.collect = function(obj, iteratee, context) { 158 | iteratee = cb(iteratee, context); 159 | var keys = !isArrayLike(obj) && _.keys(obj), 160 | length = (keys || obj).length, 161 | results = Array(length); 162 | for (var index = 0; index < length; index++) { 163 | var currentKey = keys ? keys[index] : index; 164 | results[index] = iteratee(obj[currentKey], currentKey, obj); 165 | } 166 | return results; 167 | }; 168 | 169 | // Create a reducing function iterating left or right. 170 | function createReduce(dir) { 171 | // Optimized iterator function as using arguments.length 172 | // in the main function will deoptimize the, see #1991. 173 | function iterator(obj, iteratee, memo, keys, index, length) { 174 | for (; index >= 0 && index < length; index += dir) { 175 | var currentKey = keys ? keys[index] : index; 176 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 177 | } 178 | return memo; 179 | } 180 | 181 | return function(obj, iteratee, memo, context) { 182 | iteratee = optimizeCb(iteratee, context, 4); 183 | var keys = !isArrayLike(obj) && _.keys(obj), 184 | length = (keys || obj).length, 185 | index = dir > 0 ? 0 : length - 1; 186 | // Determine the initial value if none is provided. 187 | if (arguments.length < 3) { 188 | memo = obj[keys ? keys[index] : index]; 189 | index += dir; 190 | } 191 | return iterator(obj, iteratee, memo, keys, index, length); 192 | }; 193 | } 194 | 195 | // **Reduce** builds up a single result from a list of values, aka `inject`, 196 | // or `foldl`. 197 | _.reduce = _.foldl = _.inject = createReduce(1); 198 | 199 | // The right-associative version of reduce, also known as `foldr`. 200 | _.reduceRight = _.foldr = createReduce(-1); 201 | 202 | // Return the first value which passes a truth test. Aliased as `detect`. 203 | _.find = _.detect = function(obj, predicate, context) { 204 | var key; 205 | if (isArrayLike(obj)) { 206 | key = _.findIndex(obj, predicate, context); 207 | } else { 208 | key = _.findKey(obj, predicate, context); 209 | } 210 | if (key !== void 0 && key !== -1) return obj[key]; 211 | }; 212 | 213 | // Return all the elements that pass a truth test. 214 | // Aliased as `select`. 215 | _.filter = _.select = function(obj, predicate, context) { 216 | var results = []; 217 | predicate = cb(predicate, context); 218 | _.each(obj, function(value, index, list) { 219 | if (predicate(value, index, list)) results.push(value); 220 | }); 221 | return results; 222 | }; 223 | 224 | // Return all the elements for which a truth test fails. 225 | _.reject = function(obj, predicate, context) { 226 | return _.filter(obj, _.negate(cb(predicate)), context); 227 | }; 228 | 229 | // Determine whether all of the elements match a truth test. 230 | // Aliased as `all`. 231 | _.every = _.all = function(obj, predicate, context) { 232 | predicate = cb(predicate, context); 233 | var keys = !isArrayLike(obj) && _.keys(obj), 234 | length = (keys || obj).length; 235 | for (var index = 0; index < length; index++) { 236 | var currentKey = keys ? keys[index] : index; 237 | if (!predicate(obj[currentKey], currentKey, obj)) return false; 238 | } 239 | return true; 240 | }; 241 | 242 | // Determine if at least one element in the object matches a truth test. 243 | // Aliased as `any`. 244 | _.some = _.any = function(obj, predicate, context) { 245 | predicate = cb(predicate, context); 246 | var keys = !isArrayLike(obj) && _.keys(obj), 247 | length = (keys || obj).length; 248 | for (var index = 0; index < length; index++) { 249 | var currentKey = keys ? keys[index] : index; 250 | if (predicate(obj[currentKey], currentKey, obj)) return true; 251 | } 252 | return false; 253 | }; 254 | 255 | // Determine if the array or object contains a given value (using `===`). 256 | // Aliased as `includes` and `include`. 257 | _.contains = _.includes = _.include = function(obj, target, fromIndex) { 258 | if (!isArrayLike(obj)) obj = _.values(obj); 259 | return _.indexOf(obj, target, typeof fromIndex == 'number' && fromIndex) >= 0; 260 | }; 261 | 262 | // Invoke a method (with arguments) on every item in a collection. 263 | _.invoke = function(obj, method) { 264 | var args = slice.call(arguments, 2); 265 | var isFunc = _.isFunction(method); 266 | return _.map(obj, function(value) { 267 | var func = isFunc ? method : value[method]; 268 | return func == null ? func : func.apply(value, args); 269 | }); 270 | }; 271 | 272 | // Convenience version of a common use case of `map`: fetching a property. 273 | _.pluck = function(obj, key) { 274 | return _.map(obj, _.property(key)); 275 | }; 276 | 277 | // Convenience version of a common use case of `filter`: selecting only objects 278 | // containing specific `key:value` pairs. 279 | _.where = function(obj, attrs) { 280 | return _.filter(obj, _.matcher(attrs)); 281 | }; 282 | 283 | // Convenience version of a common use case of `find`: getting the first object 284 | // containing specific `key:value` pairs. 285 | _.findWhere = function(obj, attrs) { 286 | return _.find(obj, _.matcher(attrs)); 287 | }; 288 | 289 | // Return the maximum element (or element-based computation). 290 | _.max = function(obj, iteratee, context) { 291 | var result = -Infinity, lastComputed = -Infinity, 292 | value, computed; 293 | if (iteratee == null && obj != null) { 294 | obj = isArrayLike(obj) ? obj : _.values(obj); 295 | for (var i = 0, length = obj.length; i < length; i++) { 296 | value = obj[i]; 297 | if (value > result) { 298 | result = value; 299 | } 300 | } 301 | } else { 302 | iteratee = cb(iteratee, context); 303 | _.each(obj, function(value, index, list) { 304 | computed = iteratee(value, index, list); 305 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) { 306 | result = value; 307 | lastComputed = computed; 308 | } 309 | }); 310 | } 311 | return result; 312 | }; 313 | 314 | // Return the minimum element (or element-based computation). 315 | _.min = function(obj, iteratee, context) { 316 | var result = Infinity, lastComputed = Infinity, 317 | value, computed; 318 | if (iteratee == null && obj != null) { 319 | obj = isArrayLike(obj) ? obj : _.values(obj); 320 | for (var i = 0, length = obj.length; i < length; i++) { 321 | value = obj[i]; 322 | if (value < result) { 323 | result = value; 324 | } 325 | } 326 | } else { 327 | iteratee = cb(iteratee, context); 328 | _.each(obj, function(value, index, list) { 329 | computed = iteratee(value, index, list); 330 | if (computed < lastComputed || computed === Infinity && result === Infinity) { 331 | result = value; 332 | lastComputed = computed; 333 | } 334 | }); 335 | } 336 | return result; 337 | }; 338 | 339 | // Shuffle a collection, using the modern version of the 340 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 341 | _.shuffle = function(obj) { 342 | var set = isArrayLike(obj) ? obj : _.values(obj); 343 | var length = set.length; 344 | var shuffled = Array(length); 345 | for (var index = 0, rand; index < length; index++) { 346 | rand = _.random(0, index); 347 | if (rand !== index) shuffled[index] = shuffled[rand]; 348 | shuffled[rand] = set[index]; 349 | } 350 | return shuffled; 351 | }; 352 | 353 | // Sample **n** random values from a collection. 354 | // If **n** is not specified, returns a single random element. 355 | // The internal `guard` argument allows it to work with `map`. 356 | _.sample = function(obj, n, guard) { 357 | if (n == null || guard) { 358 | if (!isArrayLike(obj)) obj = _.values(obj); 359 | return obj[_.random(obj.length - 1)]; 360 | } 361 | return _.shuffle(obj).slice(0, Math.max(0, n)); 362 | }; 363 | 364 | // Sort the object's values by a criterion produced by an iteratee. 365 | _.sortBy = function(obj, iteratee, context) { 366 | iteratee = cb(iteratee, context); 367 | return _.pluck(_.map(obj, function(value, index, list) { 368 | return { 369 | value: value, 370 | index: index, 371 | criteria: iteratee(value, index, list) 372 | }; 373 | }).sort(function(left, right) { 374 | var a = left.criteria; 375 | var b = right.criteria; 376 | if (a !== b) { 377 | if (a > b || a === void 0) return 1; 378 | if (a < b || b === void 0) return -1; 379 | } 380 | return left.index - right.index; 381 | }), 'value'); 382 | }; 383 | 384 | // An internal function used for aggregate "group by" operations. 385 | var group = function(behavior) { 386 | return function(obj, iteratee, context) { 387 | var result = {}; 388 | iteratee = cb(iteratee, context); 389 | _.each(obj, function(value, index) { 390 | var key = iteratee(value, index, obj); 391 | behavior(result, value, key); 392 | }); 393 | return result; 394 | }; 395 | }; 396 | 397 | // Groups the object's values by a criterion. Pass either a string attribute 398 | // to group by, or a function that returns the criterion. 399 | _.groupBy = group(function(result, value, key) { 400 | if (_.has(result, key)) result[key].push(value); else result[key] = [value]; 401 | }); 402 | 403 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 404 | // when you know that your index values will be unique. 405 | _.indexBy = group(function(result, value, key) { 406 | result[key] = value; 407 | }); 408 | 409 | // Counts instances of an object that group by a certain criterion. Pass 410 | // either a string attribute to count by, or a function that returns the 411 | // criterion. 412 | _.countBy = group(function(result, value, key) { 413 | if (_.has(result, key)) result[key]++; else result[key] = 1; 414 | }); 415 | 416 | // Safely create a real, live array from anything iterable. 417 | _.toArray = function(obj) { 418 | if (!obj) return []; 419 | if (_.isArray(obj)) return slice.call(obj); 420 | if (isArrayLike(obj)) return _.map(obj, _.identity); 421 | return _.values(obj); 422 | }; 423 | 424 | // Return the number of elements in an object. 425 | _.size = function(obj) { 426 | if (obj == null) return 0; 427 | return isArrayLike(obj) ? obj.length : _.keys(obj).length; 428 | }; 429 | 430 | // Split a collection into two arrays: one whose elements all satisfy the given 431 | // predicate, and one whose elements all do not satisfy the predicate. 432 | _.partition = function(obj, predicate, context) { 433 | predicate = cb(predicate, context); 434 | var pass = [], fail = []; 435 | _.each(obj, function(value, key, obj) { 436 | (predicate(value, key, obj) ? pass : fail).push(value); 437 | }); 438 | return [pass, fail]; 439 | }; 440 | 441 | // Array Functions 442 | // --------------- 443 | 444 | // Get the first element of an array. Passing **n** will return the first N 445 | // values in the array. Aliased as `head` and `take`. The **guard** check 446 | // allows it to work with `_.map`. 447 | _.first = _.head = _.take = function(array, n, guard) { 448 | if (array == null) return void 0; 449 | if (n == null || guard) return array[0]; 450 | return _.initial(array, array.length - n); 451 | }; 452 | 453 | // Returns everything but the last entry of the array. Especially useful on 454 | // the arguments object. Passing **n** will return all the values in 455 | // the array, excluding the last N. 456 | _.initial = function(array, n, guard) { 457 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); 458 | }; 459 | 460 | // Get the last element of an array. Passing **n** will return the last N 461 | // values in the array. 462 | _.last = function(array, n, guard) { 463 | if (array == null) return void 0; 464 | if (n == null || guard) return array[array.length - 1]; 465 | return _.rest(array, Math.max(0, array.length - n)); 466 | }; 467 | 468 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 469 | // Especially useful on the arguments object. Passing an **n** will return 470 | // the rest N values in the array. 471 | _.rest = _.tail = _.drop = function(array, n, guard) { 472 | return slice.call(array, n == null || guard ? 1 : n); 473 | }; 474 | 475 | // Trim out all falsy values from an array. 476 | _.compact = function(array) { 477 | return _.filter(array, _.identity); 478 | }; 479 | 480 | // Internal implementation of a recursive `flatten` function. 481 | var flatten = function(input, shallow, strict, startIndex) { 482 | var output = [], idx = 0; 483 | for (var i = startIndex || 0, length = input && input.length; i < length; i++) { 484 | var value = input[i]; 485 | if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { 486 | //flatten current level of array or arguments object 487 | if (!shallow) value = flatten(value, shallow, strict); 488 | var j = 0, len = value.length; 489 | output.length += len; 490 | while (j < len) { 491 | output[idx++] = value[j++]; 492 | } 493 | } else if (!strict) { 494 | output[idx++] = value; 495 | } 496 | } 497 | return output; 498 | }; 499 | 500 | // Flatten out an array, either recursively (by default), or just one level. 501 | _.flatten = function(array, shallow) { 502 | return flatten(array, shallow, false); 503 | }; 504 | 505 | // Return a version of the array that does not contain the specified value(s). 506 | _.without = function(array) { 507 | return _.difference(array, slice.call(arguments, 1)); 508 | }; 509 | 510 | // Produce a duplicate-free version of the array. If the array has already 511 | // been sorted, you have the option of using a faster algorithm. 512 | // Aliased as `unique`. 513 | _.uniq = _.unique = function(array, isSorted, iteratee, context) { 514 | if (array == null) return []; 515 | if (!_.isBoolean(isSorted)) { 516 | context = iteratee; 517 | iteratee = isSorted; 518 | isSorted = false; 519 | } 520 | if (iteratee != null) iteratee = cb(iteratee, context); 521 | var result = []; 522 | var seen = []; 523 | for (var i = 0, length = array.length; i < length; i++) { 524 | var value = array[i], 525 | computed = iteratee ? iteratee(value, i, array) : value; 526 | if (isSorted) { 527 | if (!i || seen !== computed) result.push(value); 528 | seen = computed; 529 | } else if (iteratee) { 530 | if (!_.contains(seen, computed)) { 531 | seen.push(computed); 532 | result.push(value); 533 | } 534 | } else if (!_.contains(result, value)) { 535 | result.push(value); 536 | } 537 | } 538 | return result; 539 | }; 540 | 541 | // Produce an array that contains the union: each distinct element from all of 542 | // the passed-in arrays. 543 | _.union = function() { 544 | return _.uniq(flatten(arguments, true, true)); 545 | }; 546 | 547 | // Produce an array that contains every item shared between all the 548 | // passed-in arrays. 549 | _.intersection = function(array) { 550 | if (array == null) return []; 551 | var result = []; 552 | var argsLength = arguments.length; 553 | for (var i = 0, length = array.length; i < length; i++) { 554 | var item = array[i]; 555 | if (_.contains(result, item)) continue; 556 | for (var j = 1; j < argsLength; j++) { 557 | if (!_.contains(arguments[j], item)) break; 558 | } 559 | if (j === argsLength) result.push(item); 560 | } 561 | return result; 562 | }; 563 | 564 | // Take the difference between one array and a number of other arrays. 565 | // Only the elements present in just the first array will remain. 566 | _.difference = function(array) { 567 | var rest = flatten(arguments, true, true, 1); 568 | return _.filter(array, function(value){ 569 | return !_.contains(rest, value); 570 | }); 571 | }; 572 | 573 | // Zip together multiple lists into a single array -- elements that share 574 | // an index go together. 575 | _.zip = function() { 576 | return _.unzip(arguments); 577 | }; 578 | 579 | // Complement of _.zip. Unzip accepts an array of arrays and groups 580 | // each array's elements on shared indices 581 | _.unzip = function(array) { 582 | var length = array && _.max(array, 'length').length || 0; 583 | var result = Array(length); 584 | 585 | for (var index = 0; index < length; index++) { 586 | result[index] = _.pluck(array, index); 587 | } 588 | return result; 589 | }; 590 | 591 | // Converts lists into objects. Pass either a single array of `[key, value]` 592 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 593 | // the corresponding values. 594 | _.object = function(list, values) { 595 | var result = {}; 596 | for (var i = 0, length = list && list.length; i < length; i++) { 597 | if (values) { 598 | result[list[i]] = values[i]; 599 | } else { 600 | result[list[i][0]] = list[i][1]; 601 | } 602 | } 603 | return result; 604 | }; 605 | 606 | // Return the position of the first occurrence of an item in an array, 607 | // or -1 if the item is not included in the array. 608 | // If the array is large and already in sort order, pass `true` 609 | // for **isSorted** to use binary search. 610 | _.indexOf = function(array, item, isSorted) { 611 | var i = 0, length = array && array.length; 612 | if (typeof isSorted == 'number') { 613 | i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; 614 | } else if (isSorted && length) { 615 | i = _.sortedIndex(array, item); 616 | return array[i] === item ? i : -1; 617 | } 618 | if (item !== item) { 619 | return _.findIndex(slice.call(array, i), _.isNaN); 620 | } 621 | for (; i < length; i++) if (array[i] === item) return i; 622 | return -1; 623 | }; 624 | 625 | _.lastIndexOf = function(array, item, from) { 626 | var idx = array ? array.length : 0; 627 | if (typeof from == 'number') { 628 | idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); 629 | } 630 | if (item !== item) { 631 | return _.findLastIndex(slice.call(array, 0, idx), _.isNaN); 632 | } 633 | while (--idx >= 0) if (array[idx] === item) return idx; 634 | return -1; 635 | }; 636 | 637 | // Generator function to create the findIndex and findLastIndex functions 638 | function createIndexFinder(dir) { 639 | return function(array, predicate, context) { 640 | predicate = cb(predicate, context); 641 | var length = array != null && array.length; 642 | var index = dir > 0 ? 0 : length - 1; 643 | for (; index >= 0 && index < length; index += dir) { 644 | if (predicate(array[index], index, array)) return index; 645 | } 646 | return -1; 647 | }; 648 | } 649 | 650 | // Returns the first index on an array-like that passes a predicate test 651 | _.findIndex = createIndexFinder(1); 652 | 653 | _.findLastIndex = createIndexFinder(-1); 654 | 655 | // Use a comparator function to figure out the smallest index at which 656 | // an object should be inserted so as to maintain order. Uses binary search. 657 | _.sortedIndex = function(array, obj, iteratee, context) { 658 | iteratee = cb(iteratee, context, 1); 659 | var value = iteratee(obj); 660 | var low = 0, high = array.length; 661 | while (low < high) { 662 | var mid = Math.floor((low + high) / 2); 663 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; 664 | } 665 | return low; 666 | }; 667 | 668 | // Generate an integer Array containing an arithmetic progression. A port of 669 | // the native Python `range()` function. See 670 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 671 | _.range = function(start, stop, step) { 672 | if (arguments.length <= 1) { 673 | stop = start || 0; 674 | start = 0; 675 | } 676 | step = step || 1; 677 | 678 | var length = Math.max(Math.ceil((stop - start) / step), 0); 679 | var range = Array(length); 680 | 681 | for (var idx = 0; idx < length; idx++, start += step) { 682 | range[idx] = start; 683 | } 684 | 685 | return range; 686 | }; 687 | 688 | // Function (ahem) Functions 689 | // ------------------ 690 | 691 | // Determines whether to execute a function as a constructor 692 | // or a normal function with the provided arguments 693 | var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { 694 | if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 695 | var self = baseCreate(sourceFunc.prototype); 696 | var result = sourceFunc.apply(self, args); 697 | if (_.isObject(result)) return result; 698 | return self; 699 | }; 700 | 701 | // Create a function bound to a given object (assigning `this`, and arguments, 702 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 703 | // available. 704 | _.bind = function(func, context) { 705 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 706 | if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); 707 | var args = slice.call(arguments, 2); 708 | var bound = function() { 709 | return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); 710 | }; 711 | return bound; 712 | }; 713 | 714 | // Partially apply a function by creating a version that has had some of its 715 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 716 | // as a placeholder, allowing any combination of arguments to be pre-filled. 717 | _.partial = function(func) { 718 | var boundArgs = slice.call(arguments, 1); 719 | var bound = function() { 720 | var position = 0, length = boundArgs.length; 721 | var args = Array(length); 722 | for (var i = 0; i < length; i++) { 723 | args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 724 | } 725 | while (position < arguments.length) args.push(arguments[position++]); 726 | return executeBound(func, bound, this, this, args); 727 | }; 728 | return bound; 729 | }; 730 | 731 | // Bind a number of an object's methods to that object. Remaining arguments 732 | // are the method names to be bound. Useful for ensuring that all callbacks 733 | // defined on an object belong to it. 734 | _.bindAll = function(obj) { 735 | var i, length = arguments.length, key; 736 | if (length <= 1) throw new Error('bindAll must be passed function names'); 737 | for (i = 1; i < length; i++) { 738 | key = arguments[i]; 739 | obj[key] = _.bind(obj[key], obj); 740 | } 741 | return obj; 742 | }; 743 | 744 | // Memoize an expensive function by storing its results. 745 | _.memoize = function(func, hasher) { 746 | var memoize = function(key) { 747 | var cache = memoize.cache; 748 | var address = '' + (hasher ? hasher.apply(this, arguments) : key); 749 | if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 750 | return cache[address]; 751 | }; 752 | memoize.cache = {}; 753 | return memoize; 754 | }; 755 | 756 | // Delays a function for the given number of milliseconds, and then calls 757 | // it with the arguments supplied. 758 | _.delay = function(func, wait) { 759 | var args = slice.call(arguments, 2); 760 | return setTimeout(function(){ 761 | return func.apply(null, args); 762 | }, wait); 763 | }; 764 | 765 | // Defers a function, scheduling it to run after the current call stack has 766 | // cleared. 767 | _.defer = _.partial(_.delay, _, 1); 768 | 769 | // Returns a function, that, when invoked, will only be triggered at most once 770 | // during a given window of time. Normally, the throttled function will run 771 | // as much as it can, without ever going more than once per `wait` duration; 772 | // but if you'd like to disable the execution on the leading edge, pass 773 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 774 | _.throttle = function(func, wait, options) { 775 | var context, args, result; 776 | var timeout = null; 777 | var previous = 0; 778 | if (!options) options = {}; 779 | var later = function() { 780 | previous = options.leading === false ? 0 : _.now(); 781 | timeout = null; 782 | result = func.apply(context, args); 783 | if (!timeout) context = args = null; 784 | }; 785 | return function() { 786 | var now = _.now(); 787 | if (!previous && options.leading === false) previous = now; 788 | var remaining = wait - (now - previous); 789 | context = this; 790 | args = arguments; 791 | if (remaining <= 0 || remaining > wait) { 792 | if (timeout) { 793 | clearTimeout(timeout); 794 | timeout = null; 795 | } 796 | previous = now; 797 | result = func.apply(context, args); 798 | if (!timeout) context = args = null; 799 | } else if (!timeout && options.trailing !== false) { 800 | timeout = setTimeout(later, remaining); 801 | } 802 | return result; 803 | }; 804 | }; 805 | 806 | // Returns a function, that, as long as it continues to be invoked, will not 807 | // be triggered. The function will be called after it stops being called for 808 | // N milliseconds. If `immediate` is passed, trigger the function on the 809 | // leading edge, instead of the trailing. 810 | _.debounce = function(func, wait, immediate) { 811 | var timeout, args, context, timestamp, result; 812 | 813 | var later = function() { 814 | var last = _.now() - timestamp; 815 | 816 | if (last < wait && last >= 0) { 817 | timeout = setTimeout(later, wait - last); 818 | } else { 819 | timeout = null; 820 | if (!immediate) { 821 | result = func.apply(context, args); 822 | if (!timeout) context = args = null; 823 | } 824 | } 825 | }; 826 | 827 | return function() { 828 | context = this; 829 | args = arguments; 830 | timestamp = _.now(); 831 | var callNow = immediate && !timeout; 832 | if (!timeout) timeout = setTimeout(later, wait); 833 | if (callNow) { 834 | result = func.apply(context, args); 835 | context = args = null; 836 | } 837 | 838 | return result; 839 | }; 840 | }; 841 | 842 | // Returns the first function passed as an argument to the second, 843 | // allowing you to adjust arguments, run code before and after, and 844 | // conditionally execute the original function. 845 | _.wrap = function(func, wrapper) { 846 | return _.partial(wrapper, func); 847 | }; 848 | 849 | // Returns a negated version of the passed-in predicate. 850 | _.negate = function(predicate) { 851 | return function() { 852 | return !predicate.apply(this, arguments); 853 | }; 854 | }; 855 | 856 | // Returns a function that is the composition of a list of functions, each 857 | // consuming the return value of the function that follows. 858 | _.compose = function() { 859 | var args = arguments; 860 | var start = args.length - 1; 861 | return function() { 862 | var i = start; 863 | var result = args[start].apply(this, arguments); 864 | while (i--) result = args[i].call(this, result); 865 | return result; 866 | }; 867 | }; 868 | 869 | // Returns a function that will only be executed on and after the Nth call. 870 | _.after = function(times, func) { 871 | return function() { 872 | if (--times < 1) { 873 | return func.apply(this, arguments); 874 | } 875 | }; 876 | }; 877 | 878 | // Returns a function that will only be executed up to (but not including) the Nth call. 879 | _.before = function(times, func) { 880 | var memo; 881 | return function() { 882 | if (--times > 0) { 883 | memo = func.apply(this, arguments); 884 | } 885 | if (times <= 1) func = null; 886 | return memo; 887 | }; 888 | }; 889 | 890 | // Returns a function that will be executed at most one time, no matter how 891 | // often you call it. Useful for lazy initialization. 892 | _.once = _.partial(_.before, 2); 893 | 894 | // Object Functions 895 | // ---------------- 896 | 897 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. 898 | var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 899 | var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 900 | 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 901 | 902 | function collectNonEnumProps(obj, keys) { 903 | var nonEnumIdx = nonEnumerableProps.length; 904 | var constructor = obj.constructor; 905 | var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; 906 | 907 | // Constructor is a special case. 908 | var prop = 'constructor'; 909 | if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); 910 | 911 | while (nonEnumIdx--) { 912 | prop = nonEnumerableProps[nonEnumIdx]; 913 | if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { 914 | keys.push(prop); 915 | } 916 | } 917 | } 918 | 919 | // Retrieve the names of an object's own properties. 920 | // Delegates to **ECMAScript 5**'s native `Object.keys` 921 | _.keys = function(obj) { 922 | if (!_.isObject(obj)) return []; 923 | if (nativeKeys) return nativeKeys(obj); 924 | var keys = []; 925 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 926 | // Ahem, IE < 9. 927 | if (hasEnumBug) collectNonEnumProps(obj, keys); 928 | return keys; 929 | }; 930 | 931 | // Retrieve all the property names of an object. 932 | _.allKeys = function(obj) { 933 | if (!_.isObject(obj)) return []; 934 | var keys = []; 935 | for (var key in obj) keys.push(key); 936 | // Ahem, IE < 9. 937 | if (hasEnumBug) collectNonEnumProps(obj, keys); 938 | return keys; 939 | }; 940 | 941 | // Retrieve the values of an object's properties. 942 | _.values = function(obj) { 943 | var keys = _.keys(obj); 944 | var length = keys.length; 945 | var values = Array(length); 946 | for (var i = 0; i < length; i++) { 947 | values[i] = obj[keys[i]]; 948 | } 949 | return values; 950 | }; 951 | 952 | // Returns the results of applying the iteratee to each element of the object 953 | // In contrast to _.map it returns an object 954 | _.mapObject = function(obj, iteratee, context) { 955 | iteratee = cb(iteratee, context); 956 | var keys = _.keys(obj), 957 | length = keys.length, 958 | results = {}, 959 | currentKey; 960 | for (var index = 0; index < length; index++) { 961 | currentKey = keys[index]; 962 | results[currentKey] = iteratee(obj[currentKey], currentKey, obj); 963 | } 964 | return results; 965 | }; 966 | 967 | // Convert an object into a list of `[key, value]` pairs. 968 | _.pairs = function(obj) { 969 | var keys = _.keys(obj); 970 | var length = keys.length; 971 | var pairs = Array(length); 972 | for (var i = 0; i < length; i++) { 973 | pairs[i] = [keys[i], obj[keys[i]]]; 974 | } 975 | return pairs; 976 | }; 977 | 978 | // Invert the keys and values of an object. The values must be serializable. 979 | _.invert = function(obj) { 980 | var result = {}; 981 | var keys = _.keys(obj); 982 | for (var i = 0, length = keys.length; i < length; i++) { 983 | result[obj[keys[i]]] = keys[i]; 984 | } 985 | return result; 986 | }; 987 | 988 | // Return a sorted list of the function names available on the object. 989 | // Aliased as `methods` 990 | _.functions = _.methods = function(obj) { 991 | var names = []; 992 | for (var key in obj) { 993 | if (_.isFunction(obj[key])) names.push(key); 994 | } 995 | return names.sort(); 996 | }; 997 | 998 | // Extend a given object with all the properties in passed-in object(s). 999 | _.extend = createAssigner(_.allKeys); 1000 | 1001 | // Assigns a given object with all the own properties in the passed-in object(s) 1002 | // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 1003 | _.extendOwn = _.assign = createAssigner(_.keys); 1004 | 1005 | // Returns the first key on an object that passes a predicate test 1006 | _.findKey = function(obj, predicate, context) { 1007 | predicate = cb(predicate, context); 1008 | var keys = _.keys(obj), key; 1009 | for (var i = 0, length = keys.length; i < length; i++) { 1010 | key = keys[i]; 1011 | if (predicate(obj[key], key, obj)) return key; 1012 | } 1013 | }; 1014 | 1015 | // Return a copy of the object only containing the whitelisted properties. 1016 | _.pick = function(object, oiteratee, context) { 1017 | var result = {}, obj = object, iteratee, keys; 1018 | if (obj == null) return result; 1019 | if (_.isFunction(oiteratee)) { 1020 | keys = _.allKeys(obj); 1021 | iteratee = optimizeCb(oiteratee, context); 1022 | } else { 1023 | keys = flatten(arguments, false, false, 1); 1024 | iteratee = function(value, key, obj) { return key in obj; }; 1025 | obj = Object(obj); 1026 | } 1027 | for (var i = 0, length = keys.length; i < length; i++) { 1028 | var key = keys[i]; 1029 | var value = obj[key]; 1030 | if (iteratee(value, key, obj)) result[key] = value; 1031 | } 1032 | return result; 1033 | }; 1034 | 1035 | // Return a copy of the object without the blacklisted properties. 1036 | _.omit = function(obj, iteratee, context) { 1037 | if (_.isFunction(iteratee)) { 1038 | iteratee = _.negate(iteratee); 1039 | } else { 1040 | var keys = _.map(flatten(arguments, false, false, 1), String); 1041 | iteratee = function(value, key) { 1042 | return !_.contains(keys, key); 1043 | }; 1044 | } 1045 | return _.pick(obj, iteratee, context); 1046 | }; 1047 | 1048 | // Fill in a given object with default properties. 1049 | _.defaults = createAssigner(_.allKeys, true); 1050 | 1051 | // Create a (shallow-cloned) duplicate of an object. 1052 | _.clone = function(obj) { 1053 | if (!_.isObject(obj)) return obj; 1054 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 1055 | }; 1056 | 1057 | // Invokes interceptor with the obj, and then returns obj. 1058 | // The primary purpose of this method is to "tap into" a method chain, in 1059 | // order to perform operations on intermediate results within the chain. 1060 | _.tap = function(obj, interceptor) { 1061 | interceptor(obj); 1062 | return obj; 1063 | }; 1064 | 1065 | // Returns whether an object has a given set of `key:value` pairs. 1066 | _.isMatch = function(object, attrs) { 1067 | var keys = _.keys(attrs), length = keys.length; 1068 | if (object == null) return !length; 1069 | var obj = Object(object); 1070 | for (var i = 0; i < length; i++) { 1071 | var key = keys[i]; 1072 | if (attrs[key] !== obj[key] || !(key in obj)) return false; 1073 | } 1074 | return true; 1075 | }; 1076 | 1077 | 1078 | // Internal recursive comparison function for `isEqual`. 1079 | var eq = function(a, b, aStack, bStack) { 1080 | // Identical objects are equal. `0 === -0`, but they aren't identical. 1081 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 1082 | if (a === b) return a !== 0 || 1 / a === 1 / b; 1083 | // A strict comparison is necessary because `null == undefined`. 1084 | if (a == null || b == null) return a === b; 1085 | // Unwrap any wrapped objects. 1086 | if (a instanceof _) a = a._wrapped; 1087 | if (b instanceof _) b = b._wrapped; 1088 | // Compare `[[Class]]` names. 1089 | var className = toString.call(a); 1090 | if (className !== toString.call(b)) return false; 1091 | switch (className) { 1092 | // Strings, numbers, regular expressions, dates, and booleans are compared by value. 1093 | case '[object RegExp]': 1094 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') 1095 | case '[object String]': 1096 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 1097 | // equivalent to `new String("5")`. 1098 | return '' + a === '' + b; 1099 | case '[object Number]': 1100 | // `NaN`s are equivalent, but non-reflexive. 1101 | // Object(NaN) is equivalent to NaN 1102 | if (+a !== +a) return +b !== +b; 1103 | // An `egal` comparison is performed for other numeric values. 1104 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 1105 | case '[object Date]': 1106 | case '[object Boolean]': 1107 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 1108 | // millisecond representations. Note that invalid dates with millisecond representations 1109 | // of `NaN` are not equivalent. 1110 | return +a === +b; 1111 | } 1112 | 1113 | var areArrays = className === '[object Array]'; 1114 | if (!areArrays) { 1115 | if (typeof a != 'object' || typeof b != 'object') return false; 1116 | 1117 | // Objects with different constructors are not equivalent, but `Object`s or `Array`s 1118 | // from different frames are. 1119 | var aCtor = a.constructor, bCtor = b.constructor; 1120 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && 1121 | _.isFunction(bCtor) && bCtor instanceof bCtor) 1122 | && ('constructor' in a && 'constructor' in b)) { 1123 | return false; 1124 | } 1125 | } 1126 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 1127 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 1128 | 1129 | // Initializing stack of traversed objects. 1130 | // It's done here since we only need them for objects and arrays comparison. 1131 | aStack = aStack || []; 1132 | bStack = bStack || []; 1133 | var length = aStack.length; 1134 | while (length--) { 1135 | // Linear search. Performance is inversely proportional to the number of 1136 | // unique nested structures. 1137 | if (aStack[length] === a) return bStack[length] === b; 1138 | } 1139 | 1140 | // Add the first object to the stack of traversed objects. 1141 | aStack.push(a); 1142 | bStack.push(b); 1143 | 1144 | // Recursively compare objects and arrays. 1145 | if (areArrays) { 1146 | // Compare array lengths to determine if a deep comparison is necessary. 1147 | length = a.length; 1148 | if (length !== b.length) return false; 1149 | // Deep compare the contents, ignoring non-numeric properties. 1150 | while (length--) { 1151 | if (!eq(a[length], b[length], aStack, bStack)) return false; 1152 | } 1153 | } else { 1154 | // Deep compare objects. 1155 | var keys = _.keys(a), key; 1156 | length = keys.length; 1157 | // Ensure that both objects contain the same number of properties before comparing deep equality. 1158 | if (_.keys(b).length !== length) return false; 1159 | while (length--) { 1160 | // Deep compare each member 1161 | key = keys[length]; 1162 | if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; 1163 | } 1164 | } 1165 | // Remove the first object from the stack of traversed objects. 1166 | aStack.pop(); 1167 | bStack.pop(); 1168 | return true; 1169 | }; 1170 | 1171 | // Perform a deep comparison to check if two objects are equal. 1172 | _.isEqual = function(a, b) { 1173 | return eq(a, b); 1174 | }; 1175 | 1176 | // Is a given array, string, or object empty? 1177 | // An "empty" object has no enumerable own-properties. 1178 | _.isEmpty = function(obj) { 1179 | if (obj == null) return true; 1180 | if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; 1181 | return _.keys(obj).length === 0; 1182 | }; 1183 | 1184 | // Is a given value a DOM element? 1185 | _.isElement = function(obj) { 1186 | return !!(obj && obj.nodeType === 1); 1187 | }; 1188 | 1189 | // Is a given value an array? 1190 | // Delegates to ECMA5's native Array.isArray 1191 | _.isArray = nativeIsArray || function(obj) { 1192 | return toString.call(obj) === '[object Array]'; 1193 | }; 1194 | 1195 | // Is a given variable an object? 1196 | _.isObject = function(obj) { 1197 | var type = typeof obj; 1198 | return type === 'function' || type === 'object' && !!obj; 1199 | }; 1200 | 1201 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. 1202 | _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { 1203 | _['is' + name] = function(obj) { 1204 | return toString.call(obj) === '[object ' + name + ']'; 1205 | }; 1206 | }); 1207 | 1208 | // Define a fallback version of the method in browsers (ahem, IE < 9), where 1209 | // there isn't any inspectable "Arguments" type. 1210 | if (!_.isArguments(arguments)) { 1211 | _.isArguments = function(obj) { 1212 | return _.has(obj, 'callee'); 1213 | }; 1214 | } 1215 | 1216 | // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, 1217 | // IE 11 (#1621), and in Safari 8 (#1929). 1218 | if (typeof /./ != 'function' && typeof Int8Array != 'object') { 1219 | _.isFunction = function(obj) { 1220 | return typeof obj == 'function' || false; 1221 | }; 1222 | } 1223 | 1224 | // Is a given object a finite number? 1225 | _.isFinite = function(obj) { 1226 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1227 | }; 1228 | 1229 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1230 | _.isNaN = function(obj) { 1231 | return _.isNumber(obj) && obj !== +obj; 1232 | }; 1233 | 1234 | // Is a given value a boolean? 1235 | _.isBoolean = function(obj) { 1236 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; 1237 | }; 1238 | 1239 | // Is a given value equal to null? 1240 | _.isNull = function(obj) { 1241 | return obj === null; 1242 | }; 1243 | 1244 | // Is a given variable undefined? 1245 | _.isUndefined = function(obj) { 1246 | return obj === void 0; 1247 | }; 1248 | 1249 | // Shortcut function for checking if an object has a given property directly 1250 | // on itself (in other words, not on a prototype). 1251 | _.has = function(obj, key) { 1252 | return obj != null && hasOwnProperty.call(obj, key); 1253 | }; 1254 | 1255 | // Utility Functions 1256 | // ----------------- 1257 | 1258 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1259 | // previous owner. Returns a reference to the Underscore object. 1260 | _.noConflict = function() { 1261 | root._ = previousUnderscore; 1262 | return this; 1263 | }; 1264 | 1265 | // Keep the identity function around for default iteratees. 1266 | _.identity = function(value) { 1267 | return value; 1268 | }; 1269 | 1270 | // Predicate-generating functions. Often useful outside of Underscore. 1271 | _.constant = function(value) { 1272 | return function() { 1273 | return value; 1274 | }; 1275 | }; 1276 | 1277 | _.noop = function(){}; 1278 | 1279 | _.property = function(key) { 1280 | return function(obj) { 1281 | return obj == null ? void 0 : obj[key]; 1282 | }; 1283 | }; 1284 | 1285 | // Generates a function for a given object that returns a given property. 1286 | _.propertyOf = function(obj) { 1287 | return obj == null ? function(){} : function(key) { 1288 | return obj[key]; 1289 | }; 1290 | }; 1291 | 1292 | // Returns a predicate for checking whether an object has a given set of 1293 | // `key:value` pairs. 1294 | _.matcher = _.matches = function(attrs) { 1295 | attrs = _.extendOwn({}, attrs); 1296 | return function(obj) { 1297 | return _.isMatch(obj, attrs); 1298 | }; 1299 | }; 1300 | 1301 | // Run a function **n** times. 1302 | _.times = function(n, iteratee, context) { 1303 | var accum = Array(Math.max(0, n)); 1304 | iteratee = optimizeCb(iteratee, context, 1); 1305 | for (var i = 0; i < n; i++) accum[i] = iteratee(i); 1306 | return accum; 1307 | }; 1308 | 1309 | // Return a random integer between min and max (inclusive). 1310 | _.random = function(min, max) { 1311 | if (max == null) { 1312 | max = min; 1313 | min = 0; 1314 | } 1315 | return min + Math.floor(Math.random() * (max - min + 1)); 1316 | }; 1317 | 1318 | // A (possibly faster) way to get the current timestamp as an integer. 1319 | _.now = Date.now || function() { 1320 | return new Date().getTime(); 1321 | }; 1322 | 1323 | // List of HTML entities for escaping. 1324 | var escapeMap = { 1325 | '&': '&', 1326 | '<': '<', 1327 | '>': '>', 1328 | '"': '"', 1329 | "'": ''', 1330 | '`': '`' 1331 | }; 1332 | var unescapeMap = _.invert(escapeMap); 1333 | 1334 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1335 | var createEscaper = function(map) { 1336 | var escaper = function(match) { 1337 | return map[match]; 1338 | }; 1339 | // Regexes for identifying a key that needs to be escaped 1340 | var source = '(?:' + _.keys(map).join('|') + ')'; 1341 | var testRegexp = RegExp(source); 1342 | var replaceRegexp = RegExp(source, 'g'); 1343 | return function(string) { 1344 | string = string == null ? '' : '' + string; 1345 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 1346 | }; 1347 | }; 1348 | _.escape = createEscaper(escapeMap); 1349 | _.unescape = createEscaper(unescapeMap); 1350 | 1351 | // If the value of the named `property` is a function then invoke it with the 1352 | // `object` as context; otherwise, return it. 1353 | _.result = function(object, property, fallback) { 1354 | var value = object == null ? void 0 : object[property]; 1355 | if (value === void 0) { 1356 | value = fallback; 1357 | } 1358 | return _.isFunction(value) ? value.call(object) : value; 1359 | }; 1360 | 1361 | // Generate a unique integer id (unique within the entire client session). 1362 | // Useful for temporary DOM ids. 1363 | var idCounter = 0; 1364 | _.uniqueId = function(prefix) { 1365 | var id = ++idCounter + ''; 1366 | return prefix ? prefix + id : id; 1367 | }; 1368 | 1369 | // By default, Underscore uses ERB-style template delimiters, change the 1370 | // following template settings to use alternative delimiters. 1371 | _.templateSettings = { 1372 | evaluate : /<%([\s\S]+?)%>/g, 1373 | interpolate : /<%=([\s\S]+?)%>/g, 1374 | escape : /<%-([\s\S]+?)%>/g 1375 | }; 1376 | 1377 | // When customizing `templateSettings`, if you don't want to define an 1378 | // interpolation, evaluation or escaping regex, we need one that is 1379 | // guaranteed not to match. 1380 | var noMatch = /(.)^/; 1381 | 1382 | // Certain characters need to be escaped so that they can be put into a 1383 | // string literal. 1384 | var escapes = { 1385 | "'": "'", 1386 | '\\': '\\', 1387 | '\r': 'r', 1388 | '\n': 'n', 1389 | '\u2028': 'u2028', 1390 | '\u2029': 'u2029' 1391 | }; 1392 | 1393 | var escaper = /\\|'|\r|\n|\u2028|\u2029/g; 1394 | 1395 | var escapeChar = function(match) { 1396 | return '\\' + escapes[match]; 1397 | }; 1398 | 1399 | // JavaScript micro-templating, similar to John Resig's implementation. 1400 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1401 | // and correctly escapes quotes within interpolated code. 1402 | // NB: `oldSettings` only exists for backwards compatibility. 1403 | _.template = function(text, settings, oldSettings) { 1404 | if (!settings && oldSettings) settings = oldSettings; 1405 | settings = _.defaults({}, settings, _.templateSettings); 1406 | 1407 | // Combine delimiters into one regular expression via alternation. 1408 | var matcher = RegExp([ 1409 | (settings.escape || noMatch).source, 1410 | (settings.interpolate || noMatch).source, 1411 | (settings.evaluate || noMatch).source 1412 | ].join('|') + '|$', 'g'); 1413 | 1414 | // Compile the template source, escaping string literals appropriately. 1415 | var index = 0; 1416 | var source = "__p+='"; 1417 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1418 | source += text.slice(index, offset).replace(escaper, escapeChar); 1419 | index = offset + match.length; 1420 | 1421 | if (escape) { 1422 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1423 | } else if (interpolate) { 1424 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1425 | } else if (evaluate) { 1426 | source += "';\n" + evaluate + "\n__p+='"; 1427 | } 1428 | 1429 | // Adobe VMs need the match returned to produce the correct offest. 1430 | return match; 1431 | }); 1432 | source += "';\n"; 1433 | 1434 | // If a variable is not specified, place data values in local scope. 1435 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1436 | 1437 | source = "var __t,__p='',__j=Array.prototype.join," + 1438 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1439 | source + 'return __p;\n'; 1440 | 1441 | try { 1442 | var render = new Function(settings.variable || 'obj', '_', source); 1443 | } catch (e) { 1444 | e.source = source; 1445 | throw e; 1446 | } 1447 | 1448 | var template = function(data) { 1449 | return render.call(this, data, _); 1450 | }; 1451 | 1452 | // Provide the compiled source as a convenience for precompilation. 1453 | var argument = settings.variable || 'obj'; 1454 | template.source = 'function(' + argument + '){\n' + source + '}'; 1455 | 1456 | return template; 1457 | }; 1458 | 1459 | // Add a "chain" function. Start chaining a wrapped Underscore object. 1460 | _.chain = function(obj) { 1461 | var instance = _(obj); 1462 | instance._chain = true; 1463 | return instance; 1464 | }; 1465 | 1466 | // OOP 1467 | // --------------- 1468 | // If Underscore is called as a function, it returns a wrapped object that 1469 | // can be used OO-style. This wrapper holds altered versions of all the 1470 | // underscore functions. Wrapped objects may be chained. 1471 | 1472 | // Helper function to continue chaining intermediate results. 1473 | var result = function(instance, obj) { 1474 | return instance._chain ? _(obj).chain() : obj; 1475 | }; 1476 | 1477 | // Add your own custom functions to the Underscore object. 1478 | _.mixin = function(obj) { 1479 | _.each(_.functions(obj), function(name) { 1480 | var func = _[name] = obj[name]; 1481 | _.prototype[name] = function() { 1482 | var args = [this._wrapped]; 1483 | push.apply(args, arguments); 1484 | return result(this, func.apply(_, args)); 1485 | }; 1486 | }); 1487 | }; 1488 | 1489 | // Add all of the Underscore functions to the wrapper object. 1490 | _.mixin(_); 1491 | 1492 | // Add all mutator Array functions to the wrapper. 1493 | _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1494 | var method = ArrayProto[name]; 1495 | _.prototype[name] = function() { 1496 | var obj = this._wrapped; 1497 | method.apply(obj, arguments); 1498 | if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; 1499 | return result(this, obj); 1500 | }; 1501 | }); 1502 | 1503 | // Add all accessor Array functions to the wrapper. 1504 | _.each(['concat', 'join', 'slice'], function(name) { 1505 | var method = ArrayProto[name]; 1506 | _.prototype[name] = function() { 1507 | return result(this, method.apply(this._wrapped, arguments)); 1508 | }; 1509 | }); 1510 | 1511 | // Extracts the result from a wrapped and chained object. 1512 | _.prototype.value = function() { 1513 | return this._wrapped; 1514 | }; 1515 | 1516 | // Provide unwrapping proxy for some methods used in engine operations 1517 | // such as arithmetic and JSON stringification. 1518 | _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; 1519 | 1520 | _.prototype.toString = function() { 1521 | return '' + this._wrapped; 1522 | }; 1523 | 1524 | // AMD registration happens at the end for compatibility with AMD loaders 1525 | // that may not enforce next-turn semantics on modules. Even though general 1526 | // practice for AMD registration is to be anonymous, underscore registers 1527 | // as a named module because, like jQuery, it is a base library that is 1528 | // popular enough to be bundled in a third party lib, but not be part of 1529 | // an AMD load request. Those cases could generate an error when an 1530 | // anonymous define() is called outside of a loader request. 1531 | if (typeof define === 'function' && define.amd) { 1532 | define('underscore', [], function() { 1533 | return _; 1534 | }); 1535 | } 1536 | }.call(this)); 1537 | --------------------------------------------------------------------------------