├── .gitignore ├── src ├── js │ ├── _merge.txt │ ├── compiled │ │ ├── calendar.js │ │ ├── history.js │ │ └── underscore.js │ ├── calendar.js │ ├── history.js │ └── underscore.js └── css │ ├── history.less │ ├── _popup.less │ ├── _reset.less │ ├── _calendar.less │ ├── _dark.less │ └── _global.less ├── logo ├── dark.png ├── icon.png ├── logo.png ├── light.png └── small_promo.png ├── .gitattributes ├── LICENSE ├── README.md └── .github └── ISSUE_TEMPLATE └── bug_report.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /src/js/_merge.txt: -------------------------------------------------------------------------------- 1 | jquery.js 2 | underscore.js 3 | moment.js 4 | calendar.js 5 | history.js -------------------------------------------------------------------------------- /logo/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonofmercy/Dragon-Better-History/HEAD/logo/dark.png -------------------------------------------------------------------------------- /logo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonofmercy/Dragon-Better-History/HEAD/logo/icon.png -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonofmercy/Dragon-Better-History/HEAD/logo/logo.png -------------------------------------------------------------------------------- /logo/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonofmercy/Dragon-Better-History/HEAD/logo/light.png -------------------------------------------------------------------------------- /logo/small_promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragonofmercy/Dragon-Better-History/HEAD/logo/small_promo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | * text eol=lf 3 | 4 | *.php eol=lf 5 | *.js eol=lf 6 | *.css eol=lf 7 | 8 | *.png binary 9 | *.jpg binary 10 | *.gif binary -------------------------------------------------------------------------------- /src/css/history.less: -------------------------------------------------------------------------------- 1 | @backgound-color: #f7f7f7; 2 | @backgound-color-dark: #333333; 3 | 4 | @grey-btn-background: #ededed; 5 | @grey-btn-background-dark: #4d4d4d; 6 | 7 | @link-text-color: #0072c9; 8 | @link-text-color-dark: #4ea1e1; 9 | 10 | @text-color: #262626; 11 | @text-color-dark: #bbb; 12 | 13 | @text-grey-color: #717171; 14 | @text-grey-color-dark: #9d9d9d; 15 | 16 | @input-background: #ffffff; 17 | @input-background-dark: #2b2b2b; 18 | 19 | @border-color: #bebebe; 20 | @border-color-dark: #3b3b3b; 21 | 22 | @panel-background-color: #ffffff; 23 | @panel-background-color-dark: #3b3b3b; 24 | 25 | @import "_reset"; 26 | @import "_calendar"; 27 | @import "_global"; 28 | @import "_popup"; 29 | @import "_dark"; -------------------------------------------------------------------------------- /src/css/_popup.less: -------------------------------------------------------------------------------- 1 | body { 2 | &.popup { 3 | .wrapper { 4 | width: 650px; 5 | 6 | .history-container { 7 | padding: 20px; 8 | 9 | h1 { 10 | font-size: 16px; 11 | margin-bottom: 10px; 12 | } 13 | 14 | h2 { 15 | display: none; 16 | } 17 | 18 | #go_history { 19 | float: right; 20 | margin-top: 7px; 21 | } 22 | 23 | .history-day { 24 | .entry { 25 | .entry-link { 26 | width: 450px; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dragon 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 | # Dragon-Better-History 2 | 3 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://ko-fi.com/dragonofmercy) 4 | 5 | Better history for Microsoft Edge Chromium 6 | 7 | ## How to compile less 8 | 9 | Compile all less files with 10 | ```sh 11 | less $ProjectFileDir$/src/css/history.less $ProjectFileDir$/build/assets/application.css --clean-css="--s0 --advanced 12 | ``` 13 | 14 | ## How to compile javascript 15 | 16 | Compile all javascript files to the compile folder using this command: 17 | ```sh 18 | terser $FileName$ --output compiled/$FileNameWithoutExtension$.js --comments false 19 | ``` 20 | 21 | Then execute in powershell merge all file into a single javascript file: 22 | ```pwsh 23 | powershell Get-Content .\src\js\_merge.txt | foreach { Get-Content .\src\js\compiled\$_ } | Set-Content .\build\assets\application.js 24 | ``` 25 | 26 | If you need to add more javascript file don't forget to add it inside "_merge.txt" file. 27 | 28 | ## 29 | 30 | If this project help to increase your productivity, you can give me a cup of coffee :) 31 | 32 | [![Donate](https://cdn.ko-fi.com/cdn/kofi2.png?v=3)](https://ko-fi.com/dragonofmercy) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug report" 2 | description: Report errors or unexpected behavior 3 | labels: 4 | - bug 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please make sure to [search for existing issues](https://github.com/dragonofmercy/Dragon-Better-History/issues) before filing a new one! 10 | I accepte only issue from the Edge version of the extension, I am not responsible for the development of the Chrome version. 11 | Note: Please stop asking me for **Chrome Better History**, The project has been discontinued. 12 | - type: textarea 13 | attributes: 14 | label: Steps to reproduce 15 | description: We highly suggest including screenshots or code sample. 16 | placeholder: Tell us the steps required to trigger your bug. 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: ✔️ Expected Behavior 23 | placeholder: What were you expecting? 24 | validations: 25 | required: false 26 | 27 | - type: textarea 28 | attributes: 29 | label: ❌ Actual Behavior 30 | placeholder: What happened instead? 31 | validations: 32 | required: false 33 | 34 | - type: input 35 | attributes: 36 | label: Microsoft Edge Version 37 | description: Please fill your Edge version. Can be found by typing this "edge://settings/help" in your address bar. 38 | validations: 39 | required: true 40 | -------------------------------------------------------------------------------- /src/css/_reset.less: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -ms-text-size-adjust: 100%; 4 | -webkit-text-size-adjust: 100%; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | } 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | menu, 21 | nav, 22 | section, 23 | summary { 24 | display: block; 25 | } 26 | 27 | audio, 28 | canvas, 29 | progress, 30 | video { 31 | display: inline-block; 32 | vertical-align: baseline; 33 | } 34 | 35 | audio:not([controls]) { 36 | display: none; 37 | height: 0; 38 | } 39 | 40 | [hidden], 41 | template { 42 | display: none; 43 | } 44 | 45 | a { 46 | background-color: transparent; 47 | } 48 | 49 | a:active, 50 | a:hover { 51 | outline: 0; 52 | } 53 | 54 | abbr[title] { 55 | border-bottom: none; 56 | text-decoration: underline; 57 | text-decoration: underline dotted; 58 | } 59 | 60 | b, 61 | strong { 62 | font-weight: bold; 63 | } 64 | 65 | dfn { 66 | font-style: italic; 67 | } 68 | 69 | h1 { 70 | font-size: 2em; 71 | margin: 0.67em 0; 72 | } 73 | 74 | mark { 75 | background: #ff0; 76 | color: #000; 77 | } 78 | 79 | small { 80 | font-size: 80%; 81 | } 82 | 83 | sub, 84 | sup { 85 | font-size: 75%; 86 | line-height: 0; 87 | position: relative; 88 | vertical-align: baseline; 89 | } 90 | 91 | sup { 92 | top: -0.5em; 93 | } 94 | 95 | sub { 96 | bottom: -0.25em; 97 | } 98 | 99 | img { 100 | border: 0; 101 | } 102 | 103 | svg:not(:root) { 104 | overflow: hidden; 105 | } 106 | 107 | figure { 108 | margin: 1em 40px; 109 | } 110 | 111 | hr { 112 | box-sizing: content-box; 113 | height: 0; 114 | } 115 | 116 | pre { 117 | overflow: auto; 118 | } 119 | 120 | code, 121 | kbd, 122 | pre, 123 | samp { 124 | font-family: monospace, monospace; 125 | font-size: 1em; 126 | } 127 | 128 | button, 129 | input, 130 | optgroup, 131 | select, 132 | textarea { 133 | color: inherit; 134 | font: inherit; 135 | margin: 0; 136 | } 137 | 138 | button { 139 | overflow: visible; 140 | } 141 | 142 | button, 143 | select { 144 | text-transform: none; 145 | } 146 | 147 | button, 148 | html input[type="button"], // 1 149 | input[type="reset"], 150 | input[type="submit"] { 151 | -webkit-appearance: button; // 2 152 | cursor: pointer; // 3 153 | } 154 | 155 | button[disabled], 156 | html input[disabled] { 157 | cursor: default; 158 | } 159 | 160 | button::-moz-focus-inner, 161 | input::-moz-focus-inner { 162 | border: 0; 163 | padding: 0; 164 | } 165 | 166 | input { 167 | line-height: normal; 168 | } 169 | 170 | input[type="checkbox"], 171 | input[type="radio"] { 172 | box-sizing: border-box; 173 | padding: 0; 174 | } 175 | 176 | input[type="number"]::-webkit-inner-spin-button, 177 | input[type="number"]::-webkit-outer-spin-button { 178 | height: auto; 179 | } 180 | 181 | input[type="search"] { 182 | -webkit-appearance: textfield; // 1 183 | box-sizing: content-box; //2 184 | } 185 | 186 | input[type="search"]::-webkit-search-cancel-button, 187 | input[type="search"]::-webkit-search-decoration { 188 | -webkit-appearance: none; 189 | } 190 | 191 | fieldset { 192 | border: 1px solid #c0c0c0; 193 | margin: 0 2px; 194 | padding: 0.35em 0.625em 0.75em; 195 | } 196 | 197 | legend { 198 | border: 0; 199 | padding: 0; 200 | } 201 | 202 | textarea { 203 | overflow: auto; 204 | } 205 | 206 | optgroup { 207 | font-weight: bold; 208 | } 209 | 210 | table { 211 | border-collapse: collapse; 212 | border-spacing: 0; 213 | } 214 | 215 | td, 216 | th { 217 | padding: 0; 218 | } 219 | -------------------------------------------------------------------------------- /src/css/_calendar.less: -------------------------------------------------------------------------------- 1 | /* Ion.Calendar 2 | // Version 2.0.1, build: 91 3 | // © 2013 Denis Ineshin | IonDen.com 4 | // 5 | // Project page: http://ionden.com/a/plugins/ion.calendar/ 6 | // GitHub page: https://github.com/IonDen/ion.calendar 7 | // 8 | // Released under MIT licence: 9 | // http://ionden.com/a/plugins/licence-en.html 10 | // ===================================================================================================================*/ 11 | 12 | /* ic = ion.calendar prefix */ 13 | .ic__container, 14 | .ic__week-head, 15 | .ic__week-head td, 16 | .ic__days, 17 | .ic__days td, 18 | .ic__header, 19 | .ic__prev, 20 | .ic__next, 21 | .ic__datepicker { 22 | margin: 0; 23 | padding: 0; 24 | font-size: 10px; 25 | line-height: 12px; 26 | cursor: default; 27 | text-shadow: none !important; 28 | } 29 | 30 | .ic__container { 31 | position: relative; 32 | background: @panel-background-color; 33 | } 34 | 35 | .ic__container table { 36 | width: 100%; 37 | border: 0; 38 | border-collapse: collapse !important; 39 | border-spacing: 0 !important; 40 | } 41 | 42 | 43 | .ic__header { 44 | position: relative; 45 | background: @border-color; 46 | height: 35px; 47 | } 48 | 49 | .ic__prev, .ic__next { 50 | position: absolute; 51 | top: 0; 52 | width: 7%; 53 | height: 35px; 54 | background-color: @border-color; 55 | cursor: pointer; 56 | } 57 | 58 | .ic__prev:hover, .ic__next:hover { 59 | background-color: darken(@border-color, 20%); 60 | } 61 | 62 | .ic__prev div, .ic__next div { 63 | position: absolute; 64 | width: 0; 65 | height: 0; 66 | overflow: hidden; 67 | border: 5px solid transparent; 68 | } 69 | 70 | .ic__prev { 71 | left: 0; 72 | } 73 | 74 | .ic__prev div { 75 | top: 13px; 76 | left: 50%; 77 | margin-left: -7px; 78 | border-right-color: #fff; 79 | } 80 | 81 | .ic__next { 82 | right: 0; 83 | } 84 | 85 | .ic__next div { 86 | top: 13px; 87 | right: 50%; 88 | margin-right: -7px; 89 | border-left-color: #fff; 90 | } 91 | 92 | 93 | .ic__month { 94 | position: absolute; 95 | top: 5px; 96 | left: 9%; 97 | width: 50%; 98 | } 99 | 100 | .ic__year { 101 | position: absolute; 102 | top: 5px; 103 | right: 9%; 104 | width: 30%; 105 | } 106 | 107 | .ic__header select { 108 | width: 100%; 109 | border: none; 110 | padding: 2px; 111 | font-size: 14px; 112 | border-radius: 2px; 113 | } 114 | 115 | .ic__header select:focus { 116 | outline: none; 117 | } 118 | 119 | .ic__week-head td { 120 | width: 14%; 121 | vertical-align: top; 122 | text-align: center; 123 | padding: 10px 0 8px; 124 | 125 | border: 1px solid lighten(@border-color, 10%); 126 | color: @text-grey-color; 127 | font-size: 9px; 128 | line-height: 1.333; 129 | text-transform: uppercase; 130 | } 131 | 132 | 133 | .ic__days { 134 | margin-top: -1px; 135 | } 136 | 137 | .ic__days td { 138 | width: 14%; 139 | vertical-align: top; 140 | text-align: center; 141 | padding: 10px 0 8px; 142 | 143 | border: 1px solid lighten(@border-color, 10%); 144 | color: @text-color; 145 | font-size: 9px; 146 | line-height: 1.333; 147 | text-transform: uppercase; 148 | } 149 | 150 | .ic__days .ic__day { 151 | background: @panel-background-color; 152 | cursor: pointer; 153 | } 154 | 155 | .ic__days .ic__day:hover { 156 | background: lighten(@link-text-color, 40%); 157 | color: #fff; 158 | } 159 | 160 | .ic__days .ic__day-empty { 161 | background-image: linear-gradient(135deg, 162 | @panel-background-color 25%, 163 | lighten(@border-color, 20%) 25%, 164 | lighten(@border-color, 20%) 50%, 165 | @panel-background-color 50%, 166 | @panel-background-color 75%, 167 | lighten(@border-color, 20%) 75%, 168 | lighten(@border-color, 20%) 100%); 169 | 170 | background-size: 5px 5px; 171 | } 172 | 173 | .ic__days .ic__day_state_current { 174 | background: @link-text-color; 175 | color: #fff; 176 | } 177 | 178 | .ic__days .ic__day_state_selected { 179 | background: lighten(@border-color, 20%); 180 | color: #000; 181 | } 182 | 183 | .ic__month-select, 184 | .ic__year-select { 185 | background-color: @input-background; 186 | } -------------------------------------------------------------------------------- /src/js/compiled/calendar.js: -------------------------------------------------------------------------------- 1 | (function($){try{var timeNow=moment()}catch(e){alert("Can't find Moment.js, please read the ion.calendar description.");throw new Error("Can't find Moment.js library")}var methods={init:function(options){var settings=$.extend({lang:"en",sundayFirst:true,years:"80",format:"",clickable:true,startDate:"",hideArrows:false,onClick:null,onReady:null},options),html,i;return this.each((function(){var $calendar=$(this);if($calendar.data("isActive")){return}$calendar.data("isActive",true);var $prev,$next,$month,$year,$day,timeSelected,timeNowLocal=moment(timeNow.locale(settings.lang)),timeForWork,weekFirstDay,weekLastDay,monthLastDay,tempYears,fromYear,toYear,firstStart=true;this.updateData=function(options){settings=$.extend(settings,options);removeHTML()};var removeHTML=function(){$prev.off();$next.off();$month.off();$year.off();$calendar.empty();prepareData();prepareCalendar()};var prepareData=function(){if(settings.startDate){if(settings.format.indexOf("L")>=0){timeSelected=moment(settings.startDate,"YYYY.MM.DD").locale(settings.lang)}else{timeSelected=moment(settings.startDate,settings.format).locale(settings.lang)}}settings.years=settings.years.toString();tempYears=settings.years.split("-");if(tempYears.length===1){fromYear=moment().subtract(tempYears[0],"years").format("YYYY");toYear=moment().format("YYYY")}else if(tempYears.length===2){fromYear=tempYears[0];toYear=tempYears[1]}fromYear=parseInt(fromYear);toYear=parseInt(toYear);if(toYeartimeNowLocal.format("YYYY")){timeNowLocal.year(fromYear).month(0)}};var prepareCalendar=function(){timeForWork=moment(timeNowLocal);weekFirstDay=parseInt(timeForWork.startOf("month").format("d"));weekLastDay=parseInt(timeForWork.endOf("month").format("d"));monthLastDay=parseInt(timeForWork.endOf("month").format("D"));html='
';html+='
';html+='
';html+='
';html+='
";html+='
";html+="
";if(settings.sundayFirst){html+='';for(i=0;i<7;i++){html+=""}html+="
"+timeForWork.day(i).format("dd")+"
";html+='';for(i=0;i '}for(i=1;i<=monthLastDay;i++){if(moment(timeNowLocal).date(i).format("D.M.YYYY")===timeNow.format("D.M.YYYY")){html+='"}else if(timeSelected&&moment(timeNowLocal).date(i).format("D.M.YYYY")===timeSelected.format("D.M.YYYY")){html+='"}else{html+='"}if((weekFirstDay+i)/7===Math.floor((weekFirstDay+i)/7)){html+=""}}for(i=weekLastDay;i<6;i++){html+=''}html+="
'+i+"'+i+"'+i+"
 
"}else{html+='';for(i=1;i<8;i++){if(i<7){html+=""}else{html+=""}}html+="
"+timeForWork.day(i).format("dd")+""+timeForWork.day(0).format("dd")+"
";html+='';if(weekFirstDay>0){weekFirstDay=weekFirstDay-1}else{weekFirstDay=6}for(i=0;i '}for(i=1;i<=monthLastDay;i++){if(moment(timeNowLocal).date(i).format("D.M.YYYY")===timeNow.format("D.M.YYYY")){html+='"}else if(timeSelected&&moment(timeNowLocal).date(i).format("D.M.YYYY")===timeSelected.format("D.M.YYYY")){html+='"}else{html+='"}if((weekFirstDay+i)/7===Math.floor((weekFirstDay+i)/7)){html+=""}}if(weekLastDay<1){weekLastDay=7}for(i=weekLastDay-1;i<6;i++){html+=''}html+="
'+i+"'+i+"'+i+"
 
"}html+="
";placeCalendar()};var placeCalendar=function(){$calendar.html(html);$prev=$calendar.find(".ic__prev");$next=$calendar.find(".ic__next");$month=$calendar.find(".ic__month-select");$year=$calendar.find(".ic__year-select");$day=$calendar.find(".ic__day");if(settings.hideArrows){$prev[0].style.display="none";$next[0].style.display="none"}else{$prev.on("click",(function(e){e.preventDefault();timeNowLocal.subtract(1,"months");if(parseInt(timeNowLocal.format("YYYY"))toYear){timeNowLocal.subtract(1,"months")}removeHTML()}))}$month.on("change",(function(e){e.preventDefault();var toMonth=$(this).prop("value");timeNowLocal.month(parseInt(toMonth));removeHTML()}));$year.on("change",(function(e){e.preventDefault();var toYear=$(this).prop("value");timeNowLocal.year(parseInt(toYear));removeHTML()}));if(settings.clickable){$day.on("click",(function(e){e.preventDefault();var toDay=$(this).text();timeNowLocal.date(parseInt(toDay));timeSelected=moment(timeNowLocal);if(settings.format.indexOf("L")>=0){settings.startDate=timeSelected.format("YYYY-MM-DD")}else{settings.startDate=timeSelected.format(settings.format)}if(typeof settings.onClick==="function"){if(settings.format){if(settings.format==="moment"){settings.onClick.call(this,timeSelected)}else{settings.onClick.call(this,timeSelected.format(settings.format))}}else{settings.onClick.call(this,timeSelected.format())}}removeHTML()}))}if(typeof settings.onReady==="function"){if(settings.format){if(settings.format==="moment"){settings.onReady.call(this,timeNowLocal)}else{settings.onReady.call(this,timeNowLocal.format(settings.format))}}else{settings.onReady.call(this,timeNowLocal.format())}}if(settings.startDate&&firstStart){firstStart=false;timeNowLocal.year(parseInt(timeSelected.format("YYYY")));timeNowLocal.month(parseInt(timeSelected.format("M")-1));removeHTML()}};prepareData();prepareCalendar()}))},update:function(options){return this.each((function(){this.updateData(options)}))}};$.fn.ionCalendar=function(method){if(methods[method]){return methods[method].apply(this,Array.prototype.slice.call(arguments,1))}else if(typeof method==="object"||!method){return methods.init.apply(this,arguments)}else{$.error("Method "+method+" does not exist for jQuery.ionCalendar")}}})(jQuery); -------------------------------------------------------------------------------- /src/css/_dark.less: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: dark) { 2 | 3 | :root { 4 | color-scheme: dark; 5 | } 6 | 7 | body { 8 | background-color: @backgound-color-dark; 9 | color: @text-color-dark; 10 | 11 | a { 12 | color: @link-text-color-dark; 13 | 14 | &:hover{ 15 | border-bottom-color: @link-text-color-dark; 16 | } 17 | } 18 | 19 | .btn { 20 | 21 | &.btn-primary { 22 | &:hover { 23 | background-color: darken(@link-text-color-dark, 5%); 24 | } 25 | } 26 | 27 | &.btn-grey { 28 | background-color: @grey-btn-background-dark; 29 | 30 | &:hover { 31 | background-color: lighten(@grey-btn-background-dark, 5%); 32 | } 33 | } 34 | } 35 | 36 | .entry { 37 | background-color: @panel-background-color-dark; 38 | 39 | &.selected { 40 | box-shadow: @link-text-color-dark 0 0 0 1.5px; 41 | background-color: @grey-btn-background-dark; 42 | } 43 | 44 | .entry-link { 45 | a { 46 | color: @text-color-dark; 47 | } 48 | } 49 | 50 | .entry-time { 51 | color: @text-grey-color-dark; 52 | } 53 | 54 | .entry-remove { 55 | svg { 56 | fill: @text-color-dark; 57 | } 58 | 59 | &:hover { 60 | background-color: @backgound-color-dark; 61 | } 62 | } 63 | } 64 | 65 | &.main { 66 | .wrapper { 67 | .sidebar { 68 | background-color: @backgound-color-dark; 69 | border-right-color: @border-color-dark; 70 | 71 | .container { 72 | 73 | &>.search { 74 | &>.content { 75 | .search-structure { 76 | background-color: @input-background-dark; 77 | border-color: @border-color-dark; 78 | 79 | &.focus { 80 | box-shadow: 0 0 0 1px lighten(@border-color-dark, 10%) inset; 81 | border-color: lighten(@border-color-dark, 10%); 82 | } 83 | 84 | svg { 85 | fill: @text-color-dark; 86 | } 87 | 88 | .search-clear { 89 | &:hover { 90 | background-color: @backgound-color-dark; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | &>.footer { 98 | border-top-color: @border-color-dark; 99 | } 100 | } 101 | } 102 | 103 | .history-container { 104 | .gutter { 105 | .remove-confirmation { 106 | background-color: @panel-background-color-dark; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | .form-group { 114 | 115 | input[type=number] { 116 | background-color: @backgound-color-dark; 117 | border-color: #858585; 118 | outline: none; 119 | } 120 | 121 | .toggle-switch { 122 | input { 123 | background-color: @backgound-color-dark; 124 | border-color: #858585; 125 | } 126 | 127 | .dot { 128 | background-color: #fff; 129 | } 130 | } 131 | } 132 | 133 | .modal { 134 | .modal-content { 135 | background-color: @panel-background-color-dark; 136 | 137 | .modal-heading { 138 | .close { 139 | svg { 140 | fill: @text-color-dark; 141 | } 142 | &:hover { 143 | background-color: @backgound-color-dark; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | .ic__container { 152 | background: darken(@panel-background-color-dark, 5%); 153 | } 154 | 155 | .ic__month-select, 156 | .ic__year-select { 157 | background-color: @input-background-dark; 158 | } 159 | 160 | .ic__prev div { 161 | border-right-color: @text-color-dark; 162 | } 163 | 164 | .ic__next div { 165 | border-left-color: @text-color-dark; 166 | } 167 | 168 | .ic__header, 169 | .ic__prev, 170 | .ic__next { 171 | background-color: @panel-background-color-dark; 172 | } 173 | 174 | .ic__prev:hover, 175 | .ic__next:hover { 176 | background-color: lighten(@border-color-dark, 10%); 177 | } 178 | 179 | .ic__week-head td { 180 | border: 1px solid @border-color-dark; 181 | color: @text-grey-color-dark; 182 | } 183 | 184 | .ic__days td { 185 | border: 1px solid @border-color-dark; 186 | color: @text-color-dark; 187 | } 188 | 189 | .ic__days .ic__day { 190 | background: lighten(@backgound-color-dark, .5%); 191 | } 192 | 193 | .ic__days .ic__day:hover { 194 | background: darken(@link-text-color-dark, 30%); 195 | } 196 | 197 | .ic__days .ic__day-empty { 198 | background-image: linear-gradient(135deg, 199 | darken(@border-color-dark, 2%) 25%, 200 | darken(@border-color-dark, 5%) 25%, 201 | darken(@border-color-dark, 5%) 50%, 202 | darken(@border-color-dark, 2%) 50%, 203 | darken(@border-color-dark, 2%) 75%, 204 | darken(@border-color-dark, 5%) 75%, 205 | darken(@border-color-dark, 5%) 100%); 206 | 207 | background-size: 5px 5px; 208 | } 209 | 210 | .ic__days .ic__day_state_current { 211 | background: darken(@link-text-color-dark, 20%); 212 | } 213 | 214 | .ic__days .ic__day_state_selected { 215 | background: lighten(@border-color-dark, 10%); 216 | color: @text-color-dark; 217 | } 218 | } -------------------------------------------------------------------------------- /src/js/compiled/history.js: -------------------------------------------------------------------------------- 1 | var application={KEYS:{A:65},now:new Date,options:{use24HoursFormat:true,timeBeforeTitle:false,popupNbItems:10},today:null,isSearching:false,isLoading:false,autoFocus:false,trottleSearch:null,controlPressed:false,majPressed:false,multiSelectLastEntry:null,init:function(){let $this=this;moment.locale(this.getCurrentLocale());this.i18n();this.today=new Date(this.now.getFullYear(),this.now.getMonth(),this.now.getDate(),0,0,0,0);chrome.storage.sync.get((function(items){$this.options.use24HoursFormat=items.use24HoursFormat===undefined?true:items.use24HoursFormat;$this.options.timeBeforeTitle=items.timeBeforeTitle===undefined?false:items.timeBeforeTitle;$this.options.popupNbItems=items.popupNbItems===undefined?10:items.popupNbItems;if($("body.popup").length){$this.initPopup()}else{$this.initMain()}}))},initPopup:function(){this.historyGetDay(this.today,this.options.popupNbItems);$("#go_history").on("click",(function(e){e.preventDefault();chrome.tabs.create({url:"chrome://history"})}))},initMain:function(){let $this=this;this.trottleSearch=_.debounce(this.historySearch,500);if($(window).width()==360){chrome.tabs.create({url:"chrome://history"})}$("#go_options").on("click",(function(){$this.openOptions()}));$("#options_cancel, #options_close").on("click",(function(){$this.closeOptions()}));$("#options_save").on("click",(function(){$this.saveOptions()}));$("#search_input").focusin((function(){$(this).parent().addClass("focus")})).focusout((function(){$(this).parent().removeClass("focus")})).on("keyup",(function(){$this.inputSearch()}));$("#search_clear").on("click",(function(e){e.preventDefault();$this.clearSearch()}));this.initCalendar();$("#go_today").on("click",(function(e){e.preventDefault();$this.historyGetDay($this.today);$this.initCalendar()}));$(".remove-confirmation>#clear_confirm").on("click",(function(e){e.preventDefault();$this.removeSelectedEntires()}));$(".remove-confirmation>#clear_cancel").on("click",(function(e){e.preventDefault();$this.clearSelectedItems()}));$(document).keydown((function(e){if(e.which==17){$this.controlPressed=true}else if(e.which==16){$this.majPressed=true}else{$this.keypressMulti(e)}})).keyup((function(e){if(e.which==17){$this.controlPressed=false}else if(e.which==16){$this.majPressed=false}})).click((function(e){if(!$this.is(e.target,"entry")&&!$this.is(e.target,"remove-confirmation")){$this.clearSelectedItems()}}));$("#search_input").focus();this.historyGetDay(this.today)},initCalendar:function(){let $this=this;if($(".sidebar .calendar .ic__container").length){$(".sidebar .calendar").remove()}let calendar=$("
").addClass("calendar");$(".sidebar .nav .content").prepend(calendar);calendar.ionCalendar({lang:this.getCurrentLocale(),sundayFirst:moment.localeData().firstDayOfWeek()==0,startDate:this.today,years:this.now.getFullYear()-3+"-"+this.now.getFullYear(),onClick:function(date){$this.historyGetDay(new Date(date))}})},inputSearch:function(){let search_query=$("#search_input").val();if(search_query.toString()!==""){$("#search_clear").css("display","flex");this.trottleSearch(search_query)}else{if(this.isSearching){this.clearContent();this.historyGetDay(this.today)}this.trottleSearch.cancel();this.isSearching=false;$("#search_clear").hide()}},clearSearch:function(){$("#search_input").val("");this.inputSearch()},historySearch:function(query){this.isSearching=true;this.clearContent();this.historyQuery(query,new Date(1970,1,1,0,0,0,0),new Date(this.today.getFullYear(),this.today.getMonth(),this.today.getDate(),23,59,59),0)},historyGetDay:function(day,nb_entries){nb_entries=nb_entries||0;if(day>this.now){return}let date_start=new Date(day.getFullYear(),day.getMonth(),day.getDate(),0,0,0,0);let date_end=new Date(day.getFullYear(),day.getMonth(),day.getDate(),23,59,59);this.clearContent();this.historyQuery("",date_start,date_end,parseInt(nb_entries))},historyQuery:function(search,start,end,nb_entries){let $this=this;this.isLoading=true;chrome.history.search({text:search,startTime:start.getTime(),endTime:end.getTime(),maxResults:nb_entries},(function(results){$this.historyCallback(results,start,end)}))},historyCallback:function(results,start,end){let items={};let count=0;$.each(results,(function(k,v){let item_date=new Date(v.lastVisitTime);if(item_date>=start&&item_date<=end){let item_key=new Date(item_date.getFullYear(),item_date.getMonth(),item_date.getDate(),0,0,0,0).getTime().toString();if(!items[item_key]){items[item_key]=[]}items[item_key].push(v);count++}}));if($.isEmptyObject(items)){items[start.getTime()]={}}this.historyFormatDays(items,count)},historyFormatDays:function(items,count){let $this=this;this.clearContent();if(this.isSearching){this.insertContent("

"+chrome.i18n.getMessage("search_display")+' "'+$("#search_input").val()+'"

');if(count>0){this.insertContent('
'+chrome.i18n.getMessage("search_found",count.toString())+"
")}else{this.insertContent('
'+chrome.i18n.getMessage("search_empty")+"
");this.isLoading=false;return}}$.each(items,(function(k,day){let html="";k=k.toString();if($(".wrapper .history-container #"+k).length<1){html+='
'}html+="

"+moment(new Date(parseFloat(k.toString()))).format(chrome.i18n.getMessage("date_format"))+"

";if(Object.keys(day).length>0){$.each(day,(function(z,entry){html+=$this.historyEntryFormat(entry)}))}else{html+='
'+chrome.i18n.getMessage("history_date_empty")+"
"}if($(".wrapper .history-container #"+k).length<1){html+="
"}$this.insertContent(html)}));this.historyEntriesBind();this.isLoading=false},historyEntryFormat:function(entry){let html="";html+='
';if(this.options.timeBeforeTitle){html+='
'+moment(new Date(entry.lastVisitTime)).format(this.options.use24HoursFormat?"HH:mm":"hh:mm A")+"
"}html+='';html+='
'+this.escape(entry.title?entry.title:entry.url)+"
";if(!this.options.timeBeforeTitle){html+='
'+moment(new Date(entry.lastVisitTime)).format(this.options.use24HoursFormat?"HH:mm":"hh:mm A")+"
"}html+='';html+="
";return html},historyEntryDelete:function(url,sender){let $this=this;chrome.history.deleteUrl({url:url},(function(){let container=sender.parent().parent();sender.parent().remove();if($(".entry",container).length===0){$this.insertContent('
'+chrome.i18n.getMessage("history_date_empty")+"
",container)}$this.updateConfirm()}))},historyEntriesBind:function(){let $this=this;$(".history-container .entry>.entry-remove").unbind();$(".history-container .entry>.entry-remove").on("click",(function(e){e.preventDefault();$this.historyEntryDelete($(this).parent().find(".entry-link>a").attr("href"),$(this))}));$(".history-container .entry",$("body.main")).unbind();$(".history-container .entry",$("body.main")).on("click",(function(e){if(!$(e.target).is("a")&&!$this.is(e.target,"entry-remove")){e.preventDefault();if($this.majPressed&&$this.multiSelectLastEntry!==null){let tmp=$this.multiSelectLastEntry;document.getSelection().removeAllRanges();if($(this).offset().top>$this.multiSelectLastEntry.offset().top){while($(this).offset().top>tmp.offset().top){tmp=tmp.next();tmp.addClass("selected")}}else{while(tmp.offset().top>$(this).offset().top){tmp=tmp.prev();tmp.addClass("selected")}}}else{$this.multiSelectLastEntry=$(this);if($(this).hasClass("selected")){$(this).removeClass("selected")}else{$(this).addClass("selected")}}$this.updateConfirm()}}))},keypressMulti:function(e){if($(e.target).attr("id")!=="search_input"){if(this.controlPressed&&e.which==this.KEYS.A){e.preventDefault();$(".history-container .entry").addClass("selected");this.updateConfirm()}}if(e.which==46){if($(".history-container .entry.selected").length>0){this.removeSelectedEntires()}}},updateConfirm:function(){let count=$(".history-container .entry.selected").length;if(count>0){$(".remove-confirmation").show();$(".remove-confirmation>.num").html(count.toString())}else{$(".remove-confirmation").hide()}},clearSelectedItems:function(){$(".history-container .entry.selected").removeClass("selected");this.multiSelectLastEntry=null;this.updateConfirm()},removeSelectedEntires:function(){$(".history-container .entry.selected").each((function(){$(this).find(".entry-remove").click()}))},insertContent:function(html,context){context=context||null;if(context!==null){context.append(html)}else{$(".wrapper .history-container .content").append(html)}},clearContent:function(){$(".wrapper .history-container .content").html("")},i18n:function(){$("[i18n]").each((function(){let i18n=$(this).attr("i18n");if(i18n.indexOf(":")>=0){let tmp=i18n.split(":");$(this).attr(tmp[0],chrome.i18n.getMessage(tmp[1]))}else{$(this).html(chrome.i18n.getMessage(i18n))}}))},is:function(target,classname){return!!($(target).hasClass(classname)||$(target).parents("."+classname).length>0)},getCurrentLanguage:function(){return this.getCurrentLocale().substr(0,2)},getCurrentLocale:function(){return chrome.i18n.getMessage("language")},getFavicon:function(url){return"chrome-extension://"+chrome.runtime.id+"/_favicon/?size=32&pageUrl="+encodeURIComponent(this.escape(url))},openOptions:function(){$("#options_field_24hoursformat").prop("checked",this.options.use24HoursFormat);$("#options_field_displaytitlebeforetime").prop("checked",this.options.timeBeforeTitle);$("#options_field_popupnbitems").val(this.options.popupNbItems);$("#modal_options").css("display","flex")},closeOptions:function(reload){$("#modal_options").css("display","none");if(reload){location.reload()}},saveOptions:function(){let $this=this;chrome.storage.sync.set({use24HoursFormat:$("#options_field_24hoursformat").prop("checked"),timeBeforeTitle:$("#options_field_displaytitlebeforetime").prop("checked"),popupNbItems:$("#options_field_popupnbitems").val()},(function(){$this.closeOptions(true)}))},escape:function(string){return string.replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}};$((function(){application.init()})); -------------------------------------------------------------------------------- /src/css/_global.less: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | font-family: 'system-ui', sans-serif; 4 | font-size: 14px; 5 | color: @text-color; 6 | background-color: @backgound-color; 7 | 8 | & * { 9 | box-sizing: border-box; 10 | -webkit-box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | font-size: 24px; 15 | line-height: 32px; 16 | font-weight: 600; 17 | margin: 0 0 20px 0; 18 | } 19 | 20 | h2 { 21 | font-size: 20px; 22 | line-height: 28px; 23 | font-weight: 600; 24 | margin: 4px 0 15px 0; 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | color: @link-text-color; 30 | 31 | &:hover{ 32 | border-bottom: 2px solid @link-text-color; 33 | cursor: pointer; 34 | } 35 | } 36 | 37 | .btn { 38 | display: inline-block; 39 | vertical-align: middle; 40 | height: 32px; 41 | min-width: 95px; 42 | font-weight: 600; 43 | font-size: 14px; 44 | line-height: 20px; 45 | padding: 0 10px; 46 | border-radius: 2px; 47 | border: 2px solid transparent; 48 | outline: none; 49 | 50 | &.btn-primary { 51 | background-color: @link-text-color; 52 | color: #fff; 53 | 54 | &:hover { 55 | background-color: lighten(@link-text-color, 5%); 56 | } 57 | } 58 | 59 | &.btn-grey { 60 | background-color: @grey-btn-background; 61 | 62 | &:hover { 63 | background-color: darken(@grey-btn-background, 5%); 64 | } 65 | } 66 | } 67 | 68 | .entry { 69 | display: flex; 70 | align-items: center; 71 | margin-bottom: 5px; 72 | background-color: @panel-background-color; 73 | border-radius: 4px; 74 | box-shadow: 0 1.6px 3.6px rgba(0,0,0,0.13), 0 0.3px 0.9px rgba(0,0,0,0.11); 75 | 76 | &:last-child { 77 | margin-bottom: 0; 78 | } 79 | 80 | &.selected { 81 | box-shadow: 0 0 0 1.5px @link-text-color; 82 | background-color: @grey-btn-background; 83 | } 84 | 85 | .entry-icon, 86 | .entry-time, 87 | .entry-link, 88 | .entry-remove { 89 | display: inline-block; 90 | vertical-align: middle; 91 | } 92 | 93 | .entry-icon { 94 | width: 16px; 95 | height: 16px; 96 | margin: 12px; 97 | } 98 | 99 | .entry-link { 100 | overflow: hidden; 101 | text-overflow: ellipsis; 102 | white-space: nowrap; 103 | line-height: 24px; 104 | flex-grow: 1; 105 | padding-right: 10px; 106 | 107 | a { 108 | color: @text-color; 109 | border: none; 110 | font-weight: 600; 111 | } 112 | } 113 | 114 | .entry-time { 115 | text-align: right; 116 | padding: 0 10px; 117 | color: @text-grey-color; 118 | line-height: 40px; 119 | font-size: 12px; 120 | cursor: default; 121 | } 122 | 123 | :first-child.entry-time { 124 | text-align: left; 125 | } 126 | 127 | .entry-remove { 128 | margin: 6px; 129 | padding: 8px; 130 | border: none; 131 | 132 | svg { 133 | fill: #767676; 134 | width: 12px; 135 | height: 12px; 136 | float: left; 137 | } 138 | 139 | &:hover { 140 | background-color: @backgound-color; 141 | border-radius: 2px; 142 | } 143 | } 144 | } 145 | 146 | &.main { 147 | .wrapper { 148 | .sidebar { 149 | position: fixed; 150 | width: 340px; 151 | border-right: 1px solid @border-color; 152 | background-color: @backgound-color; 153 | 154 | .container { 155 | display: flex; 156 | flex-direction: column; 157 | height: 100vh; 158 | padding: 30px 25px 30px 50px; 159 | 160 | &>.search, 161 | &>.nav, 162 | &>.footer { 163 | display: flex; 164 | 165 | &>.content { 166 | width: 100%; 167 | } 168 | } 169 | 170 | &>.search { 171 | &>.content { 172 | .search-structure { 173 | background-color: @input-background; 174 | border: 1px solid @border-color; 175 | border-radius: 2px; 176 | display: flex; 177 | transition: all 0.2s ease-in-out; 178 | margin-bottom: 20px; 179 | 180 | &.focus { 181 | box-shadow: 0 0 0 1px darken(@border-color, 10%) inset; 182 | border-color: darken(@border-color, 10%); 183 | } 184 | 185 | svg { 186 | fill: @text-color; 187 | } 188 | 189 | .search-icon { 190 | width: 16px; 191 | height: 16px; 192 | margin: 8px 12px; 193 | } 194 | 195 | .search-input { 196 | border: none; 197 | background: transparent; 198 | outline: none; 199 | padding-right: 5px; 200 | flex: 1 0 0; 201 | } 202 | 203 | .search-clear { 204 | display: none; 205 | margin: 4px; 206 | padding: 6px; 207 | border: none; 208 | 209 | svg { 210 | width: 12px; 211 | height: 12px; 212 | float: left; 213 | } 214 | 215 | &:hover { 216 | background-color: @backgound-color; 217 | border-radius: 2px; 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | &>.nav { 225 | flex-grow: 1; 226 | 227 | &>.content { 228 | text-align: center; 229 | padding-bottom: 20px; 230 | display: flex; 231 | flex-direction: column; 232 | 233 | .calendar { 234 | flex-grow: 0; 235 | 236 | &:after { 237 | display: table; 238 | content: " "; 239 | clear: both; 240 | } 241 | } 242 | 243 | p { 244 | flex-grow: 0; 245 | min-height: 22px; 246 | 247 | &:last-child { 248 | margin-bottom: 0; 249 | } 250 | 251 | &.space { 252 | flex-grow: 1; 253 | } 254 | 255 | #go_today, #go_options { 256 | display: inline-block; 257 | } 258 | } 259 | } 260 | } 261 | 262 | &>.footer { 263 | border-top: 1px solid @border-color; 264 | padding-top: 20px; 265 | 266 | &>.content { 267 | img { 268 | float: left; 269 | width: 40px; 270 | height: 40px; 271 | margin-right: 15px; 272 | } 273 | 274 | div { 275 | display: inline-block; 276 | width: 200px; 277 | } 278 | 279 | a { 280 | font-size: 12px; 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | .history-container { 288 | margin-left: 340px; 289 | padding: 30px 50px; 290 | 291 | .gutter { 292 | position: relative; 293 | width: 820px; 294 | 295 | .remove-confirmation { 296 | display: none; 297 | position: absolute; 298 | right: 0; 299 | margin-top: -15px; 300 | background-color: @panel-background-color; 301 | box-shadow: 0 1.6px 3.6px rgba(0,0,0,0.13), 0 0.3px 0.9px rgba(0,0,0,0.11); 302 | padding: 10px 10px 10px 20px; 303 | border-radius: 5px; 304 | 305 | span { 306 | display: inline-block; 307 | vertical-align: middle; 308 | } 309 | 310 | .btn-primary { 311 | margin-left: 10px; 312 | margin-right: 5px; 313 | } 314 | } 315 | } 316 | 317 | .search-result { 318 | margin-bottom: 20px; 319 | } 320 | 321 | .history-day { 322 | margin-bottom: 20px; 323 | 324 | .entry { 325 | width: 820px; 326 | 327 | .entry-link { 328 | width: 660px; 329 | } 330 | } 331 | } 332 | } 333 | } 334 | } 335 | 336 | .entry, 337 | .search-clear, 338 | .calendar, 339 | #go_today { 340 | -webkit-user-select: none; 341 | -moz-user-select: none; 342 | -ms-user-select: none; 343 | user-select: none; 344 | } 345 | 346 | .form-group { 347 | margin-bottom: 20px; 348 | display: flex; 349 | 350 | label { 351 | flex-grow: 1; 352 | } 353 | 354 | input[type=number] { 355 | background-color: @panel-background-color; 356 | border: 1px solid @border-color; 357 | outline: none; 358 | } 359 | 360 | .toggle-switch { 361 | position: relative; 362 | 363 | input { 364 | appearance: none; 365 | cursor: pointer; 366 | display: block; 367 | outline: none; 368 | width: 40px; 369 | height: 20px; 370 | background-color: @panel-background-color; 371 | border: 1px solid @border-color; 372 | border-radius: 20px; 373 | } 374 | 375 | .dot { 376 | position: absolute; 377 | transition: all .1s ease; 378 | pointer-events: none; 379 | top: 5px; 380 | left: 5px; 381 | right: unset; 382 | border-radius: 10px; 383 | width: 10px; 384 | height: 10px; 385 | background: #2B2B2B; 386 | } 387 | 388 | input:checked { 389 | background-color: @link-text-color; 390 | border-color: @link-text-color; 391 | &+.dot { 392 | left: 25px; 393 | right: unset; 394 | background: @panel-background-color; 395 | } 396 | } 397 | } 398 | 399 | 400 | } 401 | 402 | .modal { 403 | position: fixed; 404 | display: none; 405 | justify-content: center; 406 | z-index: 1; 407 | left: 0; 408 | top: 0; 409 | bottom: 0; 410 | right: 0; 411 | overflow: hidden; 412 | background-color: rgb(0,0,0); 413 | background-color: rgba(0,0,0,0.4); 414 | 415 | .modal-content { 416 | background-color: @panel-background-color; 417 | width: 400px; 418 | border-radius: 4px; 419 | height: auto; 420 | margin-top: auto; 421 | margin-bottom: auto; 422 | 423 | .modal-heading { 424 | display: flex; 425 | justify-content: flex-end; 426 | padding: 5px 5px 0 5px; 427 | 428 | .close { 429 | display: flex; 430 | padding: 3px; 431 | 432 | &:hover { 433 | background-color: @backgound-color; 434 | border-radius: 2px; 435 | cursor: pointer; 436 | } 437 | } 438 | } 439 | 440 | .modal-body { 441 | padding: 0 20px 10px 20px; 442 | } 443 | 444 | .modal-footer { 445 | padding: 0 20px 20px 20px; 446 | display: flex; 447 | 448 | button { 449 | flex-grow: 1; 450 | 451 | &:first-child { 452 | margin-right: 5px; 453 | } 454 | 455 | &:last-child { 456 | margin-left: 5px; 457 | } 458 | } 459 | } 460 | } 461 | } 462 | } 463 | 464 | -------------------------------------------------------------------------------- /src/js/calendar.js: -------------------------------------------------------------------------------- 1 | // Ion.Calendar 2 | // version 2.0.2, build: 92 3 | // © 2013 Denis Ineshin | IonDen.com 4 | // 5 | // Project page: http://ionden.com/a/plugins/ion.calendar/ 6 | // GitHub page: https://github.com/IonDen/ion.calendar 7 | // 8 | // Released under MIT licence: 9 | // http://ionden.com/a/plugins/licence-en.html 10 | // ===================================================================================================================== 11 | (function($){ 12 | try { 13 | var timeNow = moment(); 14 | } catch(e){ 15 | alert("Can't find Moment.js, please read the ion.calendar description."); 16 | throw new Error("Can't find Moment.js library"); 17 | } 18 | 19 | var methods = { 20 | init: function(options){ 21 | var settings = $.extend({ 22 | lang: "en", 23 | sundayFirst: true, 24 | years: "80", 25 | format: "", 26 | clickable: true, 27 | startDate: "", 28 | hideArrows: false, 29 | onClick: null, 30 | onReady: null 31 | }, options), 32 | html, i; 33 | 34 | 35 | return this.each(function(){ 36 | var $calendar = $(this); 37 | 38 | //prevent overwrite 39 | if($calendar.data("isActive")) { 40 | return; 41 | } 42 | $calendar.data("isActive", true); 43 | 44 | 45 | 46 | var $prev, 47 | $next, 48 | $month, 49 | $year, 50 | $day, 51 | 52 | timeSelected, 53 | timeNowLocal = moment(timeNow.locale(settings.lang)), 54 | timeForWork, 55 | weekFirstDay, 56 | weekLastDay, 57 | monthLastDay, 58 | 59 | tempYears, 60 | fromYear, 61 | toYear, 62 | firstStart = true; 63 | 64 | 65 | 66 | // public methods 67 | this.updateData = function(options){ 68 | settings = $.extend(settings, options); 69 | removeHTML(); 70 | }; 71 | 72 | 73 | 74 | // private methods 75 | var removeHTML = function(){ 76 | $prev.off(); 77 | $next.off(); 78 | $month.off(); 79 | $year.off(); 80 | $calendar.empty(); 81 | 82 | prepareData(); 83 | prepareCalendar(); 84 | }; 85 | 86 | var prepareData = function(){ 87 | // start date 88 | if(settings.startDate) { 89 | if(settings.format.indexOf("L") >= 0) { 90 | timeSelected = moment(settings.startDate, "YYYY.MM.DD").locale(settings.lang); 91 | } else { 92 | timeSelected = moment(settings.startDate, settings.format).locale(settings.lang); 93 | } 94 | } 95 | 96 | 97 | // years diapason 98 | settings.years = settings.years.toString(); 99 | tempYears = settings.years.split("-"); 100 | if(tempYears.length === 1) { 101 | fromYear = moment().subtract(tempYears[0], "years").format("YYYY"); 102 | toYear = moment().format("YYYY"); 103 | } else if(tempYears.length === 2){ 104 | fromYear = tempYears[0]; 105 | toYear = tempYears[1]; 106 | } 107 | fromYear = parseInt(fromYear); 108 | toYear = parseInt(toYear); 109 | 110 | if(toYear < timeNowLocal.format("YYYY")) { 111 | timeNowLocal.year(toYear).month(11); 112 | } 113 | if(fromYear > timeNowLocal.format("YYYY")) { 114 | timeNowLocal.year(fromYear).month(0); 115 | } 116 | }; 117 | 118 | var prepareCalendar = function(){ 119 | timeForWork = moment(timeNowLocal); 120 | 121 | weekFirstDay = parseInt(timeForWork.startOf("month").format("d")); 122 | weekLastDay = parseInt(timeForWork.endOf("month").format("d")); 123 | monthLastDay = parseInt(timeForWork.endOf("month").format("D")); 124 | 125 | html = '
'; 126 | html += '
'; 127 | html += '
'; 128 | html += '
'; 129 | 130 | // head month 131 | html += '
'; 140 | 141 | // head year 142 | html += '
'; 151 | 152 | html += '
'; 153 | 154 | if(settings.sundayFirst) { 155 | 156 | // week 157 | html += ''; 158 | for(i = 0; i < 7; i++) { 159 | html += ''; 160 | } 161 | html += '
' + timeForWork.day(i).format("dd") + '
'; 162 | 163 | // month 164 | html += ''; 165 | // empty days 166 | for(i = 0; i < weekFirstDay; i++) { 167 | html += ''; 168 | } 169 | // days 170 | for(i = 1; i <= monthLastDay; i++) { 171 | // current day 172 | if(moment(timeNowLocal).date(i).format("D.M.YYYY") === timeNow.format("D.M.YYYY")) { 173 | html += ''; 174 | } else if(timeSelected && moment(timeNowLocal).date(i).format("D.M.YYYY") === timeSelected.format("D.M.YYYY")) { 175 | html += ''; 176 | } else { 177 | html += ''; 178 | } 179 | 180 | // new week - new line 181 | if((weekFirstDay + i) / 7 === Math.floor((weekFirstDay + i) / 7)) { 182 | html += ''; 183 | } 184 | } 185 | // empty days 186 | for(i = weekLastDay; i < 6; i++) { 187 | html += ''; 188 | } 189 | html += '
 ' + i + '' + i + '' + i + '
 
'; 190 | 191 | } else { 192 | 193 | // week 194 | html += ''; 195 | for(i = 1; i < 8; i++) { 196 | if(i < 7) { 197 | html += ''; 198 | } else { 199 | html += ''; 200 | } 201 | } 202 | html += '
' + timeForWork.day(i).format("dd") + '' + timeForWork.day(0).format("dd") + '
'; 203 | 204 | // days 205 | html += ''; 206 | 207 | // empty days 208 | if(weekFirstDay > 0) { 209 | weekFirstDay = weekFirstDay - 1; 210 | } else { 211 | weekFirstDay = 6; 212 | } 213 | for(i = 0; i < weekFirstDay; i++) { 214 | html += ''; 215 | } 216 | 217 | for(i = 1; i <= monthLastDay; i++) { 218 | // current day 219 | if(moment(timeNowLocal).date(i).format("D.M.YYYY") === timeNow.format("D.M.YYYY")) { 220 | html += ''; 221 | } else if(timeSelected && moment(timeNowLocal).date(i).format("D.M.YYYY") === timeSelected.format("D.M.YYYY")) { 222 | html += ''; 223 | } else { 224 | html += ''; 225 | } 226 | 227 | // new week - new line 228 | if((weekFirstDay + i) / 7 === Math.floor((weekFirstDay + i) / 7)) { 229 | html += ''; 230 | } 231 | } 232 | // empty days 233 | if(weekLastDay < 1) { 234 | weekLastDay = 7; 235 | } 236 | for(i = weekLastDay - 1; i < 6; i++) { 237 | html += ''; 238 | } 239 | html += '
 ' + i + '' + i + '' + i + '
 
'; 240 | } 241 | 242 | html += '
'; 243 | 244 | 245 | placeCalendar(); 246 | }; 247 | 248 | var placeCalendar = function(){ 249 | $calendar.html(html); 250 | 251 | $prev = $calendar.find(".ic__prev"); 252 | $next = $calendar.find(".ic__next"); 253 | $month = $calendar.find(".ic__month-select"); 254 | $year = $calendar.find(".ic__year-select"); 255 | $day = $calendar.find(".ic__day"); 256 | 257 | if(settings.hideArrows) { 258 | $prev[0].style.display = "none"; 259 | $next[0].style.display = "none"; 260 | } else { 261 | $prev.on("click", function(e){ 262 | e.preventDefault(); 263 | timeNowLocal.subtract(1, "months"); 264 | if(parseInt(timeNowLocal.format("YYYY")) < fromYear) { 265 | timeNowLocal.add("months", 1); 266 | } 267 | removeHTML(); 268 | }); 269 | $next.on("click", function(e){ 270 | e.preventDefault(); 271 | timeNowLocal.add("months", 1); 272 | if(parseInt(timeNowLocal.format("YYYY")) > toYear) { 273 | timeNowLocal.subtract(1, "months"); 274 | } 275 | removeHTML(); 276 | }); 277 | } 278 | 279 | $month.on("change", function(e){ 280 | e.preventDefault(); 281 | var toMonth = $(this).prop("value"); 282 | timeNowLocal.month(parseInt(toMonth)); 283 | removeHTML(); 284 | }); 285 | $year.on("change", function(e){ 286 | e.preventDefault(); 287 | var toYear = $(this).prop("value"); 288 | timeNowLocal.year(parseInt(toYear)); 289 | removeHTML(); 290 | }); 291 | 292 | if(settings.clickable) { 293 | $day.on("click", function(e){ 294 | e.preventDefault(); 295 | var toDay = $(this).text(); 296 | timeNowLocal.date(parseInt(toDay)); 297 | timeSelected = moment(timeNowLocal); 298 | if(settings.format.indexOf("L") >= 0) { 299 | settings.startDate = timeSelected.format("YYYY-MM-DD"); 300 | } else { 301 | settings.startDate = timeSelected.format(settings.format); 302 | } 303 | 304 | // trigger callback function 305 | if(typeof settings.onClick === "function") { 306 | if(settings.format) { 307 | if(settings.format === "moment") { 308 | settings.onClick.call(this, timeSelected); 309 | } else { 310 | settings.onClick.call(this, timeSelected.format(settings.format)); 311 | } 312 | } else { 313 | settings.onClick.call(this, timeSelected.format()); 314 | } 315 | } 316 | 317 | removeHTML(); 318 | }); 319 | } 320 | 321 | // trigger onReady function 322 | if(typeof settings.onReady === "function") { 323 | if(settings.format) { 324 | if(settings.format === "moment") { 325 | settings.onReady.call(this, timeNowLocal); 326 | } else { 327 | settings.onReady.call(this, timeNowLocal.format(settings.format)); 328 | } 329 | } else { 330 | settings.onReady.call(this, timeNowLocal.format()); 331 | } 332 | } 333 | 334 | // go to startDate 335 | if(settings.startDate && firstStart) { 336 | firstStart = false; 337 | timeNowLocal.year(parseInt(timeSelected.format("YYYY"))); 338 | timeNowLocal.month(parseInt(timeSelected.format("M") - 1)); 339 | removeHTML(); 340 | } 341 | }; 342 | 343 | 344 | 345 | // yarrr! 346 | prepareData(); 347 | prepareCalendar(); 348 | }); 349 | }, 350 | update: function(options){ 351 | return this.each(function(){ 352 | this.updateData(options); 353 | }); 354 | } 355 | }; 356 | 357 | $.fn.ionCalendar = function(method){ 358 | if (methods[method]) { 359 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 360 | } else if (typeof method === 'object' || !method) { 361 | return methods.init.apply(this, arguments); 362 | } else { 363 | $.error('Method ' + method + ' does not exist for jQuery.ionCalendar'); 364 | } 365 | }; 366 | })(jQuery); 367 | -------------------------------------------------------------------------------- /src/js/history.js: -------------------------------------------------------------------------------- 1 | var application = { 2 | 3 | KEYS: { 4 | A: 65 5 | }, 6 | 7 | /** 8 | * @type {Date} 9 | */ 10 | now: new Date(), 11 | 12 | /** 13 | * Options {Object} 14 | */ 15 | options: { 16 | use24HoursFormat: true, 17 | timeBeforeTitle: false, 18 | popupNbItems: 10 19 | }, 20 | 21 | /** 22 | * @type {Date} 23 | */ 24 | today: null, 25 | 26 | /** 27 | * @type {boolean} 28 | */ 29 | isSearching: false, 30 | 31 | /** 32 | * @type {boolean} 33 | */ 34 | isLoading: false, 35 | 36 | /** 37 | * @type {boolean} 38 | */ 39 | autoFocus: false, 40 | 41 | /** 42 | * @type {function(_.debounce)} 43 | */ 44 | trottleSearch: null, 45 | 46 | /** 47 | * @type {boolean} 48 | */ 49 | controlPressed: false, 50 | 51 | /** 52 | * @type {boolean} 53 | */ 54 | majPressed: false, 55 | 56 | /** 57 | * @type {JQuery} 58 | */ 59 | multiSelectLastEntry: null, 60 | 61 | /** 62 | * Init application 63 | */ 64 | init: function(){ 65 | let $this = this; 66 | 67 | moment.locale(this.getCurrentLocale()); 68 | 69 | this.i18n(); 70 | this.today = new Date(this.now.getFullYear(), this.now.getMonth(), this.now.getDate(), 0, 0, 0, 0); 71 | 72 | chrome.storage.sync.get(function(items){ 73 | $this.options.use24HoursFormat = items.use24HoursFormat === undefined ? true : items.use24HoursFormat; 74 | $this.options.timeBeforeTitle = items.timeBeforeTitle === undefined ? false : items.timeBeforeTitle; 75 | $this.options.popupNbItems = items.popupNbItems === undefined ? 10 : items.popupNbItems; 76 | 77 | if($('body.popup').length){ 78 | $this.initPopup(); 79 | } else { 80 | $this.initMain(); 81 | } 82 | }); 83 | }, 84 | 85 | /** 86 | * Init popup 87 | */ 88 | initPopup: function(){ 89 | this.historyGetDay(this.today, this.options.popupNbItems); 90 | 91 | $('#go_history').on('click', function(e){ 92 | e.preventDefault(); 93 | chrome.tabs.create({url: 'chrome://history'}); 94 | }); 95 | }, 96 | 97 | /** 98 | * Init main 99 | */ 100 | initMain: function(){ 101 | 102 | let $this = this; 103 | this.trottleSearch = _.debounce(this.historySearch, 500); 104 | 105 | if($(window).width() == 360) 106 | { 107 | chrome.tabs.create({url: 'chrome://history'}); 108 | } 109 | 110 | $('#go_options').on('click', function(){ 111 | $this.openOptions(); 112 | }); 113 | 114 | $('#options_cancel, #options_close').on('click', function(){ 115 | $this.closeOptions(); 116 | }); 117 | 118 | $('#options_save').on('click', function(){ 119 | $this.saveOptions(); 120 | }); 121 | 122 | $('#search_input').focusin(function(){ 123 | $(this).parent().addClass('focus'); 124 | }).focusout(function(){ 125 | $(this).parent().removeClass('focus'); 126 | }).on('keyup', function(){ 127 | $this.inputSearch(); 128 | }); 129 | 130 | $('#search_clear').on('click', function(e){ 131 | e.preventDefault(); 132 | $this.clearSearch(); 133 | }); 134 | 135 | this.initCalendar(); 136 | 137 | $('#go_today').on('click', function(e){ 138 | e.preventDefault(); 139 | $this.historyGetDay($this.today); 140 | $this.initCalendar(); 141 | }); 142 | 143 | $('.remove-confirmation>#clear_confirm').on('click', function(e){ 144 | e.preventDefault(); 145 | $this.removeSelectedEntires(); 146 | }); 147 | 148 | $('.remove-confirmation>#clear_cancel').on('click', function(e){ 149 | e.preventDefault(); 150 | $this.clearSelectedItems(); 151 | }); 152 | 153 | $(document).keydown(function(e){ 154 | if(e.which == 17){ 155 | $this.controlPressed = true; 156 | } else if (e.which == 16){ 157 | $this.majPressed = true; 158 | } else { 159 | $this.keypressMulti(e); 160 | } 161 | }).keyup(function(e){ 162 | if(e.which == 17){ 163 | $this.controlPressed = false; 164 | } else if (e.which == 16){ 165 | $this.majPressed = false; 166 | } 167 | }).click(function(e){ 168 | if(!$this.is(e.target, 'entry') && !$this.is(e.target, 'remove-confirmation')){ 169 | $this.clearSelectedItems(); 170 | } 171 | }); 172 | 173 | $('#search_input').focus(); 174 | 175 | this.historyGetDay(this.today); 176 | }, 177 | 178 | /** 179 | * Init or reload calendar 180 | */ 181 | initCalendar: function(){ 182 | let $this = this; 183 | 184 | if($('.sidebar .calendar .ic__container').length){ 185 | $('.sidebar .calendar').remove(); 186 | } 187 | 188 | let calendar = $('
').addClass('calendar'); 189 | $('.sidebar .nav .content').prepend(calendar); 190 | 191 | calendar.ionCalendar({ 192 | lang: this.getCurrentLocale(), 193 | sundayFirst: moment.localeData().firstDayOfWeek() == 0, 194 | startDate: this.today, 195 | years: (this.now.getFullYear() - 3) + "-" + this.now.getFullYear(), 196 | onClick: function(date){ 197 | $this.historyGetDay(new Date(date)); 198 | } 199 | }); 200 | }, 201 | 202 | /** 203 | * Input search change 204 | */ 205 | inputSearch: function(){ 206 | let search_query = $('#search_input').val(); 207 | 208 | if(search_query.toString() !== ''){ 209 | $('#search_clear').css('display', 'flex'); 210 | this.trottleSearch(search_query); 211 | } else { 212 | if(this.isSearching){ 213 | this.clearContent(); 214 | this.historyGetDay(this.today); 215 | } 216 | this.trottleSearch.cancel(); 217 | this.isSearching = false; 218 | $('#search_clear').hide(); 219 | } 220 | }, 221 | 222 | /** 223 | * Cleanup search 224 | */ 225 | clearSearch: function(){ 226 | $('#search_input').val(''); 227 | this.inputSearch(); 228 | }, 229 | 230 | /** 231 | * Search inside history 232 | * 233 | * @param {string} query 234 | */ 235 | historySearch: function(query){ 236 | this.isSearching = true; 237 | this.clearContent(); 238 | this.historyQuery(query, new Date(1970, 1, 1, 0, 0, 0, 0), new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate(), 23, 59, 59), 0); 239 | }, 240 | 241 | /** 242 | * Gets history for specific day 243 | * 244 | * @param {Date} day 245 | * @param {int} [nb_entries] 246 | */ 247 | historyGetDay: function(day, nb_entries){ 248 | nb_entries = nb_entries || 0; 249 | 250 | if(day > this.now){ 251 | return; 252 | } 253 | 254 | let date_start = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 0, 0, 0, 0); 255 | let date_end = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59, 59); 256 | 257 | this.clearContent(); 258 | this.historyQuery('', date_start, date_end, parseInt(nb_entries)); 259 | }, 260 | 261 | /** 262 | * Query history 263 | * 264 | * @param {string} search 265 | * @param {Date} start 266 | * @param {Date} end 267 | * @param {int} nb_entries 268 | */ 269 | historyQuery: function(search, start, end, nb_entries){ 270 | let $this = this; 271 | this.isLoading = true; 272 | chrome.history.search({ text: search, startTime: start.getTime(), endTime: end.getTime(), maxResults: nb_entries }, function(results){ 273 | $this.historyCallback(results, start, end); 274 | }); 275 | }, 276 | 277 | /** 278 | * History callback 279 | * 280 | * @param {chrome.history.HistoryItem[]} results 281 | * @param {Date} start 282 | * @param {Date} end 283 | * @returns {void} 284 | */ 285 | historyCallback: function(results, start, end){ 286 | let items = {}; 287 | let count = 0; 288 | 289 | $.each(results, function(k, v){ 290 | let item_date = new Date(v.lastVisitTime); 291 | 292 | if(item_date >= start && item_date <= end){ 293 | let item_key = new Date(item_date.getFullYear(), item_date.getMonth(), item_date.getDate(), 0, 0, 0, 0).getTime().toString(); 294 | 295 | if(!items[item_key]){ 296 | items[item_key] = []; 297 | } 298 | items[item_key].push(v); 299 | count++; 300 | } 301 | }); 302 | 303 | if($.isEmptyObject(items)){ 304 | items[start.getTime()] = {}; 305 | } 306 | 307 | this.historyFormatDays(items, count); 308 | }, 309 | 310 | /** 311 | * Format history item 312 | * 313 | * @param {object[]} items 314 | * @param {int} count 315 | */ 316 | historyFormatDays: function(items, count){ 317 | let $this = this; 318 | 319 | this.clearContent(); 320 | 321 | if(this.isSearching){ 322 | this.insertContent('

' + chrome.i18n.getMessage('search_display') + ' "' + $('#search_input').val() + '"

'); 323 | 324 | if(count > 0){ 325 | this.insertContent('
' + chrome.i18n.getMessage('search_found', count.toString()) + '
'); 326 | } else { 327 | this.insertContent('
' + chrome.i18n.getMessage('search_empty') + '
'); 328 | this.isLoading = false; 329 | return; 330 | } 331 | } 332 | 333 | $.each(items, function(k, day){ 334 | 335 | let html = ''; 336 | k = k.toString(); 337 | 338 | if($('.wrapper .history-container #' + k).length < 1){ 339 | html+= '
'; 340 | } 341 | 342 | html+= '

' + moment(new Date(parseFloat(k.toString()))).format(chrome.i18n.getMessage('date_format')) + '

'; 343 | 344 | if(Object.keys(day).length > 0){ 345 | $.each(day, function(z, entry){ 346 | html+= $this.historyEntryFormat(entry); 347 | }); 348 | } else { 349 | html+= '
' + chrome.i18n.getMessage('history_date_empty') + '
'; 350 | } 351 | 352 | if($('.wrapper .history-container #' + k).length < 1){ 353 | html+= '
'; 354 | } 355 | 356 | $this.insertContent(html); 357 | }); 358 | 359 | this.historyEntriesBind(); 360 | this.isLoading = false; 361 | }, 362 | 363 | /** 364 | * Format entry 365 | * 366 | * @param {chrome.history.HistoryItem} entry 367 | */ 368 | historyEntryFormat: function(entry){ 369 | let html = ''; 370 | 371 | html+= '
'; 372 | 373 | if(this.options.timeBeforeTitle){ 374 | html+= '
' + moment(new Date(entry.lastVisitTime)).format(this.options.use24HoursFormat ? 'HH:mm' : 'hh:mm A') + '
'; 375 | } 376 | 377 | html+= ''; 378 | html+= '
' + this.escape(entry.title ? entry.title : entry.url) + '
'; 379 | 380 | if(!this.options.timeBeforeTitle) 381 | { 382 | html+= '
' + moment(new Date(entry.lastVisitTime)).format(this.options.use24HoursFormat ? 'HH:mm' : 'hh:mm A') + '
'; 383 | } 384 | 385 | html+= ''; 386 | html+= '
'; 387 | 388 | return html; 389 | }, 390 | 391 | /** 392 | * Remove history entry 393 | * 394 | * @param {string} url 395 | * @param {JQuery} sender 396 | */ 397 | historyEntryDelete: function(url, sender){ 398 | let $this = this; 399 | chrome.history.deleteUrl({ url: url }, function(){ 400 | let container = sender.parent().parent(); 401 | sender.parent().remove(); 402 | 403 | if($('.entry', container).length === 0){ 404 | $this.insertContent('
' + chrome.i18n.getMessage('history_date_empty') + '
', container); 405 | } 406 | 407 | $this.updateConfirm(); 408 | }); 409 | }, 410 | 411 | /** 412 | * Bind entries events 413 | */ 414 | historyEntriesBind: function(){ 415 | let $this = this; 416 | 417 | $('.history-container .entry>.entry-remove').unbind(); 418 | $('.history-container .entry>.entry-remove').on('click', function(e){ 419 | e.preventDefault(); 420 | $this.historyEntryDelete($(this).parent().find('.entry-link>a').attr('href'), $(this)); 421 | }); 422 | 423 | $('.history-container .entry', $('body.main')).unbind(); 424 | $('.history-container .entry', $('body.main')).on('click', function(e){ 425 | if(!$(e.target).is('a') && !$this.is(e.target, 'entry-remove')){ 426 | e.preventDefault(); 427 | 428 | if($this.majPressed && $this.multiSelectLastEntry !== null){ 429 | let tmp = $this.multiSelectLastEntry; 430 | document.getSelection().removeAllRanges(); 431 | 432 | if($(this).offset().top > $this.multiSelectLastEntry.offset().top){ 433 | while($(this).offset().top > tmp.offset().top){ 434 | tmp = tmp.next(); 435 | tmp.addClass('selected'); 436 | } 437 | } else { 438 | while(tmp.offset().top > $(this).offset().top){ 439 | tmp = tmp.prev(); 440 | tmp.addClass('selected'); 441 | } 442 | } 443 | } else { 444 | $this.multiSelectLastEntry = $(this); 445 | 446 | if($(this).hasClass('selected')){ 447 | $(this).removeClass('selected'); 448 | } else { 449 | $(this).addClass('selected'); 450 | } 451 | } 452 | 453 | $this.updateConfirm(); 454 | } 455 | }); 456 | }, 457 | 458 | /** 459 | * Keypressed multi action 460 | * 461 | * @param {JQueryKeyEventObject} e 462 | */ 463 | keypressMulti: function(e){ 464 | if($(e.target).attr('id') !== 'search_input'){ 465 | if(this.controlPressed && (e.which == this.KEYS.A)){ 466 | e.preventDefault(); 467 | $('.history-container .entry').addClass('selected'); 468 | this.updateConfirm(); 469 | } 470 | } 471 | 472 | if(e.which == 46){ 473 | if($('.history-container .entry.selected').length > 0){ 474 | this.removeSelectedEntires(); 475 | } 476 | } 477 | }, 478 | 479 | /** 480 | * Display / hide delete confirmation 481 | */ 482 | updateConfirm: function(){ 483 | let count = $('.history-container .entry.selected').length; 484 | 485 | if(count > 0){ 486 | $('.remove-confirmation').show(); 487 | $('.remove-confirmation>.num').html(count.toString()); 488 | } else { 489 | $('.remove-confirmation').hide(); 490 | } 491 | }, 492 | 493 | /** 494 | * Clear selection 495 | */ 496 | clearSelectedItems: function(){ 497 | $('.history-container .entry.selected').removeClass('selected'); 498 | this.multiSelectLastEntry = null; 499 | this.updateConfirm(); 500 | }, 501 | 502 | /** 503 | * Remove selected entries 504 | */ 505 | removeSelectedEntires: function(){ 506 | $('.history-container .entry.selected').each(function(){ 507 | $(this).find('.entry-remove').click(); 508 | }); 509 | }, 510 | 511 | /** 512 | * Insert content in html 513 | * 514 | * @param {string} html 515 | * @param {(JQuery|null)} [context] 516 | */ 517 | insertContent: function(html, context){ 518 | context = context || null; 519 | 520 | if(context !== null){ 521 | context.append(html) 522 | } else { 523 | $('.wrapper .history-container .content').append(html); 524 | } 525 | }, 526 | 527 | /** 528 | * Clear html content 529 | */ 530 | clearContent: function(){ 531 | $('.wrapper .history-container .content').html(''); 532 | }, 533 | 534 | /** 535 | * Replace i18n item in html 536 | */ 537 | i18n: function(){ 538 | $('[i18n]').each(function(){ 539 | let i18n = $(this).attr('i18n'); 540 | if(i18n.indexOf(':') >= 0){ 541 | let tmp = i18n.split(':'); 542 | $(this).attr(tmp[0], chrome.i18n.getMessage(tmp[1])); 543 | } else { 544 | $(this).html(chrome.i18n.getMessage(i18n)); 545 | } 546 | }); 547 | }, 548 | 549 | /** 550 | * Target is classname or children 551 | * 552 | * @param {Element} target 553 | * @param {string} classname 554 | * @returns {boolean} 555 | */ 556 | is: function(target, classname){ 557 | return !!($(target).hasClass(classname) || $(target).parents('.' + classname).length > 0); 558 | }, 559 | 560 | /** 561 | * Gets current language 562 | * 563 | * @returns {string} 564 | */ 565 | getCurrentLanguage: function(){ 566 | return this.getCurrentLocale().substr(0, 2); 567 | }, 568 | 569 | /** 570 | * Gets current locale 571 | * 572 | * @returns {string} 573 | */ 574 | getCurrentLocale: function(){ 575 | return chrome.i18n.getMessage('language'); 576 | }, 577 | 578 | /** 579 | * Gets favicon source file 580 | * 581 | * @param {string} url 582 | * @param {int} size 583 | * @returns {string} 584 | */ 585 | getFavicon: function(url){ 586 | return "chrome-extension://" + chrome.runtime.id + "/_favicon/?size=32&pageUrl=" + encodeURIComponent(this.escape(url)); 587 | }, 588 | 589 | /** 590 | * Open options modal 591 | * 592 | * @returns {void} 593 | */ 594 | openOptions: function(){ 595 | $('#options_field_24hoursformat').prop('checked', this.options.use24HoursFormat); 596 | $('#options_field_displaytitlebeforetime').prop('checked', this.options.timeBeforeTitle); 597 | $('#options_field_popupnbitems').val(this.options.popupNbItems); 598 | $('#modal_options').css('display', 'flex'); 599 | }, 600 | 601 | /** 602 | * Close options modal 603 | * 604 | * @param {boolean} reload 605 | * @returns {void} 606 | */ 607 | closeOptions: function(reload){ 608 | $('#modal_options').css('display', 'none'); 609 | if(reload){ 610 | location.reload(); 611 | } 612 | }, 613 | 614 | /** 615 | * Save options 616 | * 617 | * @returns {void} 618 | */ 619 | saveOptions: function(){ 620 | let $this = this; 621 | chrome.storage.sync.set({ 622 | use24HoursFormat: $('#options_field_24hoursformat').prop('checked'), 623 | timeBeforeTitle: $('#options_field_displaytitlebeforetime').prop('checked'), 624 | popupNbItems: $('#options_field_popupnbitems').val() 625 | }, function(){ 626 | $this.closeOptions(true); 627 | }); 628 | }, 629 | 630 | /** 631 | * Espace string 632 | * 633 | * @param {string} string 634 | * @returns {string} 635 | */ 636 | escape: function(string) { 637 | return string 638 | .replace(//g, ">") 640 | .replace(/"/g, """) 641 | .replace(/'/g, "'"); 642 | } 643 | 644 | }; 645 | 646 | $(function(){ 647 | application.init(); 648 | }); -------------------------------------------------------------------------------- /src/js/compiled/underscore.js: -------------------------------------------------------------------------------- 1 | (function(){var root=typeof self=="object"&&self.self===self&&self||typeof global=="object"&&global.global===global&&global||this||{};var previousUnderscore=root._;var ArrayProto=Array.prototype,ObjProto=Object.prototype;var SymbolProto=typeof Symbol!=="undefined"?Symbol.prototype:null;var push=ArrayProto.push,slice=ArrayProto.slice,toString=ObjProto.toString,hasOwnProperty=ObjProto.hasOwnProperty;var nativeIsArray=Array.isArray,nativeKeys=Object.keys,nativeCreate=Object.create;var Ctor=function(){};var _=function(obj){if(obj instanceof _)return obj;if(!(this instanceof _))return new _(obj);this._wrapped=obj};if(typeof exports!="undefined"&&!exports.nodeType){if(typeof module!="undefined"&&!module.nodeType&&module.exports){exports=module.exports=_}exports._=_}else{root._=_}_.VERSION="1.9.1";var optimizeCb=function(func,context,argCount){if(context===void 0)return func;switch(argCount==null?3:argCount){case 1:return function(value){return func.call(context,value)};case 3:return function(value,index,collection){return func.call(context,value,index,collection)};case 4:return function(accumulator,value,index,collection){return func.call(context,accumulator,value,index,collection)}}return function(){return func.apply(context,arguments)}};var builtinIteratee;var cb=function(value,context,argCount){if(_.iteratee!==builtinIteratee)return _.iteratee(value,context);if(value==null)return _.identity;if(_.isFunction(value))return optimizeCb(value,context,argCount);if(_.isObject(value)&&!_.isArray(value))return _.matcher(value);return _.property(value)};_.iteratee=builtinIteratee=function(value,context){return cb(value,context,Infinity)};var restArguments=function(func,startIndex){startIndex=startIndex==null?func.length-1:+startIndex;return function(){var length=Math.max(arguments.length-startIndex,0),rest=Array(length),index=0;for(;index=0&&length<=MAX_ARRAY_INDEX};_.each=_.forEach=function(obj,iteratee,context){iteratee=optimizeCb(iteratee,context);var i,length;if(isArrayLike(obj)){for(i=0,length=obj.length;i0?0:length-1;if(!initial){memo=obj[keys?keys[index]:index];index+=dir}for(;index>=0&&index=3;return reducer(obj,optimizeCb(iteratee,context,4),memo,initial)}};_.reduce=_.foldl=_.inject=createReduce(1);_.reduceRight=_.foldr=createReduce(-1);_.find=_.detect=function(obj,predicate,context){var keyFinder=isArrayLike(obj)?_.findIndex:_.findKey;var key=keyFinder(obj,predicate,context);if(key!==void 0&&key!==-1)return obj[key]};_.filter=_.select=function(obj,predicate,context){var results=[];predicate=cb(predicate,context);_.each(obj,function(value,index,list){if(predicate(value,index,list))results.push(value)});return results};_.reject=function(obj,predicate,context){return _.filter(obj,_.negate(cb(predicate)),context)};_.every=_.all=function(obj,predicate,context){predicate=cb(predicate,context);var keys=!isArrayLike(obj)&&_.keys(obj),length=(keys||obj).length;for(var index=0;index=0};_.invoke=restArguments(function(obj,path,args){var contextPath,func;if(_.isFunction(path)){func=path}else if(_.isArray(path)){contextPath=path.slice(0,-1);path=path[path.length-1]}return _.map(obj,function(context){var method=func;if(!method){if(contextPath&&contextPath.length){context=deepGet(context,contextPath)}if(context==null)return void 0;method=context[path]}return method==null?method:method.apply(context,args)})});_.pluck=function(obj,key){return _.map(obj,_.property(key))};_.where=function(obj,attrs){return _.filter(obj,_.matcher(attrs))};_.findWhere=function(obj,attrs){return _.find(obj,_.matcher(attrs))};_.max=function(obj,iteratee,context){var result=-Infinity,lastComputed=-Infinity,value,computed;if(iteratee==null||typeof iteratee=="number"&&typeof obj[0]!="object"&&obj!=null){obj=isArrayLike(obj)?obj:_.values(obj);for(var i=0,length=obj.length;iresult){result=value}}}else{iteratee=cb(iteratee,context);_.each(obj,function(v,index,list){computed=iteratee(v,index,list);if(computed>lastComputed||computed===-Infinity&&result===-Infinity){result=v;lastComputed=computed}})}return result};_.min=function(obj,iteratee,context){var result=Infinity,lastComputed=Infinity,value,computed;if(iteratee==null||typeof iteratee=="number"&&typeof obj[0]!="object"&&obj!=null){obj=isArrayLike(obj)?obj:_.values(obj);for(var i=0,length=obj.length;ib||a===void 0)return 1;if(a0?0:length-1;for(;index>=0&&index0){i=idx>=0?idx:Math.max(idx+length,i)}else{length=idx>=0?Math.min(idx+1,length):idx+length+1}}else if(sortedIndex&&idx&&length){idx=sortedIndex(array,item);return array[idx]===item?idx:-1}if(item!==item){idx=predicateFind(slice.call(array,i,length),_.isNaN);return idx>=0?idx+i:-1}for(idx=dir>0?i:length-1;idx>=0&&idxwait){if(timeout){clearTimeout(timeout);timeout=null}previous=now;result=func.apply(context,args);if(!timeout)context=args=null}else if(!timeout&&options.trailing!==false){timeout=setTimeout(later,remaining)}return result};throttled.cancel=function(){clearTimeout(timeout);previous=0;timeout=context=args=null};return throttled};_.debounce=function(func,wait,immediate){var timeout,result;var later=function(context,args){timeout=null;if(args)result=func.apply(context,args)};var debounced=restArguments(function(args){if(timeout)clearTimeout(timeout);if(immediate){var callNow=!timeout;timeout=setTimeout(later,wait);if(callNow)result=func.apply(this,args)}else{timeout=_.delay(later,wait,this,args)}return result});debounced.cancel=function(){clearTimeout(timeout);timeout=null};return debounced};_.wrap=function(func,wrapper){return _.partial(wrapper,func)};_.negate=function(predicate){return function(){return!predicate.apply(this,arguments)}};_.compose=function(){var args=arguments;var start=args.length-1;return function(){var i=start;var result=args[start].apply(this,arguments);while(i--)result=args[i].call(this,result);return result}};_.after=function(times,func){return function(){if(--times<1){return func.apply(this,arguments)}}};_.before=function(times,func){var memo;return function(){if(--times>0){memo=func.apply(this,arguments)}if(times<=1)func=null;return memo}};_.once=_.partial(_.before,2);_.restArguments=restArguments;var hasEnumBug=!{toString:null}.propertyIsEnumerable("toString");var nonEnumerableProps=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];var collectNonEnumProps=function(obj,keys){var nonEnumIdx=nonEnumerableProps.length;var constructor=obj.constructor;var proto=_.isFunction(constructor)&&constructor.prototype||ObjProto;var prop="constructor";if(has(obj,prop)&&!_.contains(keys,prop))keys.push(prop);while(nonEnumIdx--){prop=nonEnumerableProps[nonEnumIdx];if(prop in obj&&obj[prop]!==proto[prop]&&!_.contains(keys,prop)){keys.push(prop)}}};_.keys=function(obj){if(!_.isObject(obj))return[];if(nativeKeys)return nativeKeys(obj);var keys=[];for(var key in obj)if(has(obj,key))keys.push(key);if(hasEnumBug)collectNonEnumProps(obj,keys);return keys};_.allKeys=function(obj){if(!_.isObject(obj))return[];var keys=[];for(var key in obj)keys.push(key);if(hasEnumBug)collectNonEnumProps(obj,keys);return keys};_.values=function(obj){var keys=_.keys(obj);var length=keys.length;var values=Array(length);for(var i=0;i1)iteratee=optimizeCb(iteratee,keys[1]);keys=_.allKeys(obj)}else{iteratee=keyInObj;keys=flatten(keys,false,false);obj=Object(obj)}for(var i=0,length=keys.length;i1)context=keys[1]}else{keys=_.map(flatten(keys,false,false),String);iteratee=function(value,key){return!_.contains(keys,key)}}return _.pick(obj,iteratee,context)});_.defaults=createAssigner(_.allKeys,true);_.create=function(prototype,props){var result=baseCreate(prototype);if(props)_.extendOwn(result,props);return result};_.clone=function(obj){if(!_.isObject(obj))return obj;return _.isArray(obj)?obj.slice():_.extend({},obj)};_.tap=function(obj,interceptor){interceptor(obj);return obj};_.isMatch=function(object,attrs){var keys=_.keys(attrs),length=keys.length;if(object==null)return!length;var obj=Object(object);for(var i=0;i":">",'"':""","'":"'","`":"`"};var unescapeMap=_.invert(escapeMap);var createEscaper=function(map){var escaper=function(match){return map[match]};var source="(?:"+_.keys(map).join("|")+")";var testRegexp=RegExp(source);var replaceRegexp=RegExp(source,"g");return function(string){string=string==null?"":""+string;return testRegexp.test(string)?string.replace(replaceRegexp,escaper):string}};_.escape=createEscaper(escapeMap);_.unescape=createEscaper(unescapeMap);_.result=function(obj,path,fallback){if(!_.isArray(path))path=[path];var length=path.length;if(!length){return _.isFunction(fallback)?fallback.call(obj):fallback}for(var i=0;i/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var noMatch=/(.)^/;var escapes={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"};var escapeRegExp=/\\|'|\r|\n|\u2028|\u2029/g;var escapeChar=function(match){return"\\"+escapes[match]};_.template=function(text,settings,oldSettings){if(!settings&&oldSettings)settings=oldSettings;settings=_.defaults({},settings,_.templateSettings);var matcher=RegExp([(settings.escape||noMatch).source,(settings.interpolate||noMatch).source,(settings.evaluate||noMatch).source].join("|")+"|$","g");var index=0;var source="__p+='";text.replace(matcher,function(match,escape,interpolate,evaluate,offset){source+=text.slice(index,offset).replace(escapeRegExp,escapeChar);index=offset+match.length;if(escape){source+="'+\n((__t=("+escape+"))==null?'':_.escape(__t))+\n'"}else if(interpolate){source+="'+\n((__t=("+interpolate+"))==null?'':__t)+\n'"}else if(evaluate){source+="';\n"+evaluate+"\n__p+='"}return match});source+="';\n";if(!settings.variable)source="with(obj||{}){\n"+source+"}\n";source="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+source+"return __p;\n";var render;try{render=new Function(settings.variable||"obj","_",source)}catch(e){e.source=source;throw e}var template=function(data){return render.call(this,data,_)};var argument=settings.variable||"obj";template.source="function("+argument+"){\n"+source+"}";return template};_.chain=function(obj){var instance=_(obj);instance._chain=true;return instance};var chainResult=function(instance,obj){return instance._chain?_(obj).chain():obj};_.mixin=function(obj){_.each(_.functions(obj),function(name){var func=_[name]=obj[name];_.prototype[name]=function(){var args=[this._wrapped];push.apply(args,arguments);return chainResult(this,func.apply(_,args))}});return _};_.mixin(_);_.each(["pop","push","reverse","shift","sort","splice","unshift"],function(name){var method=ArrayProto[name];_.prototype[name]=function(){var obj=this._wrapped;method.apply(obj,arguments);if((name==="shift"||name==="splice")&&obj.length===0)delete obj[0];return chainResult(this,obj)}});_.each(["concat","join","slice"],function(name){var method=ArrayProto[name];_.prototype[name]=function(){return chainResult(this,method.apply(this._wrapped,arguments))}});_.prototype.value=function(){return this._wrapped};_.prototype.valueOf=_.prototype.toJSON=_.prototype.value;_.prototype.toString=function(){return String(this._wrapped)};if(typeof define=="function"&&define.amd){define("underscore",[],function(){return _})}})(); -------------------------------------------------------------------------------- /src/js/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.9.1 2 | // http://underscorejs.org 3 | // (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function() { 6 | // Baseline setup 7 | // -------------- 8 | 9 | // Establish the root object, `window` (`self`) in the browser, `global` 10 | // on the server, or `this` in some virtual machines. We use `self` 11 | // instead of `window` for `WebWorker` support. 12 | var root = typeof self == 'object' && self.self === self && self || 13 | typeof global == 'object' && global.global === global && global || 14 | this || 15 | {}; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype; 22 | var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; 23 | 24 | // Create quick reference variables for speed access to core prototypes. 25 | var push = ArrayProto.push, 26 | slice = ArrayProto.slice, 27 | toString = ObjProto.toString, 28 | hasOwnProperty = ObjProto.hasOwnProperty; 29 | 30 | // All **ECMAScript 5** native function implementations that we hope to use 31 | // are declared here. 32 | var nativeIsArray = Array.isArray, 33 | nativeKeys = Object.keys, 34 | nativeCreate = Object.create; 35 | 36 | // Naked function reference for surrogate-prototype-swapping. 37 | var Ctor = function(){}; 38 | 39 | // Create a safe reference to the Underscore object for use below. 40 | var _ = function(obj) { 41 | if (obj instanceof _) return obj; 42 | if (!(this instanceof _)) return new _(obj); 43 | this._wrapped = obj; 44 | }; 45 | 46 | // Export the Underscore object for **Node.js**, with 47 | // backwards-compatibility for their old module API. If we're in 48 | // the browser, add `_` as a global object. 49 | // (`nodeType` is checked to ensure that `module` 50 | // and `exports` are not HTML elements.) 51 | if (typeof exports != 'undefined' && !exports.nodeType) { 52 | if (typeof module != 'undefined' && !module.nodeType && module.exports) { 53 | exports = module.exports = _; 54 | } 55 | exports._ = _; 56 | } else { 57 | root._ = _; 58 | } 59 | 60 | // Current version. 61 | _.VERSION = '1.9.1'; 62 | 63 | // Internal function that returns an efficient (for current engines) version 64 | // of the passed-in callback, to be repeatedly applied in other Underscore 65 | // functions. 66 | var optimizeCb = function(func, context, argCount) { 67 | if (context === void 0) return func; 68 | switch (argCount == null ? 3 : argCount) { 69 | case 1: return function(value) { 70 | return func.call(context, value); 71 | }; 72 | // The 2-argument case is omitted because we’re not using it. 73 | case 3: return function(value, index, collection) { 74 | return func.call(context, value, index, collection); 75 | }; 76 | case 4: return function(accumulator, value, index, collection) { 77 | return func.call(context, accumulator, value, index, collection); 78 | }; 79 | } 80 | return function() { 81 | return func.apply(context, arguments); 82 | }; 83 | }; 84 | 85 | var builtinIteratee; 86 | 87 | // An internal function to generate callbacks that can be applied to each 88 | // element in a collection, returning the desired result — either `identity`, 89 | // an arbitrary callback, a property matcher, or a property accessor. 90 | var cb = function(value, context, argCount) { 91 | if (_.iteratee !== builtinIteratee) return _.iteratee(value, context); 92 | if (value == null) return _.identity; 93 | if (_.isFunction(value)) return optimizeCb(value, context, argCount); 94 | if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); 95 | return _.property(value); 96 | }; 97 | 98 | // External wrapper for our callback generator. Users may customize 99 | // `_.iteratee` if they want additional predicate/iteratee shorthand styles. 100 | // This abstraction hides the internal-only argCount argument. 101 | _.iteratee = builtinIteratee = function(value, context) { 102 | return cb(value, context, Infinity); 103 | }; 104 | 105 | // Some functions take a variable number of arguments, or a few expected 106 | // arguments at the beginning and then a variable number of values to operate 107 | // on. This helper accumulates all remaining arguments past the function’s 108 | // argument length (or an explicit `startIndex`), into an array that becomes 109 | // the last argument. Similar to ES6’s "rest parameter". 110 | var restArguments = function(func, startIndex) { 111 | startIndex = startIndex == null ? func.length - 1 : +startIndex; 112 | return function() { 113 | var length = Math.max(arguments.length - startIndex, 0), 114 | rest = Array(length), 115 | index = 0; 116 | for (; index < length; index++) { 117 | rest[index] = arguments[index + startIndex]; 118 | } 119 | switch (startIndex) { 120 | case 0: return func.call(this, rest); 121 | case 1: return func.call(this, arguments[0], rest); 122 | case 2: return func.call(this, arguments[0], arguments[1], rest); 123 | } 124 | var args = Array(startIndex + 1); 125 | for (index = 0; index < startIndex; index++) { 126 | args[index] = arguments[index]; 127 | } 128 | args[startIndex] = rest; 129 | return func.apply(this, args); 130 | }; 131 | }; 132 | 133 | // An internal function for creating a new object that inherits from another. 134 | var baseCreate = function(prototype) { 135 | if (!_.isObject(prototype)) return {}; 136 | if (nativeCreate) return nativeCreate(prototype); 137 | Ctor.prototype = prototype; 138 | var result = new Ctor; 139 | Ctor.prototype = null; 140 | return result; 141 | }; 142 | 143 | var shallowProperty = function(key) { 144 | return function(obj) { 145 | return obj == null ? void 0 : obj[key]; 146 | }; 147 | }; 148 | 149 | var has = function(obj, path) { 150 | return obj != null && hasOwnProperty.call(obj, path); 151 | } 152 | 153 | var deepGet = function(obj, path) { 154 | var length = path.length; 155 | for (var i = 0; i < length; i++) { 156 | if (obj == null) return void 0; 157 | obj = obj[path[i]]; 158 | } 159 | return length ? obj : void 0; 160 | }; 161 | 162 | // Helper for collection methods to determine whether a collection 163 | // should be iterated as an array or as an object. 164 | // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength 165 | // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 166 | var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; 167 | var getLength = shallowProperty('length'); 168 | var isArrayLike = function(collection) { 169 | var length = getLength(collection); 170 | return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; 171 | }; 172 | 173 | // Collection Functions 174 | // -------------------- 175 | 176 | // The cornerstone, an `each` implementation, aka `forEach`. 177 | // Handles raw objects in addition to array-likes. Treats all 178 | // sparse array-likes as if they were dense. 179 | _.each = _.forEach = function(obj, iteratee, context) { 180 | iteratee = optimizeCb(iteratee, context); 181 | var i, length; 182 | if (isArrayLike(obj)) { 183 | for (i = 0, length = obj.length; i < length; i++) { 184 | iteratee(obj[i], i, obj); 185 | } 186 | } else { 187 | var keys = _.keys(obj); 188 | for (i = 0, length = keys.length; i < length; i++) { 189 | iteratee(obj[keys[i]], keys[i], obj); 190 | } 191 | } 192 | return obj; 193 | }; 194 | 195 | // Return the results of applying the iteratee to each element. 196 | _.map = _.collect = function(obj, iteratee, context) { 197 | iteratee = cb(iteratee, context); 198 | var keys = !isArrayLike(obj) && _.keys(obj), 199 | length = (keys || obj).length, 200 | results = Array(length); 201 | for (var index = 0; index < length; index++) { 202 | var currentKey = keys ? keys[index] : index; 203 | results[index] = iteratee(obj[currentKey], currentKey, obj); 204 | } 205 | return results; 206 | }; 207 | 208 | // Create a reducing function iterating left or right. 209 | var createReduce = function(dir) { 210 | // Wrap code that reassigns argument variables in a separate function than 211 | // the one that accesses `arguments.length` to avoid a perf hit. (#1991) 212 | var reducer = function(obj, iteratee, memo, initial) { 213 | var keys = !isArrayLike(obj) && _.keys(obj), 214 | length = (keys || obj).length, 215 | index = dir > 0 ? 0 : length - 1; 216 | if (!initial) { 217 | memo = obj[keys ? keys[index] : index]; 218 | index += dir; 219 | } 220 | for (; index >= 0 && index < length; index += dir) { 221 | var currentKey = keys ? keys[index] : index; 222 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 223 | } 224 | return memo; 225 | }; 226 | 227 | return function(obj, iteratee, memo, context) { 228 | var initial = arguments.length >= 3; 229 | return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); 230 | }; 231 | }; 232 | 233 | // **Reduce** builds up a single result from a list of values, aka `inject`, 234 | // or `foldl`. 235 | _.reduce = _.foldl = _.inject = createReduce(1); 236 | 237 | // The right-associative version of reduce, also known as `foldr`. 238 | _.reduceRight = _.foldr = createReduce(-1); 239 | 240 | // Return the first value which passes a truth test. Aliased as `detect`. 241 | _.find = _.detect = function(obj, predicate, context) { 242 | var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey; 243 | var key = keyFinder(obj, predicate, context); 244 | if (key !== void 0 && key !== -1) return obj[key]; 245 | }; 246 | 247 | // Return all the elements that pass a truth test. 248 | // Aliased as `select`. 249 | _.filter = _.select = function(obj, predicate, context) { 250 | var results = []; 251 | predicate = cb(predicate, context); 252 | _.each(obj, function(value, index, list) { 253 | if (predicate(value, index, list)) results.push(value); 254 | }); 255 | return results; 256 | }; 257 | 258 | // Return all the elements for which a truth test fails. 259 | _.reject = function(obj, predicate, context) { 260 | return _.filter(obj, _.negate(cb(predicate)), context); 261 | }; 262 | 263 | // Determine whether all of the elements match a truth test. 264 | // Aliased as `all`. 265 | _.every = _.all = function(obj, predicate, context) { 266 | predicate = cb(predicate, context); 267 | var keys = !isArrayLike(obj) && _.keys(obj), 268 | length = (keys || obj).length; 269 | for (var index = 0; index < length; index++) { 270 | var currentKey = keys ? keys[index] : index; 271 | if (!predicate(obj[currentKey], currentKey, obj)) return false; 272 | } 273 | return true; 274 | }; 275 | 276 | // Determine if at least one element in the object matches a truth test. 277 | // Aliased as `any`. 278 | _.some = _.any = function(obj, predicate, context) { 279 | predicate = cb(predicate, context); 280 | var keys = !isArrayLike(obj) && _.keys(obj), 281 | length = (keys || obj).length; 282 | for (var index = 0; index < length; index++) { 283 | var currentKey = keys ? keys[index] : index; 284 | if (predicate(obj[currentKey], currentKey, obj)) return true; 285 | } 286 | return false; 287 | }; 288 | 289 | // Determine if the array or object contains a given item (using `===`). 290 | // Aliased as `includes` and `include`. 291 | _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { 292 | if (!isArrayLike(obj)) obj = _.values(obj); 293 | if (typeof fromIndex != 'number' || guard) fromIndex = 0; 294 | return _.indexOf(obj, item, fromIndex) >= 0; 295 | }; 296 | 297 | // Invoke a method (with arguments) on every item in a collection. 298 | _.invoke = restArguments(function(obj, path, args) { 299 | var contextPath, func; 300 | if (_.isFunction(path)) { 301 | func = path; 302 | } else if (_.isArray(path)) { 303 | contextPath = path.slice(0, -1); 304 | path = path[path.length - 1]; 305 | } 306 | return _.map(obj, function(context) { 307 | var method = func; 308 | if (!method) { 309 | if (contextPath && contextPath.length) { 310 | context = deepGet(context, contextPath); 311 | } 312 | if (context == null) return void 0; 313 | method = context[path]; 314 | } 315 | return method == null ? method : method.apply(context, args); 316 | }); 317 | }); 318 | 319 | // Convenience version of a common use case of `map`: fetching a property. 320 | _.pluck = function(obj, key) { 321 | return _.map(obj, _.property(key)); 322 | }; 323 | 324 | // Convenience version of a common use case of `filter`: selecting only objects 325 | // containing specific `key:value` pairs. 326 | _.where = function(obj, attrs) { 327 | return _.filter(obj, _.matcher(attrs)); 328 | }; 329 | 330 | // Convenience version of a common use case of `find`: getting the first object 331 | // containing specific `key:value` pairs. 332 | _.findWhere = function(obj, attrs) { 333 | return _.find(obj, _.matcher(attrs)); 334 | }; 335 | 336 | // Return the maximum element (or element-based computation). 337 | _.max = function(obj, iteratee, context) { 338 | var result = -Infinity, lastComputed = -Infinity, 339 | value, computed; 340 | if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { 341 | obj = isArrayLike(obj) ? obj : _.values(obj); 342 | for (var i = 0, length = obj.length; i < length; i++) { 343 | value = obj[i]; 344 | if (value != null && value > result) { 345 | result = value; 346 | } 347 | } 348 | } else { 349 | iteratee = cb(iteratee, context); 350 | _.each(obj, function(v, index, list) { 351 | computed = iteratee(v, index, list); 352 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) { 353 | result = v; 354 | lastComputed = computed; 355 | } 356 | }); 357 | } 358 | return result; 359 | }; 360 | 361 | // Return the minimum element (or element-based computation). 362 | _.min = function(obj, iteratee, context) { 363 | var result = Infinity, lastComputed = Infinity, 364 | value, computed; 365 | if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { 366 | obj = isArrayLike(obj) ? obj : _.values(obj); 367 | for (var i = 0, length = obj.length; i < length; i++) { 368 | value = obj[i]; 369 | if (value != null && value < result) { 370 | result = value; 371 | } 372 | } 373 | } else { 374 | iteratee = cb(iteratee, context); 375 | _.each(obj, function(v, index, list) { 376 | computed = iteratee(v, index, list); 377 | if (computed < lastComputed || computed === Infinity && result === Infinity) { 378 | result = v; 379 | lastComputed = computed; 380 | } 381 | }); 382 | } 383 | return result; 384 | }; 385 | 386 | // Shuffle a collection. 387 | _.shuffle = function(obj) { 388 | return _.sample(obj, Infinity); 389 | }; 390 | 391 | // Sample **n** random values from a collection using the modern version of the 392 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 393 | // If **n** is not specified, returns a single random element. 394 | // The internal `guard` argument allows it to work with `map`. 395 | _.sample = function(obj, n, guard) { 396 | if (n == null || guard) { 397 | if (!isArrayLike(obj)) obj = _.values(obj); 398 | return obj[_.random(obj.length - 1)]; 399 | } 400 | var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj); 401 | var length = getLength(sample); 402 | n = Math.max(Math.min(n, length), 0); 403 | var last = length - 1; 404 | for (var index = 0; index < n; index++) { 405 | var rand = _.random(index, last); 406 | var temp = sample[index]; 407 | sample[index] = sample[rand]; 408 | sample[rand] = temp; 409 | } 410 | return sample.slice(0, n); 411 | }; 412 | 413 | // Sort the object's values by a criterion produced by an iteratee. 414 | _.sortBy = function(obj, iteratee, context) { 415 | var index = 0; 416 | iteratee = cb(iteratee, context); 417 | return _.pluck(_.map(obj, function(value, key, list) { 418 | return { 419 | value: value, 420 | index: index++, 421 | criteria: iteratee(value, key, list) 422 | }; 423 | }).sort(function(left, right) { 424 | var a = left.criteria; 425 | var b = right.criteria; 426 | if (a !== b) { 427 | if (a > b || a === void 0) return 1; 428 | if (a < b || b === void 0) return -1; 429 | } 430 | return left.index - right.index; 431 | }), 'value'); 432 | }; 433 | 434 | // An internal function used for aggregate "group by" operations. 435 | var group = function(behavior, partition) { 436 | return function(obj, iteratee, context) { 437 | var result = partition ? [[], []] : {}; 438 | iteratee = cb(iteratee, context); 439 | _.each(obj, function(value, index) { 440 | var key = iteratee(value, index, obj); 441 | behavior(result, value, key); 442 | }); 443 | return result; 444 | }; 445 | }; 446 | 447 | // Groups the object's values by a criterion. Pass either a string attribute 448 | // to group by, or a function that returns the criterion. 449 | _.groupBy = group(function(result, value, key) { 450 | if (has(result, key)) result[key].push(value); else result[key] = [value]; 451 | }); 452 | 453 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 454 | // when you know that your index values will be unique. 455 | _.indexBy = group(function(result, value, key) { 456 | result[key] = value; 457 | }); 458 | 459 | // Counts instances of an object that group by a certain criterion. Pass 460 | // either a string attribute to count by, or a function that returns the 461 | // criterion. 462 | _.countBy = group(function(result, value, key) { 463 | if (has(result, key)) result[key]++; else result[key] = 1; 464 | }); 465 | 466 | var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; 467 | // Safely create a real, live array from anything iterable. 468 | _.toArray = function(obj) { 469 | if (!obj) return []; 470 | if (_.isArray(obj)) return slice.call(obj); 471 | if (_.isString(obj)) { 472 | // Keep surrogate pair characters together 473 | return obj.match(reStrSymbol); 474 | } 475 | if (isArrayLike(obj)) return _.map(obj, _.identity); 476 | return _.values(obj); 477 | }; 478 | 479 | // Return the number of elements in an object. 480 | _.size = function(obj) { 481 | if (obj == null) return 0; 482 | return isArrayLike(obj) ? obj.length : _.keys(obj).length; 483 | }; 484 | 485 | // Split a collection into two arrays: one whose elements all satisfy the given 486 | // predicate, and one whose elements all do not satisfy the predicate. 487 | _.partition = group(function(result, value, pass) { 488 | result[pass ? 0 : 1].push(value); 489 | }, true); 490 | 491 | // Array Functions 492 | // --------------- 493 | 494 | // Get the first element of an array. Passing **n** will return the first N 495 | // values in the array. Aliased as `head` and `take`. The **guard** check 496 | // allows it to work with `_.map`. 497 | _.first = _.head = _.take = function(array, n, guard) { 498 | if (array == null || array.length < 1) return n == null ? void 0 : []; 499 | if (n == null || guard) return array[0]; 500 | return _.initial(array, array.length - n); 501 | }; 502 | 503 | // Returns everything but the last entry of the array. Especially useful on 504 | // the arguments object. Passing **n** will return all the values in 505 | // the array, excluding the last N. 506 | _.initial = function(array, n, guard) { 507 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); 508 | }; 509 | 510 | // Get the last element of an array. Passing **n** will return the last N 511 | // values in the array. 512 | _.last = function(array, n, guard) { 513 | if (array == null || array.length < 1) return n == null ? void 0 : []; 514 | if (n == null || guard) return array[array.length - 1]; 515 | return _.rest(array, Math.max(0, array.length - n)); 516 | }; 517 | 518 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 519 | // Especially useful on the arguments object. Passing an **n** will return 520 | // the rest N values in the array. 521 | _.rest = _.tail = _.drop = function(array, n, guard) { 522 | return slice.call(array, n == null || guard ? 1 : n); 523 | }; 524 | 525 | // Trim out all falsy values from an array. 526 | _.compact = function(array) { 527 | return _.filter(array, Boolean); 528 | }; 529 | 530 | // Internal implementation of a recursive `flatten` function. 531 | var flatten = function(input, shallow, strict, output) { 532 | output = output || []; 533 | var idx = output.length; 534 | for (var i = 0, length = getLength(input); i < length; i++) { 535 | var value = input[i]; 536 | if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { 537 | // Flatten current level of array or arguments object. 538 | if (shallow) { 539 | var j = 0, len = value.length; 540 | while (j < len) output[idx++] = value[j++]; 541 | } else { 542 | flatten(value, shallow, strict, output); 543 | idx = output.length; 544 | } 545 | } else if (!strict) { 546 | output[idx++] = value; 547 | } 548 | } 549 | return output; 550 | }; 551 | 552 | // Flatten out an array, either recursively (by default), or just one level. 553 | _.flatten = function(array, shallow) { 554 | return flatten(array, shallow, false); 555 | }; 556 | 557 | // Return a version of the array that does not contain the specified value(s). 558 | _.without = restArguments(function(array, otherArrays) { 559 | return _.difference(array, otherArrays); 560 | }); 561 | 562 | // Produce a duplicate-free version of the array. If the array has already 563 | // been sorted, you have the option of using a faster algorithm. 564 | // The faster algorithm will not work with an iteratee if the iteratee 565 | // is not a one-to-one function, so providing an iteratee will disable 566 | // the faster algorithm. 567 | // Aliased as `unique`. 568 | _.uniq = _.unique = function(array, isSorted, iteratee, context) { 569 | if (!_.isBoolean(isSorted)) { 570 | context = iteratee; 571 | iteratee = isSorted; 572 | isSorted = false; 573 | } 574 | if (iteratee != null) iteratee = cb(iteratee, context); 575 | var result = []; 576 | var seen = []; 577 | for (var i = 0, length = getLength(array); i < length; i++) { 578 | var value = array[i], 579 | computed = iteratee ? iteratee(value, i, array) : value; 580 | if (isSorted && !iteratee) { 581 | if (!i || seen !== computed) result.push(value); 582 | seen = computed; 583 | } else if (iteratee) { 584 | if (!_.contains(seen, computed)) { 585 | seen.push(computed); 586 | result.push(value); 587 | } 588 | } else if (!_.contains(result, value)) { 589 | result.push(value); 590 | } 591 | } 592 | return result; 593 | }; 594 | 595 | // Produce an array that contains the union: each distinct element from all of 596 | // the passed-in arrays. 597 | _.union = restArguments(function(arrays) { 598 | return _.uniq(flatten(arrays, true, true)); 599 | }); 600 | 601 | // Produce an array that contains every item shared between all the 602 | // passed-in arrays. 603 | _.intersection = function(array) { 604 | var result = []; 605 | var argsLength = arguments.length; 606 | for (var i = 0, length = getLength(array); i < length; i++) { 607 | var item = array[i]; 608 | if (_.contains(result, item)) continue; 609 | var j; 610 | for (j = 1; j < argsLength; j++) { 611 | if (!_.contains(arguments[j], item)) break; 612 | } 613 | if (j === argsLength) result.push(item); 614 | } 615 | return result; 616 | }; 617 | 618 | // Take the difference between one array and a number of other arrays. 619 | // Only the elements present in just the first array will remain. 620 | _.difference = restArguments(function(array, rest) { 621 | rest = flatten(rest, true, true); 622 | return _.filter(array, function(value){ 623 | return !_.contains(rest, value); 624 | }); 625 | }); 626 | 627 | // Complement of _.zip. Unzip accepts an array of arrays and groups 628 | // each array's elements on shared indices. 629 | _.unzip = function(array) { 630 | var length = array && _.max(array, getLength).length || 0; 631 | var result = Array(length); 632 | 633 | for (var index = 0; index < length; index++) { 634 | result[index] = _.pluck(array, index); 635 | } 636 | return result; 637 | }; 638 | 639 | // Zip together multiple lists into a single array -- elements that share 640 | // an index go together. 641 | _.zip = restArguments(_.unzip); 642 | 643 | // Converts lists into objects. Pass either a single array of `[key, value]` 644 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 645 | // the corresponding values. Passing by pairs is the reverse of _.pairs. 646 | _.object = function(list, values) { 647 | var result = {}; 648 | for (var i = 0, length = getLength(list); i < length; i++) { 649 | if (values) { 650 | result[list[i]] = values[i]; 651 | } else { 652 | result[list[i][0]] = list[i][1]; 653 | } 654 | } 655 | return result; 656 | }; 657 | 658 | // Generator function to create the findIndex and findLastIndex functions. 659 | var createPredicateIndexFinder = function(dir) { 660 | return function(array, predicate, context) { 661 | predicate = cb(predicate, context); 662 | var length = getLength(array); 663 | var index = dir > 0 ? 0 : length - 1; 664 | for (; index >= 0 && index < length; index += dir) { 665 | if (predicate(array[index], index, array)) return index; 666 | } 667 | return -1; 668 | }; 669 | }; 670 | 671 | // Returns the first index on an array-like that passes a predicate test. 672 | _.findIndex = createPredicateIndexFinder(1); 673 | _.findLastIndex = createPredicateIndexFinder(-1); 674 | 675 | // Use a comparator function to figure out the smallest index at which 676 | // an object should be inserted so as to maintain order. Uses binary search. 677 | _.sortedIndex = function(array, obj, iteratee, context) { 678 | iteratee = cb(iteratee, context, 1); 679 | var value = iteratee(obj); 680 | var low = 0, high = getLength(array); 681 | while (low < high) { 682 | var mid = Math.floor((low + high) / 2); 683 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; 684 | } 685 | return low; 686 | }; 687 | 688 | // Generator function to create the indexOf and lastIndexOf functions. 689 | var createIndexFinder = function(dir, predicateFind, sortedIndex) { 690 | return function(array, item, idx) { 691 | var i = 0, length = getLength(array); 692 | if (typeof idx == 'number') { 693 | if (dir > 0) { 694 | i = idx >= 0 ? idx : Math.max(idx + length, i); 695 | } else { 696 | length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; 697 | } 698 | } else if (sortedIndex && idx && length) { 699 | idx = sortedIndex(array, item); 700 | return array[idx] === item ? idx : -1; 701 | } 702 | if (item !== item) { 703 | idx = predicateFind(slice.call(array, i, length), _.isNaN); 704 | return idx >= 0 ? idx + i : -1; 705 | } 706 | for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { 707 | if (array[idx] === item) return idx; 708 | } 709 | return -1; 710 | }; 711 | }; 712 | 713 | // Return the position of the first occurrence of an item in an array, 714 | // or -1 if the item is not included in the array. 715 | // If the array is large and already in sort order, pass `true` 716 | // for **isSorted** to use binary search. 717 | _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); 718 | _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); 719 | 720 | // Generate an integer Array containing an arithmetic progression. A port of 721 | // the native Python `range()` function. See 722 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 723 | _.range = function(start, stop, step) { 724 | if (stop == null) { 725 | stop = start || 0; 726 | start = 0; 727 | } 728 | if (!step) { 729 | step = stop < start ? -1 : 1; 730 | } 731 | 732 | var length = Math.max(Math.ceil((stop - start) / step), 0); 733 | var range = Array(length); 734 | 735 | for (var idx = 0; idx < length; idx++, start += step) { 736 | range[idx] = start; 737 | } 738 | 739 | return range; 740 | }; 741 | 742 | // Chunk a single array into multiple arrays, each containing `count` or fewer 743 | // items. 744 | _.chunk = function(array, count) { 745 | if (count == null || count < 1) return []; 746 | var result = []; 747 | var i = 0, length = array.length; 748 | while (i < length) { 749 | result.push(slice.call(array, i, i += count)); 750 | } 751 | return result; 752 | }; 753 | 754 | // Function (ahem) Functions 755 | // ------------------ 756 | 757 | // Determines whether to execute a function as a constructor 758 | // or a normal function with the provided arguments. 759 | var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { 760 | if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 761 | var self = baseCreate(sourceFunc.prototype); 762 | var result = sourceFunc.apply(self, args); 763 | if (_.isObject(result)) return result; 764 | return self; 765 | }; 766 | 767 | // Create a function bound to a given object (assigning `this`, and arguments, 768 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 769 | // available. 770 | _.bind = restArguments(function(func, context, args) { 771 | if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); 772 | var bound = restArguments(function(callArgs) { 773 | return executeBound(func, bound, context, this, args.concat(callArgs)); 774 | }); 775 | return bound; 776 | }); 777 | 778 | // Partially apply a function by creating a version that has had some of its 779 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 780 | // as a placeholder by default, allowing any combination of arguments to be 781 | // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. 782 | _.partial = restArguments(function(func, boundArgs) { 783 | var placeholder = _.partial.placeholder; 784 | var bound = function() { 785 | var position = 0, length = boundArgs.length; 786 | var args = Array(length); 787 | for (var i = 0; i < length; i++) { 788 | args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; 789 | } 790 | while (position < arguments.length) args.push(arguments[position++]); 791 | return executeBound(func, bound, this, this, args); 792 | }; 793 | return bound; 794 | }); 795 | 796 | _.partial.placeholder = _; 797 | 798 | // Bind a number of an object's methods to that object. Remaining arguments 799 | // are the method names to be bound. Useful for ensuring that all callbacks 800 | // defined on an object belong to it. 801 | _.bindAll = restArguments(function(obj, keys) { 802 | keys = flatten(keys, false, false); 803 | var index = keys.length; 804 | if (index < 1) throw new Error('bindAll must be passed function names'); 805 | while (index--) { 806 | var key = keys[index]; 807 | obj[key] = _.bind(obj[key], obj); 808 | } 809 | }); 810 | 811 | // Memoize an expensive function by storing its results. 812 | _.memoize = function(func, hasher) { 813 | var memoize = function(key) { 814 | var cache = memoize.cache; 815 | var address = '' + (hasher ? hasher.apply(this, arguments) : key); 816 | if (!has(cache, address)) cache[address] = func.apply(this, arguments); 817 | return cache[address]; 818 | }; 819 | memoize.cache = {}; 820 | return memoize; 821 | }; 822 | 823 | // Delays a function for the given number of milliseconds, and then calls 824 | // it with the arguments supplied. 825 | _.delay = restArguments(function(func, wait, args) { 826 | return setTimeout(function() { 827 | return func.apply(null, args); 828 | }, wait); 829 | }); 830 | 831 | // Defers a function, scheduling it to run after the current call stack has 832 | // cleared. 833 | _.defer = _.partial(_.delay, _, 1); 834 | 835 | // Returns a function, that, when invoked, will only be triggered at most once 836 | // during a given window of time. Normally, the throttled function will run 837 | // as much as it can, without ever going more than once per `wait` duration; 838 | // but if you'd like to disable the execution on the leading edge, pass 839 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 840 | _.throttle = function(func, wait, options) { 841 | var timeout, context, args, result; 842 | var previous = 0; 843 | if (!options) options = {}; 844 | 845 | var later = function() { 846 | previous = options.leading === false ? 0 : _.now(); 847 | timeout = null; 848 | result = func.apply(context, args); 849 | if (!timeout) context = args = null; 850 | }; 851 | 852 | var throttled = function() { 853 | var now = _.now(); 854 | if (!previous && options.leading === false) previous = now; 855 | var remaining = wait - (now - previous); 856 | context = this; 857 | args = arguments; 858 | if (remaining <= 0 || remaining > wait) { 859 | if (timeout) { 860 | clearTimeout(timeout); 861 | timeout = null; 862 | } 863 | previous = now; 864 | result = func.apply(context, args); 865 | if (!timeout) context = args = null; 866 | } else if (!timeout && options.trailing !== false) { 867 | timeout = setTimeout(later, remaining); 868 | } 869 | return result; 870 | }; 871 | 872 | throttled.cancel = function() { 873 | clearTimeout(timeout); 874 | previous = 0; 875 | timeout = context = args = null; 876 | }; 877 | 878 | return throttled; 879 | }; 880 | 881 | // Returns a function, that, as long as it continues to be invoked, will not 882 | // be triggered. The function will be called after it stops being called for 883 | // N milliseconds. If `immediate` is passed, trigger the function on the 884 | // leading edge, instead of the trailing. 885 | _.debounce = function(func, wait, immediate) { 886 | var timeout, result; 887 | 888 | var later = function(context, args) { 889 | timeout = null; 890 | if (args) result = func.apply(context, args); 891 | }; 892 | 893 | var debounced = restArguments(function(args) { 894 | if (timeout) clearTimeout(timeout); 895 | if (immediate) { 896 | var callNow = !timeout; 897 | timeout = setTimeout(later, wait); 898 | if (callNow) result = func.apply(this, args); 899 | } else { 900 | timeout = _.delay(later, wait, this, args); 901 | } 902 | 903 | return result; 904 | }); 905 | 906 | debounced.cancel = function() { 907 | clearTimeout(timeout); 908 | timeout = null; 909 | }; 910 | 911 | return debounced; 912 | }; 913 | 914 | // Returns the first function passed as an argument to the second, 915 | // allowing you to adjust arguments, run code before and after, and 916 | // conditionally execute the original function. 917 | _.wrap = function(func, wrapper) { 918 | return _.partial(wrapper, func); 919 | }; 920 | 921 | // Returns a negated version of the passed-in predicate. 922 | _.negate = function(predicate) { 923 | return function() { 924 | return !predicate.apply(this, arguments); 925 | }; 926 | }; 927 | 928 | // Returns a function that is the composition of a list of functions, each 929 | // consuming the return value of the function that follows. 930 | _.compose = function() { 931 | var args = arguments; 932 | var start = args.length - 1; 933 | return function() { 934 | var i = start; 935 | var result = args[start].apply(this, arguments); 936 | while (i--) result = args[i].call(this, result); 937 | return result; 938 | }; 939 | }; 940 | 941 | // Returns a function that will only be executed on and after the Nth call. 942 | _.after = function(times, func) { 943 | return function() { 944 | if (--times < 1) { 945 | return func.apply(this, arguments); 946 | } 947 | }; 948 | }; 949 | 950 | // Returns a function that will only be executed up to (but not including) the Nth call. 951 | _.before = function(times, func) { 952 | var memo; 953 | return function() { 954 | if (--times > 0) { 955 | memo = func.apply(this, arguments); 956 | } 957 | if (times <= 1) func = null; 958 | return memo; 959 | }; 960 | }; 961 | 962 | // Returns a function that will be executed at most one time, no matter how 963 | // often you call it. Useful for lazy initialization. 964 | _.once = _.partial(_.before, 2); 965 | 966 | _.restArguments = restArguments; 967 | 968 | // Object Functions 969 | // ---------------- 970 | 971 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. 972 | var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 973 | var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 974 | 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 975 | 976 | var collectNonEnumProps = function(obj, keys) { 977 | var nonEnumIdx = nonEnumerableProps.length; 978 | var constructor = obj.constructor; 979 | var proto = _.isFunction(constructor) && constructor.prototype || ObjProto; 980 | 981 | // Constructor is a special case. 982 | var prop = 'constructor'; 983 | if (has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); 984 | 985 | while (nonEnumIdx--) { 986 | prop = nonEnumerableProps[nonEnumIdx]; 987 | if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { 988 | keys.push(prop); 989 | } 990 | } 991 | }; 992 | 993 | // Retrieve the names of an object's own properties. 994 | // Delegates to **ECMAScript 5**'s native `Object.keys`. 995 | _.keys = function(obj) { 996 | if (!_.isObject(obj)) return []; 997 | if (nativeKeys) return nativeKeys(obj); 998 | var keys = []; 999 | for (var key in obj) if (has(obj, key)) keys.push(key); 1000 | // Ahem, IE < 9. 1001 | if (hasEnumBug) collectNonEnumProps(obj, keys); 1002 | return keys; 1003 | }; 1004 | 1005 | // Retrieve all the property names of an object. 1006 | _.allKeys = function(obj) { 1007 | if (!_.isObject(obj)) return []; 1008 | var keys = []; 1009 | for (var key in obj) keys.push(key); 1010 | // Ahem, IE < 9. 1011 | if (hasEnumBug) collectNonEnumProps(obj, keys); 1012 | return keys; 1013 | }; 1014 | 1015 | // Retrieve the values of an object's properties. 1016 | _.values = function(obj) { 1017 | var keys = _.keys(obj); 1018 | var length = keys.length; 1019 | var values = Array(length); 1020 | for (var i = 0; i < length; i++) { 1021 | values[i] = obj[keys[i]]; 1022 | } 1023 | return values; 1024 | }; 1025 | 1026 | // Returns the results of applying the iteratee to each element of the object. 1027 | // In contrast to _.map it returns an object. 1028 | _.mapObject = function(obj, iteratee, context) { 1029 | iteratee = cb(iteratee, context); 1030 | var keys = _.keys(obj), 1031 | length = keys.length, 1032 | results = {}; 1033 | for (var index = 0; index < length; index++) { 1034 | var currentKey = keys[index]; 1035 | results[currentKey] = iteratee(obj[currentKey], currentKey, obj); 1036 | } 1037 | return results; 1038 | }; 1039 | 1040 | // Convert an object into a list of `[key, value]` pairs. 1041 | // The opposite of _.object. 1042 | _.pairs = function(obj) { 1043 | var keys = _.keys(obj); 1044 | var length = keys.length; 1045 | var pairs = Array(length); 1046 | for (var i = 0; i < length; i++) { 1047 | pairs[i] = [keys[i], obj[keys[i]]]; 1048 | } 1049 | return pairs; 1050 | }; 1051 | 1052 | // Invert the keys and values of an object. The values must be serializable. 1053 | _.invert = function(obj) { 1054 | var result = {}; 1055 | var keys = _.keys(obj); 1056 | for (var i = 0, length = keys.length; i < length; i++) { 1057 | result[obj[keys[i]]] = keys[i]; 1058 | } 1059 | return result; 1060 | }; 1061 | 1062 | // Return a sorted list of the function names available on the object. 1063 | // Aliased as `methods`. 1064 | _.functions = _.methods = function(obj) { 1065 | var names = []; 1066 | for (var key in obj) { 1067 | if (_.isFunction(obj[key])) names.push(key); 1068 | } 1069 | return names.sort(); 1070 | }; 1071 | 1072 | // An internal function for creating assigner functions. 1073 | var createAssigner = function(keysFunc, defaults) { 1074 | return function(obj) { 1075 | var length = arguments.length; 1076 | if (defaults) obj = Object(obj); 1077 | if (length < 2 || obj == null) return obj; 1078 | for (var index = 1; index < length; index++) { 1079 | var source = arguments[index], 1080 | keys = keysFunc(source), 1081 | l = keys.length; 1082 | for (var i = 0; i < l; i++) { 1083 | var key = keys[i]; 1084 | if (!defaults || obj[key] === void 0) obj[key] = source[key]; 1085 | } 1086 | } 1087 | return obj; 1088 | }; 1089 | }; 1090 | 1091 | // Extend a given object with all the properties in passed-in object(s). 1092 | _.extend = createAssigner(_.allKeys); 1093 | 1094 | // Assigns a given object with all the own properties in the passed-in object(s). 1095 | // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 1096 | _.extendOwn = _.assign = createAssigner(_.keys); 1097 | 1098 | // Returns the first key on an object that passes a predicate test. 1099 | _.findKey = function(obj, predicate, context) { 1100 | predicate = cb(predicate, context); 1101 | var keys = _.keys(obj), key; 1102 | for (var i = 0, length = keys.length; i < length; i++) { 1103 | key = keys[i]; 1104 | if (predicate(obj[key], key, obj)) return key; 1105 | } 1106 | }; 1107 | 1108 | // Internal pick helper function to determine if `obj` has key `key`. 1109 | var keyInObj = function(value, key, obj) { 1110 | return key in obj; 1111 | }; 1112 | 1113 | // Return a copy of the object only containing the whitelisted properties. 1114 | _.pick = restArguments(function(obj, keys) { 1115 | var result = {}, iteratee = keys[0]; 1116 | if (obj == null) return result; 1117 | if (_.isFunction(iteratee)) { 1118 | if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); 1119 | keys = _.allKeys(obj); 1120 | } else { 1121 | iteratee = keyInObj; 1122 | keys = flatten(keys, false, false); 1123 | obj = Object(obj); 1124 | } 1125 | for (var i = 0, length = keys.length; i < length; i++) { 1126 | var key = keys[i]; 1127 | var value = obj[key]; 1128 | if (iteratee(value, key, obj)) result[key] = value; 1129 | } 1130 | return result; 1131 | }); 1132 | 1133 | // Return a copy of the object without the blacklisted properties. 1134 | _.omit = restArguments(function(obj, keys) { 1135 | var iteratee = keys[0], context; 1136 | if (_.isFunction(iteratee)) { 1137 | iteratee = _.negate(iteratee); 1138 | if (keys.length > 1) context = keys[1]; 1139 | } else { 1140 | keys = _.map(flatten(keys, false, false), String); 1141 | iteratee = function(value, key) { 1142 | return !_.contains(keys, key); 1143 | }; 1144 | } 1145 | return _.pick(obj, iteratee, context); 1146 | }); 1147 | 1148 | // Fill in a given object with default properties. 1149 | _.defaults = createAssigner(_.allKeys, true); 1150 | 1151 | // Creates an object that inherits from the given prototype object. 1152 | // If additional properties are provided then they will be added to the 1153 | // created object. 1154 | _.create = function(prototype, props) { 1155 | var result = baseCreate(prototype); 1156 | if (props) _.extendOwn(result, props); 1157 | return result; 1158 | }; 1159 | 1160 | // Create a (shallow-cloned) duplicate of an object. 1161 | _.clone = function(obj) { 1162 | if (!_.isObject(obj)) return obj; 1163 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 1164 | }; 1165 | 1166 | // Invokes interceptor with the obj, and then returns obj. 1167 | // The primary purpose of this method is to "tap into" a method chain, in 1168 | // order to perform operations on intermediate results within the chain. 1169 | _.tap = function(obj, interceptor) { 1170 | interceptor(obj); 1171 | return obj; 1172 | }; 1173 | 1174 | // Returns whether an object has a given set of `key:value` pairs. 1175 | _.isMatch = function(object, attrs) { 1176 | var keys = _.keys(attrs), length = keys.length; 1177 | if (object == null) return !length; 1178 | var obj = Object(object); 1179 | for (var i = 0; i < length; i++) { 1180 | var key = keys[i]; 1181 | if (attrs[key] !== obj[key] || !(key in obj)) return false; 1182 | } 1183 | return true; 1184 | }; 1185 | 1186 | 1187 | // Internal recursive comparison function for `isEqual`. 1188 | var eq, deepEq; 1189 | eq = function(a, b, aStack, bStack) { 1190 | // Identical objects are equal. `0 === -0`, but they aren't identical. 1191 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 1192 | if (a === b) return a !== 0 || 1 / a === 1 / b; 1193 | // `null` or `undefined` only equal to itself (strict comparison). 1194 | if (a == null || b == null) return false; 1195 | // `NaN`s are equivalent, but non-reflexive. 1196 | if (a !== a) return b !== b; 1197 | // Exhaust primitive checks 1198 | var type = typeof a; 1199 | if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; 1200 | return deepEq(a, b, aStack, bStack); 1201 | }; 1202 | 1203 | // Internal recursive comparison function for `isEqual`. 1204 | deepEq = function(a, b, aStack, bStack) { 1205 | // Unwrap any wrapped objects. 1206 | if (a instanceof _) a = a._wrapped; 1207 | if (b instanceof _) b = b._wrapped; 1208 | // Compare `[[Class]]` names. 1209 | var className = toString.call(a); 1210 | if (className !== toString.call(b)) return false; 1211 | switch (className) { 1212 | // Strings, numbers, regular expressions, dates, and booleans are compared by value. 1213 | case '[object RegExp]': 1214 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') 1215 | case '[object String]': 1216 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 1217 | // equivalent to `new String("5")`. 1218 | return '' + a === '' + b; 1219 | case '[object Number]': 1220 | // `NaN`s are equivalent, but non-reflexive. 1221 | // Object(NaN) is equivalent to NaN. 1222 | if (+a !== +a) return +b !== +b; 1223 | // An `egal` comparison is performed for other numeric values. 1224 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 1225 | case '[object Date]': 1226 | case '[object Boolean]': 1227 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 1228 | // millisecond representations. Note that invalid dates with millisecond representations 1229 | // of `NaN` are not equivalent. 1230 | return +a === +b; 1231 | case '[object Symbol]': 1232 | return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); 1233 | } 1234 | 1235 | var areArrays = className === '[object Array]'; 1236 | if (!areArrays) { 1237 | if (typeof a != 'object' || typeof b != 'object') return false; 1238 | 1239 | // Objects with different constructors are not equivalent, but `Object`s or `Array`s 1240 | // from different frames are. 1241 | var aCtor = a.constructor, bCtor = b.constructor; 1242 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && 1243 | _.isFunction(bCtor) && bCtor instanceof bCtor) 1244 | && ('constructor' in a && 'constructor' in b)) { 1245 | return false; 1246 | } 1247 | } 1248 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 1249 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 1250 | 1251 | // Initializing stack of traversed objects. 1252 | // It's done here since we only need them for objects and arrays comparison. 1253 | aStack = aStack || []; 1254 | bStack = bStack || []; 1255 | var length = aStack.length; 1256 | while (length--) { 1257 | // Linear search. Performance is inversely proportional to the number of 1258 | // unique nested structures. 1259 | if (aStack[length] === a) return bStack[length] === b; 1260 | } 1261 | 1262 | // Add the first object to the stack of traversed objects. 1263 | aStack.push(a); 1264 | bStack.push(b); 1265 | 1266 | // Recursively compare objects and arrays. 1267 | if (areArrays) { 1268 | // Compare array lengths to determine if a deep comparison is necessary. 1269 | length = a.length; 1270 | if (length !== b.length) return false; 1271 | // Deep compare the contents, ignoring non-numeric properties. 1272 | while (length--) { 1273 | if (!eq(a[length], b[length], aStack, bStack)) return false; 1274 | } 1275 | } else { 1276 | // Deep compare objects. 1277 | var keys = _.keys(a), key; 1278 | length = keys.length; 1279 | // Ensure that both objects contain the same number of properties before comparing deep equality. 1280 | if (_.keys(b).length !== length) return false; 1281 | while (length--) { 1282 | // Deep compare each member 1283 | key = keys[length]; 1284 | if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; 1285 | } 1286 | } 1287 | // Remove the first object from the stack of traversed objects. 1288 | aStack.pop(); 1289 | bStack.pop(); 1290 | return true; 1291 | }; 1292 | 1293 | // Perform a deep comparison to check if two objects are equal. 1294 | _.isEqual = function(a, b) { 1295 | return eq(a, b); 1296 | }; 1297 | 1298 | // Is a given array, string, or object empty? 1299 | // An "empty" object has no enumerable own-properties. 1300 | _.isEmpty = function(obj) { 1301 | if (obj == null) return true; 1302 | if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; 1303 | return _.keys(obj).length === 0; 1304 | }; 1305 | 1306 | // Is a given value a DOM element? 1307 | _.isElement = function(obj) { 1308 | return !!(obj && obj.nodeType === 1); 1309 | }; 1310 | 1311 | // Is a given value an array? 1312 | // Delegates to ECMA5's native Array.isArray 1313 | _.isArray = nativeIsArray || function(obj) { 1314 | return toString.call(obj) === '[object Array]'; 1315 | }; 1316 | 1317 | // Is a given variable an object? 1318 | _.isObject = function(obj) { 1319 | var type = typeof obj; 1320 | return type === 'function' || type === 'object' && !!obj; 1321 | }; 1322 | 1323 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError, isMap, isWeakMap, isSet, isWeakSet. 1324 | _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) { 1325 | _['is' + name] = function(obj) { 1326 | return toString.call(obj) === '[object ' + name + ']'; 1327 | }; 1328 | }); 1329 | 1330 | // Define a fallback version of the method in browsers (ahem, IE < 9), where 1331 | // there isn't any inspectable "Arguments" type. 1332 | if (!_.isArguments(arguments)) { 1333 | _.isArguments = function(obj) { 1334 | return has(obj, 'callee'); 1335 | }; 1336 | } 1337 | 1338 | // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, 1339 | // IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). 1340 | var nodelist = root.document && root.document.childNodes; 1341 | if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { 1342 | _.isFunction = function(obj) { 1343 | return typeof obj == 'function' || false; 1344 | }; 1345 | } 1346 | 1347 | // Is a given object a finite number? 1348 | _.isFinite = function(obj) { 1349 | return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj)); 1350 | }; 1351 | 1352 | // Is the given value `NaN`? 1353 | _.isNaN = function(obj) { 1354 | return _.isNumber(obj) && isNaN(obj); 1355 | }; 1356 | 1357 | // Is a given value a boolean? 1358 | _.isBoolean = function(obj) { 1359 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; 1360 | }; 1361 | 1362 | // Is a given value equal to null? 1363 | _.isNull = function(obj) { 1364 | return obj === null; 1365 | }; 1366 | 1367 | // Is a given variable undefined? 1368 | _.isUndefined = function(obj) { 1369 | return obj === void 0; 1370 | }; 1371 | 1372 | // Shortcut function for checking if an object has a given property directly 1373 | // on itself (in other words, not on a prototype). 1374 | _.has = function(obj, path) { 1375 | if (!_.isArray(path)) { 1376 | return has(obj, path); 1377 | } 1378 | var length = path.length; 1379 | for (var i = 0; i < length; i++) { 1380 | var key = path[i]; 1381 | if (obj == null || !hasOwnProperty.call(obj, key)) { 1382 | return false; 1383 | } 1384 | obj = obj[key]; 1385 | } 1386 | return !!length; 1387 | }; 1388 | 1389 | // Utility Functions 1390 | // ----------------- 1391 | 1392 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1393 | // previous owner. Returns a reference to the Underscore object. 1394 | _.noConflict = function() { 1395 | root._ = previousUnderscore; 1396 | return this; 1397 | }; 1398 | 1399 | // Keep the identity function around for default iteratees. 1400 | _.identity = function(value) { 1401 | return value; 1402 | }; 1403 | 1404 | // Predicate-generating functions. Often useful outside of Underscore. 1405 | _.constant = function(value) { 1406 | return function() { 1407 | return value; 1408 | }; 1409 | }; 1410 | 1411 | _.noop = function(){}; 1412 | 1413 | // Creates a function that, when passed an object, will traverse that object’s 1414 | // properties down the given `path`, specified as an array of keys or indexes. 1415 | _.property = function(path) { 1416 | if (!_.isArray(path)) { 1417 | return shallowProperty(path); 1418 | } 1419 | return function(obj) { 1420 | return deepGet(obj, path); 1421 | }; 1422 | }; 1423 | 1424 | // Generates a function for a given object that returns a given property. 1425 | _.propertyOf = function(obj) { 1426 | if (obj == null) { 1427 | return function(){}; 1428 | } 1429 | return function(path) { 1430 | return !_.isArray(path) ? obj[path] : deepGet(obj, path); 1431 | }; 1432 | }; 1433 | 1434 | // Returns a predicate for checking whether an object has a given set of 1435 | // `key:value` pairs. 1436 | _.matcher = _.matches = function(attrs) { 1437 | attrs = _.extendOwn({}, attrs); 1438 | return function(obj) { 1439 | return _.isMatch(obj, attrs); 1440 | }; 1441 | }; 1442 | 1443 | // Run a function **n** times. 1444 | _.times = function(n, iteratee, context) { 1445 | var accum = Array(Math.max(0, n)); 1446 | iteratee = optimizeCb(iteratee, context, 1); 1447 | for (var i = 0; i < n; i++) accum[i] = iteratee(i); 1448 | return accum; 1449 | }; 1450 | 1451 | // Return a random integer between min and max (inclusive). 1452 | _.random = function(min, max) { 1453 | if (max == null) { 1454 | max = min; 1455 | min = 0; 1456 | } 1457 | return min + Math.floor(Math.random() * (max - min + 1)); 1458 | }; 1459 | 1460 | // A (possibly faster) way to get the current timestamp as an integer. 1461 | _.now = Date.now || function() { 1462 | return new Date().getTime(); 1463 | }; 1464 | 1465 | // List of HTML entities for escaping. 1466 | var escapeMap = { 1467 | '&': '&', 1468 | '<': '<', 1469 | '>': '>', 1470 | '"': '"', 1471 | "'": ''', 1472 | '`': '`' 1473 | }; 1474 | var unescapeMap = _.invert(escapeMap); 1475 | 1476 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1477 | var createEscaper = function(map) { 1478 | var escaper = function(match) { 1479 | return map[match]; 1480 | }; 1481 | // Regexes for identifying a key that needs to be escaped. 1482 | var source = '(?:' + _.keys(map).join('|') + ')'; 1483 | var testRegexp = RegExp(source); 1484 | var replaceRegexp = RegExp(source, 'g'); 1485 | return function(string) { 1486 | string = string == null ? '' : '' + string; 1487 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 1488 | }; 1489 | }; 1490 | _.escape = createEscaper(escapeMap); 1491 | _.unescape = createEscaper(unescapeMap); 1492 | 1493 | // Traverses the children of `obj` along `path`. If a child is a function, it 1494 | // is invoked with its parent as context. Returns the value of the final 1495 | // child, or `fallback` if any child is undefined. 1496 | _.result = function(obj, path, fallback) { 1497 | if (!_.isArray(path)) path = [path]; 1498 | var length = path.length; 1499 | if (!length) { 1500 | return _.isFunction(fallback) ? fallback.call(obj) : fallback; 1501 | } 1502 | for (var i = 0; i < length; i++) { 1503 | var prop = obj == null ? void 0 : obj[path[i]]; 1504 | if (prop === void 0) { 1505 | prop = fallback; 1506 | i = length; // Ensure we don't continue iterating. 1507 | } 1508 | obj = _.isFunction(prop) ? prop.call(obj) : prop; 1509 | } 1510 | return obj; 1511 | }; 1512 | 1513 | // Generate a unique integer id (unique within the entire client session). 1514 | // Useful for temporary DOM ids. 1515 | var idCounter = 0; 1516 | _.uniqueId = function(prefix) { 1517 | var id = ++idCounter + ''; 1518 | return prefix ? prefix + id : id; 1519 | }; 1520 | 1521 | // By default, Underscore uses ERB-style template delimiters, change the 1522 | // following template settings to use alternative delimiters. 1523 | _.templateSettings = { 1524 | evaluate: /<%([\s\S]+?)%>/g, 1525 | interpolate: /<%=([\s\S]+?)%>/g, 1526 | escape: /<%-([\s\S]+?)%>/g 1527 | }; 1528 | 1529 | // When customizing `templateSettings`, if you don't want to define an 1530 | // interpolation, evaluation or escaping regex, we need one that is 1531 | // guaranteed not to match. 1532 | var noMatch = /(.)^/; 1533 | 1534 | // Certain characters need to be escaped so that they can be put into a 1535 | // string literal. 1536 | var escapes = { 1537 | "'": "'", 1538 | '\\': '\\', 1539 | '\r': 'r', 1540 | '\n': 'n', 1541 | '\u2028': 'u2028', 1542 | '\u2029': 'u2029' 1543 | }; 1544 | 1545 | var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; 1546 | 1547 | var escapeChar = function(match) { 1548 | return '\\' + escapes[match]; 1549 | }; 1550 | 1551 | // JavaScript micro-templating, similar to John Resig's implementation. 1552 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1553 | // and correctly escapes quotes within interpolated code. 1554 | // NB: `oldSettings` only exists for backwards compatibility. 1555 | _.template = function(text, settings, oldSettings) { 1556 | if (!settings && oldSettings) settings = oldSettings; 1557 | settings = _.defaults({}, settings, _.templateSettings); 1558 | 1559 | // Combine delimiters into one regular expression via alternation. 1560 | var matcher = RegExp([ 1561 | (settings.escape || noMatch).source, 1562 | (settings.interpolate || noMatch).source, 1563 | (settings.evaluate || noMatch).source 1564 | ].join('|') + '|$', 'g'); 1565 | 1566 | // Compile the template source, escaping string literals appropriately. 1567 | var index = 0; 1568 | var source = "__p+='"; 1569 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1570 | source += text.slice(index, offset).replace(escapeRegExp, escapeChar); 1571 | index = offset + match.length; 1572 | 1573 | if (escape) { 1574 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1575 | } else if (interpolate) { 1576 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1577 | } else if (evaluate) { 1578 | source += "';\n" + evaluate + "\n__p+='"; 1579 | } 1580 | 1581 | // Adobe VMs need the match returned to produce the correct offset. 1582 | return match; 1583 | }); 1584 | source += "';\n"; 1585 | 1586 | // If a variable is not specified, place data values in local scope. 1587 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1588 | 1589 | source = "var __t,__p='',__j=Array.prototype.join," + 1590 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1591 | source + 'return __p;\n'; 1592 | 1593 | var render; 1594 | try { 1595 | render = new Function(settings.variable || 'obj', '_', source); 1596 | } catch (e) { 1597 | e.source = source; 1598 | throw e; 1599 | } 1600 | 1601 | var template = function(data) { 1602 | return render.call(this, data, _); 1603 | }; 1604 | 1605 | // Provide the compiled source as a convenience for precompilation. 1606 | var argument = settings.variable || 'obj'; 1607 | template.source = 'function(' + argument + '){\n' + source + '}'; 1608 | 1609 | return template; 1610 | }; 1611 | 1612 | // Add a "chain" function. Start chaining a wrapped Underscore object. 1613 | _.chain = function(obj) { 1614 | var instance = _(obj); 1615 | instance._chain = true; 1616 | return instance; 1617 | }; 1618 | 1619 | // OOP 1620 | // --------------- 1621 | // If Underscore is called as a function, it returns a wrapped object that 1622 | // can be used OO-style. This wrapper holds altered versions of all the 1623 | // underscore functions. Wrapped objects may be chained. 1624 | 1625 | // Helper function to continue chaining intermediate results. 1626 | var chainResult = function(instance, obj) { 1627 | return instance._chain ? _(obj).chain() : obj; 1628 | }; 1629 | 1630 | // Add your own custom functions to the Underscore object. 1631 | _.mixin = function(obj) { 1632 | _.each(_.functions(obj), function(name) { 1633 | var func = _[name] = obj[name]; 1634 | _.prototype[name] = function() { 1635 | var args = [this._wrapped]; 1636 | push.apply(args, arguments); 1637 | return chainResult(this, func.apply(_, args)); 1638 | }; 1639 | }); 1640 | return _; 1641 | }; 1642 | 1643 | // Add all of the Underscore functions to the wrapper object. 1644 | _.mixin(_); 1645 | 1646 | // Add all mutator Array functions to the wrapper. 1647 | _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1648 | var method = ArrayProto[name]; 1649 | _.prototype[name] = function() { 1650 | var obj = this._wrapped; 1651 | method.apply(obj, arguments); 1652 | if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; 1653 | return chainResult(this, obj); 1654 | }; 1655 | }); 1656 | 1657 | // Add all accessor Array functions to the wrapper. 1658 | _.each(['concat', 'join', 'slice'], function(name) { 1659 | var method = ArrayProto[name]; 1660 | _.prototype[name] = function() { 1661 | return chainResult(this, method.apply(this._wrapped, arguments)); 1662 | }; 1663 | }); 1664 | 1665 | // Extracts the result from a wrapped and chained object. 1666 | _.prototype.value = function() { 1667 | return this._wrapped; 1668 | }; 1669 | 1670 | // Provide unwrapping proxy for some methods used in engine operations 1671 | // such as arithmetic and JSON stringification. 1672 | _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; 1673 | 1674 | _.prototype.toString = function() { 1675 | return String(this._wrapped); 1676 | }; 1677 | 1678 | // AMD registration happens at the end for compatibility with AMD loaders 1679 | // that may not enforce next-turn semantics on modules. Even though general 1680 | // practice for AMD registration is to be anonymous, underscore registers 1681 | // as a named module because, like jQuery, it is a base library that is 1682 | // popular enough to be bundled in a third party lib, but not be part of 1683 | // an AMD load request. Those cases could generate an error when an 1684 | // anonymous define() is called outside of a loader request. 1685 | if (typeof define == 'function' && define.amd) { 1686 | define('underscore', [], function() { 1687 | return _; 1688 | }); 1689 | } 1690 | }()); 1691 | --------------------------------------------------------------------------------