├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── css │ ├── suggestions.css │ └── suggestions.min.css └── js │ ├── jquery.suggestions.js │ └── jquery.suggestions.min.js ├── examples ├── constraints │ ├── code.js │ ├── index.html │ └── styles.css ├── fias │ ├── code.js │ ├── index.html │ └── styles.css ├── granular │ ├── code.js │ ├── index.html │ └── styles.css ├── test │ ├── code.js │ ├── index.html │ └── styles.css └── token.js ├── gulpfile.js ├── less └── suggestions.less ├── package-lock.json ├── package.json ├── src ├── includes │ ├── ajax.js │ ├── bounds.js │ ├── constants.js │ ├── constraints.js │ ├── container.js │ ├── default-options.js │ ├── dom.js │ ├── element.js │ ├── enrich.js │ ├── geolocation.js │ ├── jqapi.js │ ├── matchers.js │ ├── notificator.js │ ├── promo.js │ ├── select.js │ ├── status.js │ ├── suggestions.js │ ├── types.js │ ├── types │ │ ├── address.js │ │ ├── bank.js │ │ ├── email.js │ │ ├── fias.js │ │ ├── name.js │ │ ├── outward.js │ │ └── party.js │ ├── utils.js │ └── utils │ │ ├── collection.js │ │ ├── func.js │ │ ├── lang.js │ │ ├── object.js │ │ └── text.js └── main.js └── test ├── helpers └── helpers.js ├── karma.full.js ├── karma.minified.js └── specs ├── add_space_on_select_spec.js ├── after_select_spec.js ├── autoselect_spec.js ├── bounds.js ├── constraint_location_spec.js ├── constraints_spec.js ├── email_spec.js ├── enrich_spec.js ├── events_spec.js ├── fias_spec.js ├── fix_data_spec.js ├── format_select_spec.js ├── geolocation_spec.js ├── highlight_spec.js ├── initialization_specs.js ├── navigation_spec.js ├── plugin_spec.js ├── promo_spec.js ├── select_nothing_spec.js ├── select_on_blur_spec.js ├── select_on_enter_spec.js ├── select_on_space.js └── status_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .grunt 2 | .idea 3 | node_modules 4 | npm-debug.log 5 | tmp 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.15" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Human Factor Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/hflabs/suggestions-jquery.png?branch=master)](https://travis-ci.org/hflabs/suggestions-jquery) 2 | 3 | jQuery-плагин Подсказок DaData.ru 4 | ================== 5 | 6 | jQuery-плагин для сервиса [подсказок DaData.ru](https://dadata.ru/suggestions/). 7 | 8 | Примеры подключения: 9 | 10 | - Для «[Дадаты](https://dadata.ru/suggestions/usage/address/)» 11 | - Для «[коробки](https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669104)» 12 | 13 | Настройки: 14 | 15 | - [параметры](https://confluence.hflabs.ru/pages/viewpage.action?pageId=207454318) 16 | - [методы и свойства](https://confluence.hflabs.ru/pages/viewpage.action?pageId=207454322) 17 | - [колбеки](https://confluence.hflabs.ru/pages/viewpage.action?pageId=207454320) 18 | - [события](https://confluence.hflabs.ru/pages/viewpage.action?pageId=480542795) 19 | - [cтили](https://confluence.hflabs.ru/pages/viewpage.action?pageId=207454324) 20 | 21 | npm-пакет: [suggestions-jquery](https://www.npmjs.com/package/suggestions-jquery) 22 | -------------------------------------------------------------------------------- /dist/css/suggestions.css: -------------------------------------------------------------------------------- 1 | .suggestions-nowrap { 2 | white-space: nowrap; 3 | } 4 | /** 5 | * Основной INPUT 6 | */ 7 | .suggestions-input { 8 | -ms-box-sizing: border-box; 9 | -moz-box-sizing: border-box; 10 | -webkit-box-sizing: border-box; 11 | box-sizing: border-box; 12 | width: 100%; 13 | } 14 | .suggestions-input::-ms-clear { 15 | display: none; 16 | } 17 | .suggestions-wrapper { 18 | position: relative; 19 | margin: 0; 20 | padding: 0; 21 | vertical-align: top; 22 | -webkit-text-size-adjust: 100%; 23 | } 24 | /** 25 | * Выпадающий блок с найденными подсказками 26 | */ 27 | .suggestions-suggestions { 28 | background: #fff; 29 | border: 1px solid #999; 30 | -ms-box-sizing: border-box; 31 | -moz-box-sizing: border-box; 32 | -webkit-box-sizing: border-box; 33 | box-sizing: border-box; 34 | cursor: default; 35 | left: 0; 36 | min-width: 100%; 37 | position: absolute; 38 | z-index: 9999; 39 | -webkit-text-size-adjust: 100%; 40 | } 41 | .suggestions-suggestions strong { 42 | font-weight: normal; 43 | color: #3399ff; 44 | } 45 | .suggestions-suggestions.suggestions-mobile { 46 | border-style: none; 47 | } 48 | .suggestions-suggestions.suggestions-mobile .suggestions-suggestion { 49 | border-bottom: 1px solid #ddd; 50 | } 51 | /** 52 | * Контейнер для одной подсказки 53 | */ 54 | .suggestions-suggestion { 55 | padding: 4px 4px; 56 | overflow: hidden; 57 | } 58 | .suggestions-suggestion:hover { 59 | background: #f7f7f7; 60 | } 61 | /** 62 | * Выбранная (активная) подсказка 63 | */ 64 | .suggestions-selected { 65 | background: #f0f0f0; 66 | } 67 | .suggestions-selected:hover { 68 | background: #f0f0f0; 69 | } 70 | /** 71 | * Информационный блок в верхней части выпадашки с подсказками 72 | */ 73 | .suggestions-hint { 74 | padding: 4px 4px; 75 | white-space: nowrap; 76 | overflow: hidden; 77 | color: #777; 78 | font-size: 85%; 79 | line-height: 20px; 80 | } 81 | /** 82 | * Дополнительный текст в подсказке, который идет второй строкой 83 | */ 84 | .suggestions-subtext { 85 | color: #777; 86 | } 87 | /** 88 | * Размещает дополнительный текст в одну строку с основным текстом подсказки 89 | */ 90 | .suggestions-subtext_inline { 91 | display: inline-block; 92 | min-width: 6em; 93 | vertical-align: bottom; 94 | margin: 0 0.5em 0 0; 95 | } 96 | /** 97 | * Разделитель нескольких дополнительных текстов 98 | */ 99 | .suggestions-subtext-delimiter { 100 | display: inline-block; 101 | width: 2px; 102 | } 103 | /** 104 | * Выделяет подсказку 105 | */ 106 | .suggestions-subtext_label { 107 | margin: 0 0 0 0.25em; 108 | -webkit-border-radius: 3px; 109 | -moz-border-radius: 3px; 110 | border-radius: 3px; 111 | padding: 0 3px; 112 | background: #f5f5f5; 113 | font-size: 85%; 114 | } 115 | .suggestions-value[data-suggestion-status="LIQUIDATED"] { 116 | position: relative; 117 | } 118 | .suggestions-value[data-suggestion-status="LIQUIDATED"]:after { 119 | position: absolute; 120 | left: 0; 121 | right: 0; 122 | top: 50%; 123 | border-top: 1px solid rgba(0, 0, 0, 0.4); 124 | content: ""; 125 | } 126 | /** 127 | * Промо-блок 128 | */ 129 | .suggestions-promo { 130 | font-size: 85%; 131 | display: none; 132 | color: #777; 133 | padding: 4px; 134 | text-align: center; 135 | } 136 | .suggestions-promo a { 137 | color: #777; 138 | display: block; 139 | filter: grayscale(100%); 140 | line-height: 20px; 141 | text-decoration: none; 142 | } 143 | .suggestions-promo a:hover { 144 | filter: grayscale(0); 145 | } 146 | .suggestions-promo svg { 147 | height: 20px; 148 | vertical-align: bottom; 149 | } 150 | @media screen and (min-width: 600px) { 151 | .suggestions-promo { 152 | position: absolute; 153 | top: 0; 154 | right: 0; 155 | text-align: left; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /dist/css/suggestions.min.css: -------------------------------------------------------------------------------- 1 | .suggestions-nowrap{white-space:nowrap}.suggestions-input{-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.suggestions-input::-ms-clear{display:none}.suggestions-wrapper{position:relative;margin:0;padding:0;vertical-align:top;-webkit-text-size-adjust:100%}.suggestions-suggestions{background:#fff;border:1px solid #999;-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:default;left:0;min-width:100%;position:absolute;z-index:9999;-webkit-text-size-adjust:100%}.suggestions-suggestions strong{font-weight:400;color:#39f}.suggestions-suggestions.suggestions-mobile{border-style:none}.suggestions-suggestions.suggestions-mobile .suggestions-suggestion{border-bottom:1px solid #ddd}.suggestions-suggestion{padding:4px 4px;overflow:hidden}.suggestions-suggestion:hover{background:#f7f7f7}.suggestions-selected{background:#f0f0f0}.suggestions-selected:hover{background:#f0f0f0}.suggestions-hint{padding:4px 4px;white-space:nowrap;overflow:hidden;color:#777;font-size:85%;line-height:20px}.suggestions-subtext{color:#777}.suggestions-subtext_inline{display:inline-block;min-width:6em;vertical-align:bottom;margin:0 .5em 0 0}.suggestions-subtext-delimiter{display:inline-block;width:2px}.suggestions-subtext_label{margin:0 0 0 .25em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:0 3px;background:#f5f5f5;font-size:85%}.suggestions-value[data-suggestion-status=LIQUIDATED]{position:relative}.suggestions-value[data-suggestion-status=LIQUIDATED]:after{position:absolute;left:0;right:0;top:50%;border-top:1px solid rgba(0,0,0,.4);content:""}.suggestions-promo{font-size:85%;display:none;color:#777;padding:4px;text-align:center}.suggestions-promo a{color:#777;display:block;filter:grayscale(100%);line-height:20px;text-decoration:none}.suggestions-promo a:hover{filter:grayscale(0)}.suggestions-promo svg{height:20px;vertical-align:bottom}@media screen and (min-width:600px){.suggestions-promo{position:absolute;top:0;right:0;text-align:left}} -------------------------------------------------------------------------------- /examples/constraints/code.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | $(function() { 3 | Token.init(); 4 | 5 | function switchTo(choice) { 6 | var sgt = $("#address").suggestions(), 7 | lbl = $("#label"); 8 | if (switchers[choice] !== undefined) { 9 | $("#address").val(""); 10 | switchers[choice].call(this, sgt, lbl); 11 | } 12 | } 13 | 14 | var switchers = {}; 15 | 16 | switchers["none"] = function(sgt, lbl) { 17 | lbl.text("Без ограничений"); 18 | sgt.setOptions({ 19 | constraints: {} 20 | }); 21 | }; 22 | 23 | switchers["foreign"] = function(sgt, lbl) { 24 | lbl.text("Конкретная страна (Казахстан)"); 25 | sgt.setOptions({ 26 | constraints: { 27 | locations: { country_iso_code: "KZ" } 28 | } 29 | }); 30 | }; 31 | 32 | switchers["msk"] = function(sgt, lbl) { 33 | lbl.text("Конкретный регион (Москва)"); 34 | sgt.setOptions({ 35 | constraints: { 36 | // ограничиваем поиск Москвой 37 | locations: { region: "Москва" }, 38 | deletable: true 39 | }, 40 | // в списке подсказок не показываем область 41 | restrict_value: true 42 | }); 43 | }; 44 | 45 | switchers["nsk"] = function(sgt, lbl) { 46 | lbl.text("Конкретный город (Новосбирск)"); 47 | sgt.setOptions({ 48 | constraints: { 49 | label: "Новосибирск", 50 | // ограничиваем поиск Новосибирском 51 | locations: { 52 | region: "Новосибирская", 53 | city: "Новосибирск" 54 | }, 55 | // даем пользователю возможность снять ограничение 56 | deletable: true 57 | }, 58 | // в списке подсказок не показываем область и город 59 | restrict_value: true 60 | }); 61 | }; 62 | 63 | switchers["sochi-adlersky"] = function(sgt, lbl) { 64 | lbl.text("Внутригородской район (г Сочи, Адлерский р-н)"); 65 | sgt.setOptions({ 66 | constraints: { 67 | label: "г Сочи, Адлерский р-н", 68 | // ограничиваем поиск Новосибирском 69 | locations: { 70 | city: "Сочи", 71 | city_district: "Адлерский" 72 | }, 73 | // даем пользователю возможность снять ограничение 74 | deletable: true 75 | }, 76 | // в списке подсказок не показываем область и город 77 | restrict_value: true 78 | }); 79 | }; 80 | 81 | switchers["kladr"] = function(sgt, lbl) { 82 | lbl.text("Ограничение по коду КЛАДР (Тольятти)"); 83 | sgt.setOptions({ 84 | constraints: { 85 | label: "Тольятти", 86 | // ограничиваем поиск городом Тольятти по коду КЛАДР 87 | locations: { kladr_id: "63000007" } 88 | }, 89 | // в списке подсказок не показываем область и город 90 | restrict_value: true 91 | }); 92 | }; 93 | 94 | switchers["fias"] = function(sgt, lbl) { 95 | lbl.text( 96 | "Ограничение по коду ФИАС (Краснодарский край, restrict_value = true)" 97 | ); 98 | sgt.setOptions({ 99 | constraints: { 100 | label: "Краснодарский край", 101 | // ограничиваем поиск Красндарским Краем по коду ФИАС 102 | locations: { 103 | region_fias_id: "d00e1013-16bd-4c09-b3d5-3cb09fc54bd8" 104 | } 105 | }, 106 | // в списке подсказок не показываем регион 107 | restrict_value: true 108 | }); 109 | }; 110 | 111 | switchers["fias-no-restrict"] = function(sgt, lbl) { 112 | lbl.text( 113 | "Ограничение по коду ФИАС (Краснодарский край, restrict_value = false)" 114 | ); 115 | sgt.setOptions({ 116 | constraints: { 117 | label: "Краснодарский край", 118 | // ограничиваем поиск Красндарским Краем по коду ФИАС 119 | locations: { 120 | region_fias_id: "d00e1013-16bd-4c09-b3d5-3cb09fc54bd8" 121 | } 122 | }, 123 | // в списке подсказок не показываем регион 124 | restrict_value: false 125 | }); 126 | }; 127 | 128 | switchers["regions"] = function(sgt, lbl) { 129 | lbl.text("Несколько регионов (Москва и Московская область)"); 130 | sgt.setOptions({ 131 | constraints: [ 132 | // Москва 133 | { 134 | locations: { region: "Москва" }, 135 | deletable: true 136 | }, 137 | // Московская область 138 | { 139 | label: "МО", 140 | locations: { kladr_id: "50" }, 141 | deletable: true 142 | } 143 | ] 144 | }); 145 | }; 146 | 147 | switchers["fd"] = function(sgt, lbl) { 148 | lbl.text("Федеральный округ (ЮФО)"); 149 | sgt.setOptions({ 150 | constraints: { 151 | label: "ЮФО", 152 | // несколько ограничений по ИЛИ 153 | locations: [ 154 | { region: "адыгея" }, 155 | { region: "астраханская" }, 156 | { region: "волгоградская" }, 157 | { region: "калмыкия" }, 158 | { region: "краснодарский" }, 159 | { region: "ростовская" } 160 | ] 161 | } 162 | }); 163 | }; 164 | 165 | $("#address").suggestions({ 166 | token: Token.get(), 167 | type: "ADDRESS", 168 | constraints: {}, 169 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 170 | onSelect: function(suggestion) { 171 | console.log(suggestion); 172 | } 173 | }); 174 | 175 | $("#switcher a").click(function(e) { 176 | e.preventDefault(); 177 | switchTo($(this).data("switch")); 178 | }); 179 | 180 | $("#fio").suggestions({ 181 | token: Token.get(), 182 | type: "NAME", 183 | constraints: {}, 184 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 185 | onSelect: function(suggestion) { 186 | console.log(suggestion); 187 | } 188 | }); 189 | 190 | $("#party").suggestions({ 191 | token: Token.get(), 192 | type: "PARTY", 193 | constraints: {}, 194 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 195 | onSelect: function(suggestion) { 196 | console.log(suggestion); 197 | } 198 | }); 199 | 200 | $("#email").suggestions({ 201 | token: Token.get(), 202 | type: "EMAIL", 203 | constraints: {}, 204 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 205 | onSelect: function(suggestion) { 206 | console.log(suggestion); 207 | } 208 | }); 209 | 210 | $("#bank").suggestions({ 211 | token: Token.get(), 212 | type: "BANK", 213 | constraints: {}, 214 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 215 | onSelect: function(suggestion) { 216 | console.log(suggestion); 217 | } 218 | }); 219 | 220 | $("#region-city").suggestions({ 221 | token: Token.get(), 222 | type: "ADDRESS", 223 | constraints: {}, 224 | bounds: "region-city", 225 | /* Вызывается, когда пользователь выбирает одну из подсказок */ 226 | onSelect: function(suggestion) { 227 | console.log(suggestion); 228 | } 229 | }); 230 | }); 231 | })(); 232 | -------------------------------------------------------------------------------- /examples/constraints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ограничиваем сектор поиска в подсказках по адресу 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 |

21 |
22 | 23 |
24 |

Ограничиваем сектор поиска в подсказках по адресу

25 |

Без ограничений:

26 | 27 |

Выберите вариант:

28 | 60 |
61 | 62 |
63 |

Поиск по ФИО

64 | 65 |
66 | 67 |
68 |

Поиск по организациям

69 | 70 |
71 | 72 |
73 |

Поиск по email

74 | 75 |
76 | 77 |
78 |

Поиск по банкам

79 | 80 |
81 | 82 |
83 |

bounds = region-city

84 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/constraints/styles.css: -------------------------------------------------------------------------------- 1 | input { 2 | font-size: 16px; 3 | padding: 4px; 4 | } 5 | #switcher a { 6 | text-decoration: none; 7 | border-bottom: 1px dotted blue; 8 | } 9 | #switcher a:hover { 10 | border-bottom: none; 11 | } 12 | #switcher li { 13 | line-height: 1.5; 14 | } 15 | -------------------------------------------------------------------------------- /examples/fias/code.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function log(suggestion) { 3 | console.log(suggestion); 4 | } 5 | 6 | $(function() { 7 | Token.init(); 8 | 9 | var serviceUrl = 10 | "http://suggestions-1.office.dadata.ru:8080/suggestions/api/4_1/rs", 11 | token = Token.get(), 12 | type = "FIAS", 13 | $address = $("#address"), 14 | $region = $("#region"), 15 | $area = $("#area"), 16 | $city = $("#city"), 17 | $cityDistrict = $("#city_district"), 18 | $settlement = $("#settlement"), 19 | $planningStructure = $("#planning_structure"), 20 | $street = $("#street"), 21 | $house = $("#house"); 22 | 23 | // одной строкой 24 | $address.suggestions({ 25 | serviceUrl: serviceUrl, 26 | token: token, 27 | type: type, 28 | onSelect: log 29 | }); 30 | 31 | // регион 32 | $region.suggestions({ 33 | serviceUrl: serviceUrl, 34 | token: token, 35 | type: type, 36 | hint: false, 37 | bounds: "region", 38 | onSelect: log 39 | }); 40 | 41 | // район 42 | $area.suggestions({ 43 | serviceUrl: serviceUrl, 44 | token: token, 45 | type: type, 46 | hint: false, 47 | bounds: "area", 48 | constraints: $region, 49 | onSelect: log 50 | }); 51 | 52 | // город и населенный пункт 53 | $city.suggestions({ 54 | serviceUrl: serviceUrl, 55 | token: token, 56 | type: type, 57 | hint: false, 58 | bounds: "city", 59 | constraints: $area, 60 | onSelect: log 61 | }); 62 | 63 | // район города 64 | $cityDistrict.suggestions({ 65 | serviceUrl: serviceUrl, 66 | token: token, 67 | type: type, 68 | hint: false, 69 | bounds: "city_district", 70 | constraints: $city, 71 | onSelect: log 72 | }); 73 | 74 | // населенный пункт 75 | $settlement.suggestions({ 76 | serviceUrl: serviceUrl, 77 | token: token, 78 | type: type, 79 | hint: false, 80 | bounds: "settlement", 81 | constraints: $cityDistrict, 82 | onSelect: log 83 | }); 84 | 85 | // план. структура 86 | $planningStructure.suggestions({ 87 | serviceUrl: serviceUrl, 88 | token: token, 89 | type: type, 90 | hint: false, 91 | bounds: "planning_structure", 92 | constraints: $settlement, 93 | onSelect: log 94 | }); 95 | 96 | // улица 97 | $street.suggestions({ 98 | serviceUrl: serviceUrl, 99 | token: token, 100 | type: type, 101 | hint: false, 102 | bounds: "street", 103 | constraints: $planningStructure, 104 | onSelect: log 105 | }); 106 | 107 | // дом 108 | $house.suggestions({ 109 | serviceUrl: serviceUrl, 110 | token: token, 111 | type: type, 112 | hint: false, 113 | bounds: "house", 114 | constraints: $street, 115 | onSelect: log 116 | }); 117 | }); 118 | })(jQuery); 119 | -------------------------------------------------------------------------------- /examples/fias/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Подсказки по ФИАС 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

18 | 19 |

20 |
21 | 22 |
23 |

Подсказки по ФИАС одной строкой

24 | 25 |
26 | 27 |
28 |

Гранулярные подсказки по ФИАС (все поля)

29 | 30 | 31 | 32 |

33 | 34 | 35 | 36 |

37 | 38 | 39 | 40 |

41 | 42 | 43 | 44 |

45 | 46 | 47 | 48 |

49 | 50 | 51 | 52 |

53 | 54 | 55 | 56 |

57 | 58 | 59 | 60 |

61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/fias/styles.css: -------------------------------------------------------------------------------- 1 | .info p { 2 | background: #31b0d5; 3 | margin: 0; 4 | padding: 5px; 5 | } 6 | 7 | .container { 8 | width: 30em; 9 | } 10 | 11 | input { 12 | font-size: 16px; 13 | padding: 4px; 14 | } 15 | -------------------------------------------------------------------------------- /examples/granular/code.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function geolocateCity($city) { 3 | var citySgt = $city.suggestions(); 4 | citySgt.getGeoLocation().done(function(locationData) { 5 | if (locationData.city) { 6 | var suggestionVal = 7 | locationData.city_type + " " + locationData.city, 8 | suggestion = { value: suggestionVal, data: locationData }; 9 | citySgt.setSuggestion(suggestion); 10 | } else if (locationData.region) { 11 | var suggestionVal = 12 | locationData.region_type + " " + locationData.region, 13 | suggestion = { value: suggestionVal, data: locationData }; 14 | citySgt.setSuggestion(suggestion); 15 | } 16 | }); 17 | } 18 | 19 | $(function() { 20 | Token.init(); 21 | 22 | var serviceUrl = "https://suggestions.dadata.ru/suggestions/api/4_1/rs", 23 | token = Token.get(), 24 | type = "ADDRESS", 25 | $country = $("#country"), 26 | $region = $("#region"), 27 | $area = $("#area"), 28 | $city = $("#city"), 29 | $cityDistrict = $("#city_district"), 30 | $settlement = $("#settlement"), 31 | $street = $("#street"), 32 | $house = $("#house"); 33 | 34 | // страна 35 | $country.suggestions({ 36 | serviceUrl: serviceUrl, 37 | token: token, 38 | type: type, 39 | hint: false, 40 | bounds: "country", 41 | constraints: { 42 | locations: { country_iso_code: "*" } 43 | } 44 | }); 45 | 46 | // регион 47 | $region.suggestions({ 48 | serviceUrl: serviceUrl, 49 | token: token, 50 | type: type, 51 | hint: false, 52 | bounds: "region", 53 | constraints: $country 54 | }); 55 | 56 | // район 57 | $area.suggestions({ 58 | serviceUrl: serviceUrl, 59 | token: token, 60 | type: type, 61 | hint: false, 62 | bounds: "area", 63 | constraints: $region 64 | }); 65 | 66 | // город и населенный пункт 67 | $city.suggestions({ 68 | serviceUrl: serviceUrl, 69 | token: token, 70 | type: type, 71 | hint: false, 72 | bounds: "city", 73 | constraints: $area 74 | }); 75 | 76 | // район города 77 | $cityDistrict.suggestions({ 78 | serviceUrl: serviceUrl, 79 | token: token, 80 | type: type, 81 | hint: false, 82 | bounds: "city_district", 83 | constraints: $city 84 | }); 85 | 86 | // geolocateCity($city); 87 | 88 | // город и населенный пункт 89 | $settlement.suggestions({ 90 | serviceUrl: serviceUrl, 91 | token: token, 92 | type: type, 93 | hint: false, 94 | bounds: "settlement", 95 | constraints: $cityDistrict 96 | }); 97 | 98 | // улица 99 | $street.suggestions({ 100 | serviceUrl: serviceUrl, 101 | token: token, 102 | type: type, 103 | hint: false, 104 | bounds: "street", 105 | constraints: $settlement 106 | }); 107 | 108 | // дом 109 | $house.suggestions({ 110 | serviceUrl: serviceUrl, 111 | token: token, 112 | type: type, 113 | hint: false, 114 | bounds: "house-flat", 115 | constraints: $street 116 | }); 117 | }); 118 | })(jQuery); 119 | -------------------------------------------------------------------------------- /examples/granular/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Гранулярные подсказки по адресу (все поля) 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 |

21 |
22 | 23 |
24 |

Гранулярные подсказки по адресу (все поля)

25 | 26 | 27 |

28 | 29 | 30 | 31 |

32 | 33 | 34 | 35 |

36 | 37 | 38 | 39 |

40 | 41 | 42 | 43 |

44 | 45 | 46 | 47 |

48 | 49 | 50 | 51 |

52 | 53 | 54 | 55 |

56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/granular/styles.css: -------------------------------------------------------------------------------- 1 | .info p { 2 | background: #31b0d5; 3 | margin: 0; 4 | padding: 5px; 5 | } 6 | 7 | .container { 8 | width: 30em; 9 | } 10 | 11 | input { 12 | font-size: 16px; 13 | padding: 4px; 14 | } 15 | -------------------------------------------------------------------------------- /examples/test/code.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | $(function() { 3 | Token.init(); 4 | 5 | var token = Token.get(), 6 | type = "ADDRESS", 7 | $suggestions = $("#suggestions"), 8 | $name = $("#name"), 9 | $email = $("#email"), 10 | $party = $("#party"), 11 | $outward = $("#outward"), 12 | $fixDataButton = $("#fixData"), 13 | $region = $("#region"), 14 | $city = $("#city"), 15 | $street = $("#street"), 16 | $house = $("#house"); 17 | 18 | // просто подсказки 19 | var suggestionsInstance = $suggestions.suggestions({ 20 | token: token, 21 | type: type, 22 | hint: false, 23 | addon: "clear", 24 | noSuggestionsHint: false, 25 | onInvalidateSelection: function() { 26 | console.log("ON INVALIDATE SELECTION"); 27 | } 28 | }); 29 | 30 | $fixDataButton.on("click", function() { 31 | $suggestions.suggestions().fixData(); 32 | }); 33 | 34 | $name.suggestions({ 35 | token: token, 36 | type: "NAME" 37 | }); 38 | 39 | $email.suggestions({ 40 | token: token, 41 | type: "EMAIL" 42 | }); 43 | 44 | $outward.suggestions({ 45 | token: token, 46 | type: "FNS_UNIT" 47 | }); 48 | 49 | // регион 50 | $region.suggestions({ 51 | token: token, 52 | type: type, 53 | hint: false, 54 | bounds: "region-area" 55 | }); 56 | 57 | // город и населенный пункт 58 | $city.suggestions({ 59 | token: token, 60 | type: type, 61 | hint: false, 62 | bounds: "city-settlement", 63 | constraints: $region 64 | }); 65 | 66 | // улица 67 | $street.suggestions({ 68 | token: token, 69 | type: type, 70 | hint: false, 71 | bounds: "street", 72 | constraints: $city 73 | }); 74 | 75 | // дом 76 | $house.suggestions({ 77 | token: token, 78 | type: type, 79 | hint: false, 80 | bounds: "house", 81 | noSuggestionsHint: false, 82 | constraints: $street 83 | }); 84 | 85 | $("#url").suggestions({ 86 | token: token, 87 | url: 88 | "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address1", 89 | type: type, 90 | hint: false, 91 | bounds: "city" 92 | }); 93 | 94 | $("#city-729").suggestions({ 95 | token: token, 96 | type: type, 97 | hint: false, 98 | bounds: "city" 99 | }); 100 | 101 | // sug-798 102 | var $sug798 = $("#sug-798"); 103 | $sug798.suggestions({ 104 | type: "ADDRESS" 105 | }); 106 | $sug798.suggestions().setSuggestion({ 107 | value: $sug798.val(), 108 | data: {} 109 | }); 110 | 111 | $party.suggestions({ 112 | token: token, 113 | type: "PARTY", 114 | onSelect: function(suggestion) { 115 | console.log(suggestion); 116 | } 117 | }); 118 | }); 119 | })(); 120 | -------------------------------------------------------------------------------- /examples/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Примеры 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 20 |

21 |
22 | 23 |
24 |

Просто подсказки

25 | 27 |

28 | 29 |

30 |
31 | 32 |
33 |

ФИО

34 | 35 |
36 | 37 |
38 |

Email

39 | 40 |
41 | 42 |
43 |

Организация

44 | 45 |
46 | 47 |
48 |

Подключаемый справочник

49 | 50 |
51 | 52 |
53 |

SUG-706 (Гранулярные подсказки по адресу (город и улица))

54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 | 70 |
71 |

Новый параметр url

72 | 73 | 74 |
75 | 76 | 81 | 82 |
83 |
84 |

SUG-729 Совместимость с jQuery 3.0.0-3.1.1

85 | 86 | 87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/test/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | .info p { 6 | background: #31b0d5; 7 | margin: 0; 8 | padding: 5px; 9 | } 10 | 11 | .container { 12 | max-width: 30em; 13 | } 14 | 15 | input { 16 | font-size: 16px; 17 | padding: 4px; 18 | } 19 | 20 | .spacing { 21 | height: 100vh; 22 | } 23 | -------------------------------------------------------------------------------- /examples/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Класс для работы с токеном (ключ API для dadata.ru) 3 | */ 4 | (function(root, factory) { 5 | if (typeof define === "function" && define.amd) { 6 | // AMD. Register as an anonymous module. 7 | define(["exports", "jquery"], factory); 8 | } else if ( 9 | typeof exports === "object" && 10 | typeof exports.nodeName !== "string" 11 | ) { 12 | // CommonJS 13 | factory(exports, require("jquery")); 14 | } else { 15 | // Browser globals 16 | factory((root.Token = {}), root.$); 17 | } 18 | })(this, function(exports, $) { 19 | exports.init = function() { 20 | var that = this, 21 | $token = $("#token"); 22 | 23 | $token.val(this.get()); 24 | 25 | $token.on("input", function() { 26 | var token = $token.val(); 27 | location.hash = token; 28 | if (that.localStorageAvailable()) { 29 | localStorage.setItem("dadata_token", token); 30 | } 31 | if (token) { 32 | location.reload(); 33 | } 34 | }); 35 | }; 36 | 37 | exports.get = function() { 38 | var token = location.hash.replace(/^#(.*)$/, "$1"); 39 | 40 | if (!token && this.localStorageAvailable()) { 41 | token = localStorage.getItem("dadata_token") || ""; 42 | } 43 | 44 | return token; 45 | }; 46 | 47 | exports.localStorageAvailable = function() { 48 | try { 49 | localStorage.setItem("test", "test"); 50 | localStorage.removeItem("test"); 51 | return true; 52 | } catch (e) { 53 | return false; 54 | } 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var pkg = require("./package.json"), 2 | gulp = require("gulp"), 3 | rollup = require("gulp-rollup"), 4 | rename = require("gulp-rename"), 5 | banner = require("gulp-banner"), 6 | ending = require("gulp-line-ending-corrector"), 7 | uglify = require("gulp-uglify"), 8 | replace = require("gulp-replace"), 9 | less = require("gulp-less"), 10 | gulpif = require("gulp-if"), 11 | karma = require("karma").Server, 12 | cleanCss = require("gulp-clean-css"), 13 | SRC_DIR = "./src/", 14 | LESS_SRC_DIR = "./less/", 15 | DIST_DIR = "./dist/", 16 | TEST_DIR = "/test/", 17 | devMode = false, 18 | comment = [ 19 | "/**", 20 | " <%= pkg.description %>, version <%= pkg.version %>", 21 | "", 22 | " <%= pkg.description %> is freely distributable under the terms of MIT-style license", 23 | " Built on DevBridge Autocomplete for jQuery (https://github.com/devbridge/jQuery-Autocomplete)", 24 | " For details, see <%= pkg.homepage %>", 25 | "/\n" 26 | ].join("\n *"); 27 | 28 | function buildScript() { 29 | return gulp 30 | .src(SRC_DIR + "**/*.js") 31 | .pipe(gulpif(!devMode, ending({ eolc: "LF" }))) 32 | .pipe(gulpif(!devMode, gulp.dest(SRC_DIR))) 33 | .pipe( 34 | rollup({ 35 | input: SRC_DIR + "main.js", 36 | output: { 37 | format: "umd", 38 | globals: { 39 | jquery: "jQuery" 40 | } 41 | }, 42 | external: ["jquery"] 43 | }) 44 | ) 45 | .pipe(rename("jquery.suggestions.js")) 46 | .pipe(banner(comment, { pkg: pkg })) 47 | .pipe(replace("%VERSION%", pkg.version)) 48 | .pipe(ending({ eolc: "LF" })) 49 | .pipe(gulp.dest(DIST_DIR + "js")) 50 | .pipe(uglify({ mangle: true })) 51 | .pipe(rename("jquery.suggestions.min.js")) 52 | .pipe(gulp.dest(DIST_DIR + "js")); 53 | } 54 | 55 | function buildStyle() { 56 | return gulp 57 | .src(LESS_SRC_DIR + "**/*") 58 | .pipe(gulpif(!devMode, ending({ eolc: "LF" }))) 59 | .pipe(gulpif(!devMode, gulp.dest(LESS_SRC_DIR))) 60 | .pipe(less({ javascriptEnabled: true })) 61 | .pipe(gulp.dest(DIST_DIR + "css")) 62 | .pipe(cleanCss({ compatibility: "ie8" })) 63 | .pipe(rename("suggestions.min.css")) 64 | .pipe(gulp.dest(DIST_DIR + "css")); 65 | } 66 | 67 | function testPrepare() { 68 | return gulp 69 | .src(TEST_DIR + "specs/*.js") 70 | .pipe(ending()) 71 | .pipe(gulp.dest("test/specs")); 72 | } 73 | 74 | function testFull(callback) { 75 | new karma( 76 | { 77 | configFile: __dirname + TEST_DIR + "karma.full.js", 78 | singleRun: true 79 | }, 80 | callback 81 | ).start(); 82 | } 83 | 84 | function testMinified(callback) { 85 | new karma( 86 | { 87 | configFile: __dirname + TEST_DIR + "karma.minified.js", 88 | singleRun: true 89 | }, 90 | callback 91 | ).start(); 92 | } 93 | 94 | function setDevMode(callback) { 95 | devMode = true; 96 | callback(); 97 | } 98 | 99 | gulp.task("watch", function() { 100 | gulp.watch([SRC_DIR + "**/*"], buildScript); 101 | gulp.watch([LESS_SRC_DIR + "**/*"], buildStyle); 102 | }); 103 | 104 | exports.build = gulp.series(buildScript, buildStyle); 105 | exports.test = gulp.series(testPrepare, testFull); 106 | exports.testall = gulp.series(exports.test, testMinified); 107 | exports.dev = gulp.series(setDevMode, exports.build, "watch"); 108 | exports.default = gulp.series(exports.build, exports.testall); 109 | -------------------------------------------------------------------------------- /less/suggestions.less: -------------------------------------------------------------------------------- 1 | @subtext-color: #777; 2 | @suggestions-bg-color: #fff; 3 | @subtext-label-color: #f5f5f5; 4 | 5 | .box-sizing(@sizing: border-box) { 6 | -ms-box-sizing: @sizing; 7 | -moz-box-sizing: @sizing; 8 | -webkit-box-sizing: @sizing; 9 | box-sizing: @sizing; 10 | } 11 | 12 | .rounded(@radius) { 13 | -webkit-border-radius: @radius; 14 | -moz-border-radius: @radius; 15 | border-radius: @radius; 16 | } 17 | 18 | .suggestions-nowrap { 19 | white-space: nowrap; 20 | } 21 | 22 | /** 23 | * Основной INPUT 24 | */ 25 | .suggestions-input { 26 | // IE9 can't determine border-style until it is specified in stylesheets 27 | // border: 1px solid grey; 28 | .box-sizing(); 29 | width: 100%; 30 | 31 | &::-ms-clear { 32 | display: none; 33 | } 34 | } 35 | 36 | .suggestions-wrapper { 37 | position: relative; 38 | margin: 0; 39 | padding: 0; 40 | vertical-align: top; 41 | 42 | //Prevent font scaling in landscape while allowing user zoom 43 | -webkit-text-size-adjust: 100%; 44 | } 45 | 46 | /** 47 | * Выпадающий блок с найденными подсказками 48 | */ 49 | .suggestions-suggestions { 50 | background: @suggestions-bg-color; 51 | border: 1px solid #999; 52 | .box-sizing; 53 | cursor: default; 54 | left: 0; 55 | min-width: 100%; 56 | position: absolute; 57 | z-index: 9999; 58 | 59 | //Prevent font scaling in landscape while allowing user zoom 60 | -webkit-text-size-adjust: 100%; 61 | 62 | strong { 63 | font-weight: normal; 64 | color: #3399ff; 65 | } 66 | 67 | &.suggestions-mobile { 68 | border-style: none; 69 | 70 | .suggestions-suggestion { 71 | border-bottom: 1px solid #ddd; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Контейнер для одной подсказки 78 | */ 79 | .suggestions-suggestion { 80 | padding: 4px 4px; 81 | overflow: hidden; 82 | 83 | &:hover { 84 | background: darken(@suggestions-bg-color, 3%); 85 | } 86 | } 87 | 88 | /** 89 | * Выбранная (активная) подсказка 90 | */ 91 | .suggestions-selected { 92 | background: darken(@suggestions-bg-color, 6%); 93 | 94 | &:hover { 95 | background: darken(@suggestions-bg-color, 6%); 96 | } 97 | } 98 | 99 | /** 100 | * Информационный блок в верхней части выпадашки с подсказками 101 | */ 102 | .suggestions-hint { 103 | padding: 4px 4px; 104 | white-space: nowrap; 105 | overflow: hidden; 106 | color: @subtext-color; 107 | font-size: 85%; 108 | line-height: 20px; 109 | } 110 | 111 | /** 112 | * Дополнительный текст в подсказке, который идет второй строкой 113 | */ 114 | .suggestions-subtext { 115 | color: @subtext-color; 116 | } 117 | 118 | /** 119 | * Размещает дополнительный текст в одну строку с основным текстом подсказки 120 | */ 121 | .suggestions-subtext_inline { 122 | display: inline-block; 123 | min-width: 6em; 124 | vertical-align: bottom; 125 | margin: 0 0.5em 0 0; 126 | } 127 | 128 | /** 129 | * Разделитель нескольких дополнительных текстов 130 | */ 131 | .suggestions-subtext-delimiter { 132 | display: inline-block; 133 | width: 2px; 134 | } 135 | 136 | /** 137 | * Выделяет подсказку 138 | */ 139 | .suggestions-subtext_label { 140 | margin: 0 0 0 0.25em; 141 | .rounded(3px); 142 | padding: 0 3px; 143 | 144 | background: @subtext-label-color; 145 | font-size: 85%; 146 | } 147 | 148 | .suggestions-value { 149 | &[data-suggestion-status="LIQUIDATED"] { 150 | position: relative; 151 | 152 | &:after { 153 | position: absolute; 154 | left: 0; 155 | right: 0; 156 | top: 50%; 157 | 158 | border-top: 1px solid rgba(0, 0, 0, 0.4); 159 | 160 | content: ""; 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Промо-блок 167 | */ 168 | .suggestions-promo { 169 | font-size: 85%; 170 | display: none; 171 | color: @subtext-color; 172 | padding: 4px; 173 | text-align: center; 174 | a { 175 | color: @subtext-color; 176 | display: block; 177 | filter: grayscale(100%); 178 | line-height: 20px; 179 | text-decoration: none; 180 | } 181 | a:hover { 182 | filter: grayscale(0); 183 | } 184 | svg { 185 | height: 20px; 186 | vertical-align: bottom; 187 | } 188 | } 189 | 190 | @media screen and (min-width: 600px) { 191 | .suggestions-promo { 192 | position: absolute; 193 | top: 0; 194 | right: 0; 195 | text-align: left; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suggestions-jquery", 3 | "version": "22.6.0", 4 | "description": "DaData.ru Suggestions jQuery plugin", 5 | "homepage": "https://github.com/hflabs/suggestions-jquery", 6 | "author": { 7 | "name": "HFLabs", 8 | "url": "https://hflabs.ru/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/hflabs/suggestions-jquery.git" 13 | }, 14 | "license": "MIT", 15 | "peerDependencies": { 16 | "jquery": ">= 1.9" 17 | }, 18 | "devDependencies": { 19 | "@metahub/karma-jasmine-jquery": "^2.0.1", 20 | "gulp": "^4.0.0", 21 | "gulp-banner": "^0.1.3", 22 | "gulp-clean-css": "^3.0.2", 23 | "gulp-if": "^2.0.2", 24 | "gulp-less": "^4.0.1", 25 | "gulp-line-ending-corrector": "^1.0.1", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-replace": "^0.5.4", 28 | "gulp-rollup": "^2.11.0", 29 | "gulp-uglify": "^2.0.0", 30 | "jasmine": "^2.5.3", 31 | "jasmine-core": "^3.3.0", 32 | "jasmine-jquery": "^2.1.1", 33 | "jasmine-sinon": "^0.4.0", 34 | "karma": "^4.0.1", 35 | "karma-chrome-launcher": "^2.2.0", 36 | "karma-jasmine": "^2.0.1", 37 | "karma-jasmine-html-reporter": "^1.4.0", 38 | "karma-jasmine-sinon": "^1.0.4", 39 | "karma-jquery": "^0.2.3", 40 | "karma-phantomjs-launcher": "^1.0.4", 41 | "karma-spec-reporter": "0.0.32", 42 | "prettier": "1.18.2", 43 | "sinon": "^4.1.5" 44 | }, 45 | "main": "dist/js/jquery.suggestions.js", 46 | "scripts": { 47 | "test": "gulp test" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/includes/ajax.js: -------------------------------------------------------------------------------- 1 | import { object_util } from "./utils/object"; 2 | import { jqapi } from "./jqapi"; 3 | 4 | /** 5 | * Утилиты для работы через AJAX 6 | */ 7 | var ajax = { 8 | /** 9 | * HTTP-метод, который поддерживает браузер 10 | */ 11 | getDefaultType: function() { 12 | return jqapi.supportsCors() ? "POST" : "GET"; 13 | }, 14 | 15 | /** 16 | * Content-type, который поддерживает браузер 17 | */ 18 | getDefaultContentType: function() { 19 | return jqapi.supportsCors() 20 | ? "application/json" 21 | : "application/x-www-form-urlencoded"; 22 | }, 23 | 24 | /** 25 | * Меняет HTTPS на протокол страницы, если браузер не поддерживает CORS 26 | */ 27 | fixURLProtocol: function(url) { 28 | return jqapi.supportsCors() 29 | ? url 30 | : url.replace(/^https?:/, location.protocol); 31 | }, 32 | 33 | /** 34 | * Записывает параметры в GET-строку 35 | */ 36 | addUrlParams: function(url, params) { 37 | return url + (/\?/.test(url) ? "&" : "?") + jqapi.param(params); 38 | }, 39 | 40 | /** 41 | * Сериализует объект для передачи по сети. 42 | * Либо в JSON-строку (если браузер поддерживает CORS), 43 | * либо в GET-строку. 44 | */ 45 | serialize: function(data) { 46 | if (jqapi.supportsCors()) { 47 | return JSON.stringify(data, function(key, value) { 48 | return value === null ? undefined : value; 49 | }); 50 | } else { 51 | data = object_util.compact(data); 52 | return jqapi.param(data, true); 53 | } 54 | } 55 | }; 56 | 57 | export { ajax }; 58 | -------------------------------------------------------------------------------- /src/includes/bounds.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | import { Suggestions } from "./suggestions"; 4 | import { DEFAULT_OPTIONS } from "./default-options"; 5 | import { notificator } from "./notificator"; 6 | 7 | /** 8 | * features for connected instances 9 | */ 10 | 11 | var optionsUsed = { 12 | bounds: null, 13 | }; 14 | 15 | var methods = { 16 | setupBounds: function() { 17 | this.bounds = { 18 | from: null, 19 | to: null, 20 | }; 21 | }, 22 | 23 | setBoundsOptions: function() { 24 | var that = this, 25 | boundsAvailable = [], 26 | newBounds = $.trim(that.options.bounds).split("-"), 27 | boundFrom = newBounds[0], 28 | boundTo = newBounds[newBounds.length - 1], 29 | boundsOwn = [], 30 | boundIsOwn, 31 | boundsAll = []; 32 | 33 | if (that.type.dataComponents) { 34 | $.each(that.type.dataComponents, function() { 35 | if (this.forBounds) { 36 | boundsAvailable.push(this.id); 37 | } 38 | }); 39 | } 40 | 41 | if (boundsAvailable.indexOf(boundFrom) === -1) { 42 | boundFrom = null; 43 | } 44 | 45 | if (boundsAvailable.indexOf(boundTo) === -1) { 46 | boundTo = null; 47 | } 48 | 49 | if (boundFrom || boundTo) { 50 | boundIsOwn = !boundFrom; 51 | $.each(boundsAvailable, function(i, bound) { 52 | if (bound == boundFrom) { 53 | boundIsOwn = true; 54 | } 55 | boundsAll.push(bound); 56 | if (boundIsOwn) { 57 | boundsOwn.push(bound); 58 | } 59 | if (bound == boundTo) { 60 | return false; 61 | } 62 | }); 63 | } 64 | 65 | that.bounds.from = boundFrom; 66 | that.bounds.to = boundTo; 67 | that.bounds.all = boundsAll; 68 | that.bounds.own = boundsOwn; 69 | }, 70 | 71 | constructBoundsParams: function() { 72 | var that = this, 73 | params = {}; 74 | 75 | if (that.bounds.from) { 76 | params["from_bound"] = { value: that.bounds.from }; 77 | } 78 | if (that.bounds.to) { 79 | params["to_bound"] = { value: that.bounds.to }; 80 | } 81 | 82 | return params; 83 | }, 84 | 85 | /** 86 | * Подстраивает suggestion.value под that.bounds.own 87 | * Ничего не возвращает, меняет в самом suggestion 88 | * @param suggestion 89 | */ 90 | checkValueBounds: function(suggestion) { 91 | var that = this, 92 | valueData; 93 | 94 | // If any bounds set up 95 | if (that.bounds.own.length && that.type.composeValue) { 96 | // делаем копию 97 | var bounds = that.bounds.own.slice(0); 98 | // если роль текущего инстанса плагина показывать только район города 99 | // то для корректного формировния нужен city_district_fias_id 100 | if (bounds.length === 1 && bounds[0] === "city_district") { 101 | bounds.push("city_district_fias_id"); 102 | } 103 | valueData = that.copyDataComponents(suggestion.data, bounds); 104 | suggestion.value = that.type.composeValue(valueData); 105 | } 106 | }, 107 | 108 | copyDataComponents: function(data, components) { 109 | var result = {}, 110 | dataComponentsById = this.type.dataComponentsById; 111 | 112 | if (dataComponentsById) { 113 | $.each(components, function(i, component) { 114 | $.each(dataComponentsById[component].fields, function( 115 | i, 116 | field 117 | ) { 118 | if (data[field] != null) { 119 | result[field] = data[field]; 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | return result; 126 | }, 127 | 128 | getBoundedKladrId: function(kladr_id, boundsRange) { 129 | var boundTo = boundsRange[boundsRange.length - 1], 130 | kladrFormat; 131 | 132 | $.each(this.type.dataComponents, function(i, component) { 133 | if (component.id === boundTo) { 134 | kladrFormat = component.kladrFormat; 135 | return false; 136 | } 137 | }); 138 | 139 | return ( 140 | kladr_id.substr(0, kladrFormat.digits) + 141 | new Array((kladrFormat.zeros || 0) + 1).join("0") 142 | ); 143 | }, 144 | }; 145 | 146 | $.extend(DEFAULT_OPTIONS, optionsUsed); 147 | 148 | $.extend(Suggestions.prototype, methods); 149 | 150 | notificator 151 | .on("initialize", methods.setupBounds) 152 | .on("setOptions", methods.setBoundsOptions) 153 | .on("requestParams", methods.constructBoundsParams); 154 | -------------------------------------------------------------------------------- /src/includes/constants.js: -------------------------------------------------------------------------------- 1 | var KEYS = { 2 | ENTER: 13, 3 | ESC: 27, 4 | TAB: 9, 5 | SPACE: 32, 6 | UP: 38, 7 | DOWN: 40 8 | }; 9 | 10 | var CLASSES = { 11 | hint: "suggestions-hint", 12 | mobile: "suggestions-mobile", 13 | nowrap: "suggestions-nowrap", 14 | promo: "suggestions-promo", 15 | selected: "suggestions-selected", 16 | suggestion: "suggestions-suggestion", 17 | subtext: "suggestions-subtext", 18 | subtext_inline: "suggestions-subtext suggestions-subtext_inline", 19 | subtext_delimiter: "suggestions-subtext-delimiter", 20 | subtext_label: "suggestions-subtext suggestions-subtext_label", 21 | removeConstraint: "suggestions-remove", 22 | value: "suggestions-value" 23 | }; 24 | 25 | var EVENT_NS = ".suggestions"; 26 | var DATA_ATTR_KEY = "suggestions"; 27 | var WORD_DELIMITERS = "\\s\"'~\\*\\.,:\\|\\[\\]\\(\\)\\{\\}<>№"; 28 | var WORD_PARTS_DELIMITERS = "\\-\\+\\\\\\?!@#$%^&"; 29 | 30 | export { 31 | KEYS, 32 | CLASSES, 33 | EVENT_NS, 34 | DATA_ATTR_KEY, 35 | WORD_DELIMITERS, 36 | WORD_PARTS_DELIMITERS 37 | }; 38 | -------------------------------------------------------------------------------- /src/includes/default-options.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | var DEFAULT_OPTIONS = { 4 | $helpers: null, 5 | autoSelectFirst: false, 6 | containerClass: "suggestions-suggestions", 7 | count: 5, 8 | deferRequestBy: 100, 9 | enrichmentEnabled: true, 10 | formatResult: null, 11 | formatSelected: null, 12 | headers: null, 13 | hint: "Выберите вариант или продолжите ввод", 14 | initializeInterval: 100, 15 | language: null, 16 | minChars: 1, 17 | mobileWidth: 600, 18 | noCache: false, 19 | noSuggestionsHint: null, 20 | onInvalidateSelection: null, 21 | onSearchComplete: $.noop, 22 | onSearchError: $.noop, 23 | onSearchStart: $.noop, 24 | onSelect: null, 25 | onSelectNothing: null, 26 | onSuggestionsFetch: null, 27 | paramName: "query", 28 | params: {}, 29 | preventBadQueries: false, 30 | requestMode: "suggest", 31 | scrollOnFocus: false, 32 | // основной url, может быть переопределен 33 | serviceUrl: "https://suggestions.dadata.ru/suggestions/api/4_1/rs", 34 | tabDisabled: false, 35 | timeout: 3000, 36 | triggerSelectOnBlur: true, 37 | triggerSelectOnEnter: true, 38 | triggerSelectOnSpace: false, 39 | type: null, 40 | // url, который заменяет serviceUrl + method + type 41 | // то есть, если он задан, то для всех запросов будет использоваться именно он 42 | // если не поддерживается cors то к url будут добавлены параметры ?token=...&version=... 43 | // и заменен протокол на протокол текущей страницы 44 | url: null 45 | }; 46 | 47 | export { DEFAULT_OPTIONS }; 48 | -------------------------------------------------------------------------------- /src/includes/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Утилиты для работы с DOM. 3 | */ 4 | var dom = { 5 | /** 6 | * Выбрать первый элемент с указанным классом. 7 | */ 8 | selectByClass: function(classname, parent) { 9 | var selector = "." + classname; 10 | if (parent) { 11 | return parent.querySelector(selector); 12 | } else { 13 | return document.querySelector(selector); 14 | } 15 | }, 16 | 17 | /** 18 | * Добавить элементу класс. 19 | */ 20 | addClass: function(element, className) { 21 | var list = element.className.split(" "); 22 | if (list.indexOf(className) === -1) { 23 | list.push(className); 24 | } 25 | element.className = list.join(" "); 26 | }, 27 | 28 | /** 29 | * Добавить элементу стиль. 30 | */ 31 | setStyle: function(element, name, value) { 32 | element.style[name] = value; 33 | }, 34 | 35 | /** 36 | * Подписаться на событие на элементе. 37 | * @param {Element} element - элемент 38 | * @param {string} eventName - название события 39 | * @param {string} namespace - пространство имён события 40 | * @param {Function} callback - функция-обработчик события 41 | */ 42 | listenTo: function(element, eventName, namespace, callback) { 43 | element.addEventListener(eventName, callback, false); 44 | if (namespace) { 45 | if (!eventsByNamespace[namespace]) { 46 | eventsByNamespace[namespace] = []; 47 | } 48 | eventsByNamespace[namespace].push({ 49 | eventName: eventName, 50 | element: element, 51 | callback: callback 52 | }); 53 | } 54 | }, 55 | 56 | /** 57 | * Отписаться от всех событий с указанным пространством имён. 58 | */ 59 | stopListeningNamespace: function(namespace) { 60 | var events = eventsByNamespace[namespace]; 61 | if (events) { 62 | events.forEach(function(event) { 63 | event.element.removeEventListener( 64 | event.eventName, 65 | event.callback, 66 | false 67 | ); 68 | }); 69 | } 70 | } 71 | }; 72 | 73 | export { dom }; 74 | -------------------------------------------------------------------------------- /src/includes/element.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | import { utils } from "./utils"; 4 | import { notificator } from "./notificator"; 5 | import { Suggestions } from "./suggestions"; 6 | 7 | import { KEYS, EVENT_NS } from "./constants"; 8 | 9 | /** 10 | * Methods related to INPUT's behavior 11 | */ 12 | 13 | var methods = { 14 | setupElement: function() { 15 | // Remove autocomplete attribute to prevent native suggestions: 16 | this.el 17 | // if it stops working, see https://stackoverflow.com/q/15738259 18 | // chrome is constantly changing this logic 19 | .attr("autocomplete", "new-password") 20 | .attr("autocorrect", "off") 21 | .attr("autocapitalize", "off") 22 | .attr("spellcheck", "false") 23 | .addClass("suggestions-input") 24 | .css("box-sizing", "border-box"); 25 | }, 26 | 27 | bindElementEvents: function() { 28 | var that = this; 29 | 30 | that.el.on("keydown" + EVENT_NS, $.proxy(that.onElementKeyDown, that)); 31 | // IE is buggy, it doesn't trigger `input` on text deletion, so use following events 32 | that.el.on( 33 | [ 34 | "keyup" + EVENT_NS, 35 | "cut" + EVENT_NS, 36 | "paste" + EVENT_NS, 37 | "input" + EVENT_NS 38 | ].join(" "), 39 | $.proxy(that.onElementKeyUp, that) 40 | ); 41 | that.el.on("blur" + EVENT_NS, $.proxy(that.onElementBlur, that)); 42 | that.el.on("focus" + EVENT_NS, $.proxy(that.onElementFocus, that)); 43 | }, 44 | 45 | unbindElementEvents: function() { 46 | this.el.off(EVENT_NS); 47 | }, 48 | 49 | onElementBlur: function() { 50 | var that = this; 51 | 52 | // suggestion was clicked, blur should be ignored 53 | // see container mousedown handler 54 | if (that.cancelBlur) { 55 | that.cancelBlur = false; 56 | return; 57 | } 58 | 59 | if (that.options.triggerSelectOnBlur) { 60 | if (!that.isUnavailable()) { 61 | that.selectCurrentValue({ noSpace: true }).always(function() { 62 | // For NAMEs selecting keeps suggestions list visible, so hide it 63 | that.hide(); 64 | }); 65 | } 66 | } else { 67 | that.hide(); 68 | } 69 | 70 | if (that.fetchPhase.abort) { 71 | that.fetchPhase.abort(); 72 | } 73 | }, 74 | 75 | onElementFocus: function() { 76 | var that = this; 77 | 78 | if (!that.cancelFocus) { 79 | // defer methods to allow browser update input's style before 80 | utils.delay($.proxy(that.completeOnFocus, that)); 81 | } 82 | that.cancelFocus = false; 83 | }, 84 | 85 | onElementKeyDown: function(e) { 86 | var that = this; 87 | 88 | if (that.isUnavailable()) { 89 | return; 90 | } 91 | 92 | if (!that.visible) { 93 | switch (e.which) { 94 | // If suggestions are hidden and user presses arrow down, display suggestions 95 | case KEYS.DOWN: 96 | that.suggest(); 97 | break; 98 | // if no suggestions available and user pressed Enter 99 | case KEYS.ENTER: 100 | if (that.options.triggerSelectOnEnter) { 101 | that.triggerOnSelectNothing(); 102 | } 103 | break; 104 | } 105 | return; 106 | } 107 | 108 | switch (e.which) { 109 | case KEYS.ESC: 110 | that.el.val(that.currentValue); 111 | that.hide(); 112 | that.abortRequest(); 113 | break; 114 | 115 | case KEYS.TAB: 116 | if (that.options.tabDisabled === false) { 117 | return; 118 | } 119 | break; 120 | 121 | case KEYS.ENTER: 122 | if (that.options.triggerSelectOnEnter) { 123 | that.selectCurrentValue(); 124 | } 125 | break; 126 | 127 | case KEYS.SPACE: 128 | if (that.options.triggerSelectOnSpace && that.isCursorAtEnd()) { 129 | e.preventDefault(); 130 | that.selectCurrentValue({ 131 | continueSelecting: true, 132 | dontEnrich: true 133 | }).fail(function() { 134 | // If all data fetched but nothing selected 135 | that.currentValue += " "; 136 | that.el.val(that.currentValue); 137 | that.proceedChangedValue(); 138 | }); 139 | } 140 | return; 141 | case KEYS.UP: 142 | that.moveUp(); 143 | break; 144 | case KEYS.DOWN: 145 | that.moveDown(); 146 | break; 147 | default: 148 | return; 149 | } 150 | 151 | // Cancel event if function did not return: 152 | e.stopImmediatePropagation(); 153 | e.preventDefault(); 154 | }, 155 | 156 | onElementKeyUp: function(e) { 157 | var that = this; 158 | 159 | if (that.isUnavailable()) { 160 | return; 161 | } 162 | 163 | switch (e.which) { 164 | case KEYS.UP: 165 | case KEYS.DOWN: 166 | case KEYS.ENTER: 167 | return; 168 | } 169 | 170 | // Cancel pending change 171 | clearTimeout(that.onChangeTimeout); 172 | that.inputPhase.reject(); 173 | 174 | if (that.currentValue !== that.el.val()) { 175 | that.proceedChangedValue(); 176 | } 177 | }, 178 | 179 | proceedChangedValue: function() { 180 | var that = this; 181 | 182 | // Cancel fetching, because it became obsolete 183 | that.abortRequest(); 184 | 185 | that.inputPhase = $.Deferred().done($.proxy(that.onValueChange, that)); 186 | 187 | if (that.options.deferRequestBy > 0) { 188 | // Defer lookup in case when value changes very quickly: 189 | that.onChangeTimeout = utils.delay(function() { 190 | that.inputPhase.resolve(); 191 | }, that.options.deferRequestBy); 192 | } else { 193 | that.inputPhase.resolve(); 194 | } 195 | }, 196 | 197 | onValueChange: function() { 198 | var that = this, 199 | currentSelection; 200 | 201 | if (that.selection) { 202 | currentSelection = that.selection; 203 | that.selection = null; 204 | that.trigger("InvalidateSelection", currentSelection); 205 | } 206 | 207 | that.selectedIndex = -1; 208 | 209 | that.update(); 210 | that.notify("valueChange"); 211 | }, 212 | 213 | completeOnFocus: function() { 214 | var that = this; 215 | 216 | if (that.isUnavailable()) { 217 | return; 218 | } 219 | 220 | if (that.isElementFocused()) { 221 | that.update(); 222 | if (that.isMobile) { 223 | that.setCursorAtEnd(); 224 | that.scrollToTop(); 225 | } 226 | } 227 | }, 228 | 229 | isElementFocused: function() { 230 | return document.activeElement === this.element; 231 | }, 232 | 233 | isElementDisabled: function() { 234 | return Boolean( 235 | this.element.getAttribute("disabled") || 236 | this.element.getAttribute("readonly") 237 | ); 238 | }, 239 | 240 | isCursorAtEnd: function() { 241 | var that = this, 242 | valLength = that.el.val().length, 243 | selectionStart, 244 | range; 245 | 246 | // `selectionStart` and `selectionEnd` are not supported by some input types 247 | try { 248 | selectionStart = that.element.selectionStart; 249 | if (typeof selectionStart === "number") { 250 | return selectionStart === valLength; 251 | } 252 | } catch (ex) {} 253 | 254 | if (document.selection) { 255 | range = document.selection.createRange(); 256 | range.moveStart("character", -valLength); 257 | return valLength === range.text.length; 258 | } 259 | return true; 260 | }, 261 | 262 | setCursorAtEnd: function() { 263 | var element = this.element; 264 | 265 | // `selectionStart` and `selectionEnd` are not supported by some input types 266 | try { 267 | element.selectionEnd = element.selectionStart = 268 | element.value.length; 269 | element.scrollLeft = element.scrollWidth; 270 | } catch (ex) { 271 | element.value = element.value; 272 | } 273 | } 274 | }; 275 | 276 | $.extend(Suggestions.prototype, methods); 277 | 278 | notificator 279 | .on("initialize", methods.bindElementEvents) 280 | .on("dispose", methods.unbindElementEvents); 281 | -------------------------------------------------------------------------------- /src/includes/enrich.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | import { Suggestions } from "./suggestions"; 4 | 5 | var methods = { 6 | enrichSuggestion: function(suggestion, selectionOptions) { 7 | var that = this, 8 | resolver = $.Deferred(); 9 | 10 | if ( 11 | !that.options.enrichmentEnabled || 12 | !that.type.enrichmentEnabled || 13 | !that.requestMode.enrichmentEnabled || 14 | (selectionOptions && selectionOptions.dontEnrich) 15 | ) { 16 | return resolver.resolve(suggestion); 17 | } 18 | 19 | // if current suggestion is already enriched, use it 20 | if (suggestion.data && suggestion.data.qc != null) { 21 | return resolver.resolve(suggestion); 22 | } 23 | 24 | that.disableDropdown(); 25 | 26 | var query = that.type.getEnrichmentQuery(suggestion); 27 | var customParams = that.type.enrichmentParams; 28 | var requestOptions = { 29 | noCallbacks: true, 30 | useEnrichmentCache: true, 31 | method: that.type.enrichmentMethod 32 | }; 33 | 34 | // Set `currentValue` to make `processResponse` to consider enrichment response valid 35 | that.currentValue = query; 36 | 37 | // prevent request abortion during onBlur 38 | that.enrichPhase = that 39 | .getSuggestions(query, customParams, requestOptions) 40 | .always(function() { 41 | that.enableDropdown(); 42 | }) 43 | .done(function(suggestions) { 44 | var enrichedSuggestion = suggestions && suggestions[0]; 45 | 46 | resolver.resolve( 47 | enrichedSuggestion || suggestion, 48 | !!enrichedSuggestion 49 | ); 50 | }) 51 | .fail(function() { 52 | resolver.resolve(suggestion); 53 | }); 54 | 55 | return resolver; 56 | }, 57 | 58 | /** 59 | * Injects enriched suggestion into response 60 | * @param response 61 | * @param query 62 | */ 63 | enrichResponse: function(response, query) { 64 | var that = this, 65 | enrichedSuggestion = that.enrichmentCache[query]; 66 | 67 | if (enrichedSuggestion) { 68 | $.each(response.suggestions, function(i, suggestion) { 69 | if (suggestion.value === query) { 70 | response.suggestions[i] = enrichedSuggestion; 71 | return false; 72 | } 73 | }); 74 | } 75 | } 76 | }; 77 | 78 | $.extend(Suggestions.prototype, methods); 79 | -------------------------------------------------------------------------------- /src/includes/geolocation.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | import { utils } from "./utils"; 4 | import { notificator } from "./notificator"; 5 | import { Suggestions } from "./suggestions"; 6 | import { DEFAULT_OPTIONS } from "./default-options"; 7 | 8 | var locationRequest, 9 | defaultGeoLocation = true; 10 | 11 | function resetLocation() { 12 | locationRequest = null; 13 | DEFAULT_OPTIONS.geoLocation = defaultGeoLocation; 14 | } 15 | 16 | var methods = { 17 | checkLocation: function() { 18 | var that = this, 19 | providedLocation = that.options.geoLocation; 20 | 21 | if (!that.type.geoEnabled || !providedLocation) { 22 | return; 23 | } 24 | 25 | that.geoLocation = $.Deferred(); 26 | if ($.isPlainObject(providedLocation) || $.isArray(providedLocation)) { 27 | that.geoLocation.resolve(providedLocation); 28 | } else { 29 | if (!locationRequest) { 30 | locationRequest = $.ajax( 31 | that.getAjaxParams("iplocate/address") 32 | ); 33 | } 34 | 35 | locationRequest 36 | .done(function(resp) { 37 | var locationData = 38 | resp && resp.location && resp.location.data; 39 | if (locationData && locationData.kladr_id) { 40 | that.geoLocation.resolve({ 41 | kladr_id: locationData.kladr_id 42 | }); 43 | } else { 44 | that.geoLocation.reject(); 45 | } 46 | }) 47 | .fail(function() { 48 | that.geoLocation.reject(); 49 | }); 50 | } 51 | }, 52 | 53 | /** 54 | * Public method to get `geoLocation` promise 55 | * @returns {$.Deferred} 56 | */ 57 | getGeoLocation: function() { 58 | return this.geoLocation; 59 | }, 60 | 61 | constructParams: function() { 62 | var that = this, 63 | params = {}; 64 | 65 | if ( 66 | that.geoLocation && 67 | $.isFunction(that.geoLocation.promise) && 68 | that.geoLocation.state() == "resolved" 69 | ) { 70 | that.geoLocation.done(function(locationData) { 71 | params["locations_boost"] = $.makeArray(locationData); 72 | }); 73 | } 74 | 75 | return params; 76 | } 77 | }; 78 | 79 | // Disable this feature when GET method used. See SUG-202 80 | if (utils.getDefaultType() != "GET") { 81 | $.extend(DEFAULT_OPTIONS, { 82 | geoLocation: defaultGeoLocation 83 | }); 84 | 85 | $.extend(Suggestions, { 86 | resetLocation: resetLocation 87 | }); 88 | 89 | $.extend(Suggestions.prototype, { 90 | getGeoLocation: methods.getGeoLocation 91 | }); 92 | 93 | notificator 94 | .on("setOptions", methods.checkLocation) 95 | .on("requestParams", methods.constructParams); 96 | } 97 | -------------------------------------------------------------------------------- /src/includes/jqapi.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | /** 4 | * jQuery API. 5 | */ 6 | var jqapi = { 7 | Deferred: function() { 8 | return $.Deferred(); 9 | }, 10 | 11 | ajax: function(settings) { 12 | return $.ajax(settings); 13 | }, 14 | 15 | extend: function() { 16 | return $.extend.apply(null, arguments); 17 | }, 18 | 19 | isJqObject: function(obj) { 20 | return obj instanceof $; 21 | }, 22 | 23 | param: function(obj) { 24 | return $.param(obj); 25 | }, 26 | 27 | proxy: function(func, context) { 28 | return $.proxy(func, context); 29 | }, 30 | 31 | select: function(selector) { 32 | return $(selector); 33 | }, 34 | 35 | supportsCors: function() { 36 | return $.support.cors; 37 | } 38 | }; 39 | 40 | export { jqapi }; 41 | -------------------------------------------------------------------------------- /src/includes/matchers.js: -------------------------------------------------------------------------------- 1 | import { collection_util } from "./utils/collection"; 2 | import { text_util } from "./utils/text"; 3 | import { object_util } from "./utils/object"; 4 | 5 | /** 6 | * Factory to create same parent checker function 7 | * @param preprocessFn called on each value before comparison 8 | * @returns {Function} same parent checker function 9 | */ 10 | function sameParentChecker(preprocessFn) { 11 | return function(suggestions) { 12 | if (suggestions.length === 0) { 13 | return false; 14 | } 15 | if (suggestions.length === 1) { 16 | return true; 17 | } 18 | 19 | var parentValue = preprocessFn(suggestions[0].value), 20 | aliens = suggestions.filter(function(suggestion) { 21 | return ( 22 | preprocessFn(suggestion.value).indexOf(parentValue) !== 0 23 | ); 24 | }); 25 | 26 | return aliens.length === 0; 27 | }; 28 | } 29 | 30 | /** 31 | * Default same parent checker. Compares raw values. 32 | * @type {Function} 33 | */ 34 | var haveSameParent = sameParentChecker(function(val) { 35 | return val; 36 | }); 37 | 38 | /** 39 | * Сравнивает запрос c подсказками, по словам. 40 | * Срабатывает, только если у всех подсказок общий родитель 41 | * (функция сверки передаётся параметром). 42 | * Игнорирует стоп-слова. 43 | * Возвращает индекс единственной подходящей подсказки 44 | * или -1, если подходящих нет или несколько. 45 | */ 46 | function _matchByWords(stopwords, parentCheckerFn) { 47 | return function(query, suggestions) { 48 | var queryTokens; 49 | var matches = []; 50 | 51 | if (parentCheckerFn(suggestions)) { 52 | queryTokens = text_util.splitTokens( 53 | text_util.split(query, stopwords) 54 | ); 55 | 56 | collection_util.each(suggestions, function(suggestion, i) { 57 | var suggestedValue = suggestion.value; 58 | 59 | if (text_util.stringEncloses(query, suggestedValue)) { 60 | return false; 61 | } 62 | 63 | // check if query words are a subset of suggested words 64 | var suggestionWords = text_util.splitTokens( 65 | text_util.split(suggestedValue, stopwords) 66 | ); 67 | 68 | if ( 69 | collection_util.minus(queryTokens, suggestionWords) 70 | .length === 0 71 | ) { 72 | matches.push(i); 73 | } 74 | }); 75 | } 76 | 77 | return matches.length === 1 ? matches[0] : -1; 78 | }; 79 | } 80 | 81 | /** 82 | * Matchers return index of suitable suggestion 83 | * Context inside is optionally set in types.js 84 | */ 85 | var matchers = { 86 | /** 87 | * Matches query against suggestions, removing all the stopwords. 88 | */ 89 | matchByNormalizedQuery: function(stopwords) { 90 | return function(query, suggestions) { 91 | var normalizedQuery = text_util.normalize(query, stopwords); 92 | var matches = []; 93 | 94 | collection_util.each(suggestions, function(suggestion, i) { 95 | var suggestedValue = suggestion.value.toLowerCase(); 96 | // if query encloses suggestion, than it has already been selected 97 | // so we should not select it anymore 98 | if (text_util.stringEncloses(query, suggestedValue)) { 99 | return false; 100 | } 101 | // if there is suggestion that contains query as its part 102 | // than we should ignore all other matches, even full ones 103 | if (suggestedValue.indexOf(normalizedQuery) > 0) { 104 | return false; 105 | } 106 | if ( 107 | normalizedQuery === 108 | text_util.normalize(suggestedValue, stopwords) 109 | ) { 110 | matches.push(i); 111 | } 112 | }); 113 | 114 | return matches.length === 1 ? matches[0] : -1; 115 | }; 116 | }, 117 | 118 | matchByWords: function(stopwords) { 119 | return _matchByWords(stopwords, haveSameParent); 120 | }, 121 | 122 | matchByWordsAddress: function(stopwords) { 123 | return _matchByWords(stopwords, haveSameParent); 124 | }, 125 | 126 | /** 127 | * Matches query against values contained in suggestion fields 128 | * for cases, when there is only one suggestion 129 | * only considers fields specified in fields map 130 | * uses partial matching: 131 | * "0445" vs { value: "ALFA-BANK", data: { "bic": "044525593" }} is a match 132 | */ 133 | matchByFields: function(fields) { 134 | return function(query, suggestions) { 135 | var tokens = text_util.splitTokens(text_util.split(query)); 136 | var suggestionWords = []; 137 | 138 | if (suggestions.length === 1) { 139 | if (fields) { 140 | collection_util.each(fields, function(stopwords, field) { 141 | var fieldValue = object_util.getDeepValue( 142 | suggestions[0], 143 | field 144 | ); 145 | var fieldWords = 146 | fieldValue && 147 | text_util.splitTokens( 148 | text_util.split(fieldValue, stopwords) 149 | ); 150 | 151 | if (fieldWords && fieldWords.length) { 152 | suggestionWords = suggestionWords.concat( 153 | fieldWords 154 | ); 155 | } 156 | }); 157 | } 158 | 159 | if ( 160 | collection_util.minusWithPartialMatching( 161 | tokens, 162 | suggestionWords 163 | ).length === 0 164 | ) { 165 | return 0; 166 | } 167 | } 168 | 169 | return -1; 170 | }; 171 | } 172 | }; 173 | 174 | export { matchers }; 175 | -------------------------------------------------------------------------------- /src/includes/notificator.js: -------------------------------------------------------------------------------- 1 | var notificator = { 2 | chains: {}, 3 | 4 | on: function(name, method) { 5 | this.get(name).push(method); 6 | return this; 7 | }, 8 | 9 | get: function(name) { 10 | var chains = this.chains; 11 | return chains[name] || (chains[name] = []); 12 | } 13 | }; 14 | 15 | export { notificator }; 16 | -------------------------------------------------------------------------------- /src/includes/promo.js: -------------------------------------------------------------------------------- 1 | import { CLASSES } from "./constants"; 2 | import { notificator } from "./notificator"; 3 | import { dom } from "./dom"; 4 | 5 | /** 6 | * Промо-ссылка в списке подсказок. 7 | */ 8 | var FREE_PLAN = "FREE"; 9 | var LINK = 10 | "https://dadata.ru/suggestions/?utm_source=dadata&utm_medium=module&utm_campaign=suggestions-jquery"; 11 | var PREFIX = ""; 12 | var SUFFIX = ""; 13 | var IMAGE = 14 | ''; 15 | 16 | function Promo(plugin) { 17 | this.plan = plugin.status.plan; 18 | var container = plugin.getContainer(); 19 | this.element = dom.selectByClass(CLASSES.promo, container); 20 | } 21 | 22 | Promo.prototype.show = function() { 23 | if (this.plan !== FREE_PLAN) { 24 | return; 25 | } 26 | if (!this.element) { 27 | return; 28 | } 29 | this.setStyles(); 30 | this.setHtml(); 31 | }; 32 | 33 | Promo.prototype.setStyles = function() { 34 | this.element.style.display = "block"; 35 | }; 36 | 37 | Promo.prototype.setHtml = function() { 38 | this.element.innerHTML = 39 | '' + 42 | PREFIX + 43 | IMAGE + 44 | SUFFIX + 45 | ""; 46 | }; 47 | 48 | function show() { 49 | new Promo(this).show(); 50 | } 51 | 52 | notificator.on("assignSuggestions", show); 53 | -------------------------------------------------------------------------------- /src/includes/select.js: -------------------------------------------------------------------------------- 1 | import { jqapi } from "./jqapi"; 2 | import { utils } from "./utils"; 3 | import { Suggestions } from "./suggestions"; 4 | import { notificator } from "./notificator"; 5 | 6 | /** 7 | * Methods for selecting a suggestion 8 | */ 9 | 10 | var methods = { 11 | proceedQuery: function(query) { 12 | var that = this; 13 | 14 | if (query.length >= that.options.minChars) { 15 | that.updateSuggestions(query); 16 | } else { 17 | that.hide(); 18 | } 19 | }, 20 | 21 | /** 22 | * Selects current or first matched suggestion, but firstly waits for data ready 23 | * @param selectionOptions 24 | * @returns {$.Deferred} promise, resolved with index of selected suggestion or rejected if nothing matched 25 | */ 26 | selectCurrentValue: function(selectionOptions) { 27 | var that = this, 28 | result = jqapi.Deferred(); 29 | 30 | // force onValueChange to be executed if it has been deferred 31 | that.inputPhase.resolve(); 32 | 33 | that.fetchPhase 34 | .done(function() { 35 | var index; 36 | 37 | // When suggestion has already been selected and not modified 38 | if (that.selection && !that.visible) { 39 | result.reject(); 40 | } else { 41 | index = that.findSuggestionIndex(); 42 | 43 | that.select(index, selectionOptions); 44 | 45 | if (index === -1) { 46 | result.reject(); 47 | } else { 48 | result.resolve(index); 49 | } 50 | } 51 | }) 52 | .fail(function() { 53 | result.reject(); 54 | }); 55 | 56 | return result; 57 | }, 58 | 59 | /** 60 | * Selects first when user interaction is not supposed 61 | */ 62 | selectFoundSuggestion: function() { 63 | var that = this; 64 | 65 | if (!that.requestMode.userSelect) { 66 | that.select(0); 67 | } 68 | }, 69 | 70 | /** 71 | * Selects current or first matched suggestion 72 | * @returns {number} index of found suggestion 73 | */ 74 | findSuggestionIndex: function() { 75 | var that = this, 76 | index = that.selectedIndex, 77 | value; 78 | 79 | if (index === -1) { 80 | // matchers always operate with trimmed strings 81 | value = that.el.val().trim(); 82 | if (value) { 83 | that.type.matchers.some(function(matcher) { 84 | index = matcher(value, that.suggestions); 85 | return index !== -1; 86 | }); 87 | } 88 | } 89 | 90 | return index; 91 | }, 92 | 93 | /** 94 | * Selects a suggestion at specified index 95 | * @param index index of suggestion to select. Can be -1 96 | * @param {Object} selectionOptions 97 | * @param {boolean} [selectionOptions.continueSelecting] prevents hiding after selection 98 | * @param {boolean} [selectionOptions.noSpace] prevents adding space at the end of current value 99 | */ 100 | select: function(index, selectionOptions) { 101 | var that = this, 102 | suggestion = that.suggestions[index], 103 | continueSelecting = 104 | selectionOptions && selectionOptions.continueSelecting, 105 | currentValue = that.currentValue, 106 | hasSameValues; 107 | 108 | // Prevent recursive execution 109 | if (that.triggering["Select"]) return; 110 | 111 | // if no suggestion to select 112 | if (!suggestion) { 113 | if (!continueSelecting && !that.selection) { 114 | that.triggerOnSelectNothing(); 115 | } 116 | that.onSelectComplete(continueSelecting); 117 | return; 118 | } 119 | 120 | hasSameValues = that.hasSameValues(suggestion); 121 | 122 | that.enrichSuggestion(suggestion, selectionOptions).done(function( 123 | enrichedSuggestion, 124 | hasBeenEnriched 125 | ) { 126 | var newSelectionOptions = jqapi.extend( 127 | { 128 | hasBeenEnriched: hasBeenEnriched, 129 | hasSameValues: hasSameValues 130 | }, 131 | selectionOptions 132 | ); 133 | that.selectSuggestion( 134 | enrichedSuggestion, 135 | index, 136 | currentValue, 137 | newSelectionOptions 138 | ); 139 | }); 140 | }, 141 | 142 | /** 143 | * Formats and selects final (enriched) suggestion 144 | * @param suggestion 145 | * @param index 146 | * @param lastValue 147 | * @param {Object} selectionOptions 148 | * @param {boolean} [selectionOptions.continueSelecting] prevents hiding after selection 149 | * @param {boolean} [selectionOptions.noSpace] prevents adding space at the end of current value 150 | * @param {boolean} selectionOptions.hasBeenEnriched 151 | * @param {boolean} selectionOptions.hasSameValues 152 | */ 153 | selectSuggestion: function(suggestion, index, lastValue, selectionOptions) { 154 | var that = this, 155 | continueSelecting = selectionOptions.continueSelecting, 156 | assumeDataComplete = 157 | !that.type.isDataComplete || 158 | that.type.isDataComplete.call(that, suggestion), 159 | currentSelection = that.selection; 160 | 161 | // Prevent recursive execution 162 | if (that.triggering["Select"]) return; 163 | 164 | if (that.type.alwaysContinueSelecting) { 165 | continueSelecting = true; 166 | } 167 | 168 | if (assumeDataComplete) { 169 | continueSelecting = false; 170 | } 171 | 172 | // `suggestions` cat be empty, e.g. during `fixData` 173 | if (selectionOptions.hasBeenEnriched && that.suggestions[index]) { 174 | that.suggestions[index].data = suggestion.data; 175 | } 176 | 177 | if (that.requestMode.updateValue) { 178 | that.checkValueBounds(suggestion); 179 | that.currentValue = that.getSuggestionValue( 180 | suggestion, 181 | selectionOptions 182 | ); 183 | 184 | if ( 185 | that.currentValue && 186 | !selectionOptions.noSpace && 187 | !assumeDataComplete 188 | ) { 189 | that.currentValue += " "; 190 | } 191 | that.el.val(that.currentValue); 192 | } 193 | 194 | if (that.currentValue) { 195 | that.selection = suggestion; 196 | if (!that.areSuggestionsSame(suggestion, currentSelection)) { 197 | that.trigger( 198 | "Select", 199 | suggestion, 200 | that.currentValue != lastValue 201 | ); 202 | } 203 | if (that.requestMode.userSelect) { 204 | that.onSelectComplete(continueSelecting); 205 | } 206 | } else { 207 | that.selection = null; 208 | that.triggerOnSelectNothing(); 209 | } 210 | 211 | that.shareWithParent(suggestion); 212 | }, 213 | 214 | onSelectComplete: function(continueSelecting) { 215 | var that = this; 216 | 217 | if (continueSelecting) { 218 | that.selectedIndex = -1; 219 | that.updateSuggestions(that.currentValue); 220 | } else { 221 | that.hide(); 222 | } 223 | }, 224 | 225 | triggerOnSelectNothing: function() { 226 | var that = this; 227 | 228 | if (!that.triggering["SelectNothing"]) { 229 | that.trigger("SelectNothing", that.currentValue); 230 | } 231 | }, 232 | 233 | trigger: function(event) { 234 | var that = this, 235 | args = utils.slice(arguments, 1), 236 | callback = that.options["on" + event]; 237 | 238 | that.triggering[event] = true; 239 | if (utils.isFunction(callback)) { 240 | callback.apply(that.element, args); 241 | } 242 | that.el.trigger.call( 243 | that.el, 244 | "suggestions-" + event.toLowerCase(), 245 | args 246 | ); 247 | that.triggering[event] = false; 248 | } 249 | }; 250 | 251 | jqapi.extend(Suggestions.prototype, methods); 252 | 253 | notificator.on("assignSuggestions", methods.selectFoundSuggestion); 254 | -------------------------------------------------------------------------------- /src/includes/status.js: -------------------------------------------------------------------------------- 1 | import { jqapi } from "./jqapi"; 2 | import { notificator } from "./notificator"; 3 | import { utils } from "./utils"; 4 | import { Suggestions } from "./suggestions"; 5 | 6 | /** 7 | * Methods related to plugin's authorization on server 8 | */ 9 | 10 | // keys are "[type][token]" 11 | var statusRequests = {}; 12 | 13 | function resetTokens() { 14 | utils.each(statusRequests, function(req) { 15 | req.abort(); 16 | }); 17 | statusRequests = {}; 18 | } 19 | 20 | resetTokens(); 21 | 22 | var methods = { 23 | checkStatus: function() { 24 | var that = this, 25 | token = (that.options.token && that.options.token.trim()) || "", 26 | requestKey = that.options.type + token, 27 | request = statusRequests[requestKey]; 28 | 29 | if (!request) { 30 | request = statusRequests[requestKey] = jqapi.ajax( 31 | that.getAjaxParams("status") 32 | ); 33 | } 34 | 35 | request 36 | .done(function(status, textStatus, request) { 37 | if (status.search) { 38 | var plan = request.getResponseHeader("X-Plan"); 39 | status.plan = plan; 40 | jqapi.extend(that.status, status); 41 | } else { 42 | triggerError("Service Unavailable"); 43 | } 44 | }) 45 | .fail(function() { 46 | triggerError(request.statusText); 47 | }); 48 | 49 | function triggerError(errorThrown) { 50 | // If unauthorized 51 | if (utils.isFunction(that.options.onSearchError)) { 52 | that.options.onSearchError.call( 53 | that.element, 54 | null, 55 | request, 56 | "error", 57 | errorThrown 58 | ); 59 | } 60 | } 61 | } 62 | }; 63 | 64 | Suggestions.resetTokens = resetTokens; 65 | 66 | jqapi.extend(Suggestions.prototype, methods); 67 | 68 | notificator.on("setOptions", methods.checkStatus); 69 | 70 | //export { methods, resetTokens }; 71 | -------------------------------------------------------------------------------- /src/includes/types.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_OPTIONS } from "./default-options"; 2 | import { jqapi } from "./jqapi"; 3 | import { ADDRESS_TYPE } from "./types/address"; 4 | import { FIAS_TYPE } from "./types/fias"; 5 | import { NAME_TYPE } from "./types/name"; 6 | import { PARTY_TYPE } from "./types/party"; 7 | import { EMAIL_TYPE } from "./types/email"; 8 | import { BANK_TYPE } from "./types/bank"; 9 | import { Outward } from "./types/outward"; 10 | 11 | /** 12 | * Type is a bundle of properties: 13 | * - urlSuffix Mandatory. String 14 | * - matchers Mandatory. Array of functions (with optional data bound as a context) that find appropriate suggestion to select 15 | * - `fieldNames` Map fields of suggestion.data to their displayable names 16 | * - `unformattableTokens` Array of strings which should not be highlighted 17 | * - `dataComponents` Array of 'bound's can be set as `bounds` option. Order is important. 18 | * 19 | * flags: 20 | * - `alwaysContinueSelecting` Forbids to hide dropdown after selecting 21 | * - `geoEnabled` Makes to detect client's location for passing it to all requests 22 | * - `enrichmentEnabled` Makes to send additional request when a suggestion is selected 23 | * 24 | * and methods: 25 | * - `isDataComplete` Checks if suggestion.data can be operated as full data of it's type 26 | * - `composeValue` returns string value based on suggestion.data 27 | * - `formatResult` returns html of a suggestion. Overrides default method 28 | * - `formatResultInn` returns html of suggestion.data.inn 29 | * - `isQueryRequestable` checks if query is appropriated for requesting server 30 | * - `formatSelected` returns string to be inserted in textbox 31 | */ 32 | 33 | var types = { 34 | NAME: NAME_TYPE, 35 | ADDRESS: ADDRESS_TYPE, 36 | FIAS: FIAS_TYPE, 37 | PARTY: PARTY_TYPE, 38 | EMAIL: EMAIL_TYPE, 39 | BANK: BANK_TYPE 40 | }; 41 | 42 | types.get = function(type) { 43 | if (types.hasOwnProperty(type)) { 44 | return types[type]; 45 | } else { 46 | return new Outward(type); 47 | } 48 | }; 49 | 50 | jqapi.extend(DEFAULT_OPTIONS, { 51 | suggest_local: true 52 | }); 53 | 54 | export { types }; 55 | -------------------------------------------------------------------------------- /src/includes/types/bank.js: -------------------------------------------------------------------------------- 1 | import { WORD_DELIMITERS } from "../constants"; 2 | import { object_util } from "../utils/object"; 3 | import { jqapi } from "../jqapi"; 4 | import { matchers } from "../matchers"; 5 | import { ADDRESS_STOPWORDS, ADDRESS_COMPONENTS } from "./address"; 6 | 7 | var BANK_TYPE = { 8 | urlSuffix: "bank", 9 | noSuggestionsHint: "Неизвестный банк", 10 | matchers: [ 11 | matchers.matchByFields( 12 | // These fields of suggestion's `data` used by by-words matcher 13 | { 14 | value: null, 15 | "data.bic": null, 16 | "data.swift": null, 17 | } 18 | ), 19 | ], 20 | dataComponents: ADDRESS_COMPONENTS, 21 | enrichmentEnabled: true, 22 | enrichmentMethod: "findById", 23 | enrichmentParams: { 24 | count: 1, 25 | }, 26 | getEnrichmentQuery: function(suggestion) { 27 | return suggestion.data.bic; 28 | }, 29 | geoEnabled: true, 30 | formatResult: function(value, currentValue, suggestion, options) { 31 | var that = this, 32 | formattedBIC = that.highlightMatches( 33 | object_util.getDeepValue(suggestion.data, "bic"), 34 | currentValue, 35 | suggestion 36 | ), 37 | address = 38 | object_util.getDeepValue(suggestion.data, "address.value") || 39 | ""; 40 | 41 | value = that.highlightMatches(value, currentValue, suggestion, options); 42 | value = that.wrapFormattedValue(value, suggestion); 43 | 44 | if (address) { 45 | address = address.replace(/^\d{6}( РОССИЯ)?, /i, ""); 46 | if (that.isMobile) { 47 | // keep only two first words 48 | address = address.replace( 49 | new RegExp( 50 | "^([^" + 51 | WORD_DELIMITERS + 52 | "]+[" + 53 | WORD_DELIMITERS + 54 | "]+[^" + 55 | WORD_DELIMITERS + 56 | "]+).*" 57 | ), 58 | "$1" 59 | ); 60 | } else { 61 | address = that.highlightMatches( 62 | address, 63 | currentValue, 64 | suggestion, 65 | { 66 | unformattableTokens: ADDRESS_STOPWORDS, 67 | } 68 | ); 69 | } 70 | } 71 | 72 | if (formattedBIC || address) { 73 | value += 74 | '
' + 77 | '' + 80 | formattedBIC + 81 | "" + 82 | address + 83 | "
"; 84 | } 85 | return value; 86 | }, 87 | formatSelected: function(suggestion) { 88 | return ( 89 | object_util.getDeepValue(suggestion, "data.name.payment") || null 90 | ); 91 | }, 92 | }; 93 | 94 | export { BANK_TYPE }; 95 | -------------------------------------------------------------------------------- /src/includes/types/email.js: -------------------------------------------------------------------------------- 1 | import { matchers } from "../matchers"; 2 | 3 | var EMAIL_TYPE = { 4 | urlSuffix: "email", 5 | noSuggestionsHint: false, 6 | matchers: [matchers.matchByNormalizedQuery()], 7 | isQueryRequestable: function(query) { 8 | return this.options.suggest_local || query.indexOf("@") >= 0; 9 | } 10 | }; 11 | 12 | export { EMAIL_TYPE }; 13 | -------------------------------------------------------------------------------- /src/includes/types/name.js: -------------------------------------------------------------------------------- 1 | import { WORD_DELIMITERS } from "../constants"; 2 | import { lang_util } from "../utils/lang"; 3 | import { collection_util } from "../utils/collection"; 4 | import { text_util } from "../utils/text"; 5 | import { object_util } from "../utils/object"; 6 | import { matchers } from "../matchers"; 7 | 8 | function valueStartsWith(suggestion, field) { 9 | var fieldValue = suggestion.data && suggestion.data[field]; 10 | 11 | return ( 12 | fieldValue && 13 | new RegExp( 14 | "^" + 15 | text_util.escapeRegExChars(fieldValue) + 16 | "([" + 17 | WORD_DELIMITERS + 18 | "]|$)", 19 | "i" 20 | ).test(suggestion.value) 21 | ); 22 | } 23 | 24 | var NAME_TYPE = { 25 | urlSuffix: "fio", 26 | noSuggestionsHint: false, 27 | matchers: [matchers.matchByNormalizedQuery(), matchers.matchByWords()], 28 | // names for labels, describing which fields are displayed 29 | fieldNames: { 30 | surname: "фамилия", 31 | name: "имя", 32 | patronymic: "отчество" 33 | }, 34 | isDataComplete: function(suggestion) { 35 | var that = this, 36 | params = that.options.params, 37 | data = suggestion.data, 38 | fields; 39 | 40 | if (lang_util.isFunction(params)) { 41 | params = params.call(that.element, suggestion.value); 42 | } 43 | if (params && params.parts) { 44 | fields = params.parts.map(function(part) { 45 | return part.toLowerCase(); 46 | }); 47 | } else { 48 | // when NAME is first, patronymic is mot mandatory 49 | fields = ["surname", "name"]; 50 | // when SURNAME is first, it is 51 | if (valueStartsWith(suggestion, "surname")) { 52 | fields.push("patronymic"); 53 | } 54 | } 55 | return object_util.fieldsAreNotEmpty(data, fields); 56 | }, 57 | composeValue: function(data) { 58 | return collection_util 59 | .compact([data.surname, data.name, data.patronymic]) 60 | .join(" "); 61 | } 62 | }; 63 | 64 | export { NAME_TYPE }; 65 | -------------------------------------------------------------------------------- /src/includes/types/outward.js: -------------------------------------------------------------------------------- 1 | import { matchers } from "../matchers"; 2 | 3 | function Outward(name) { 4 | this.urlSuffix = name.toLowerCase(); 5 | this.noSuggestionsHint = "Неизвестное значение"; 6 | this.matchers = [ 7 | matchers.matchByNormalizedQuery(), 8 | matchers.matchByWords() 9 | ]; 10 | } 11 | 12 | export { Outward }; 13 | -------------------------------------------------------------------------------- /src/includes/types/party.js: -------------------------------------------------------------------------------- 1 | import { WORD_DELIMITERS } from "../constants"; 2 | import { object_util } from "../utils/object"; 3 | import { jqapi } from "../jqapi"; 4 | import { matchers } from "../matchers"; 5 | import { ADDRESS_STOPWORDS, ADDRESS_COMPONENTS } from "./address"; 6 | 7 | var innPartsLengths = { 8 | LEGAL: [2, 2, 5, 1], 9 | INDIVIDUAL: [2, 2, 6, 2] 10 | }; 11 | 12 | function chooseFormattedField(formattedMain, formattedAlt) { 13 | var rHasMatch = //; 14 | return rHasMatch.test(formattedAlt) && !rHasMatch.test(formattedMain) 15 | ? formattedAlt 16 | : formattedMain; 17 | } 18 | 19 | function formattedField(main, alt, currentValue, suggestion, options) { 20 | var that = this, 21 | formattedMain = that.highlightMatches( 22 | main, 23 | currentValue, 24 | suggestion, 25 | options 26 | ), 27 | formattedAlt = that.highlightMatches( 28 | alt, 29 | currentValue, 30 | suggestion, 31 | options 32 | ); 33 | 34 | return chooseFormattedField(formattedMain, formattedAlt); 35 | } 36 | 37 | var PARTY_TYPE = { 38 | urlSuffix: "party", 39 | noSuggestionsHint: "Неизвестная организация", 40 | matchers: [ 41 | matchers.matchByFields( 42 | // These fields of suggestion's `data` used by by-words matcher 43 | { 44 | value: null, 45 | "data.address.value": ADDRESS_STOPWORDS, 46 | "data.inn": null, 47 | "data.ogrn": null 48 | } 49 | ) 50 | ], 51 | dataComponents: ADDRESS_COMPONENTS, 52 | enrichmentEnabled: true, 53 | enrichmentMethod: "findById", 54 | enrichmentParams: { 55 | count: 1, 56 | locations_boost: null 57 | }, 58 | getEnrichmentQuery: function(suggestion) { 59 | return suggestion.data.hid; 60 | }, 61 | geoEnabled: true, 62 | formatResult: function(value, currentValue, suggestion, options) { 63 | var that = this, 64 | formattedInn = that.type.formatResultInn.call( 65 | that, 66 | suggestion, 67 | currentValue 68 | ), 69 | formatterOGRN = that.highlightMatches( 70 | object_util.getDeepValue(suggestion.data, "ogrn"), 71 | currentValue, 72 | suggestion 73 | ), 74 | formattedInnOGRN = chooseFormattedField( 75 | formattedInn, 76 | formatterOGRN 77 | ), 78 | formattedFIO = that.highlightMatches( 79 | object_util.getDeepValue(suggestion.data, "management.name"), 80 | currentValue, 81 | suggestion 82 | ), 83 | address = 84 | object_util.getDeepValue(suggestion.data, "address.value") || 85 | ""; 86 | 87 | if (that.isMobile) { 88 | (options || (options = {})).maxLength = 50; 89 | } 90 | 91 | value = formattedField.call( 92 | that, 93 | value, 94 | object_util.getDeepValue(suggestion.data, "name.latin"), 95 | currentValue, 96 | suggestion, 97 | options 98 | ); 99 | value = that.wrapFormattedValue(value, suggestion); 100 | 101 | if (address) { 102 | address = address.replace(/^(\d{6}|Россия),\s+/i, ""); 103 | if (that.isMobile) { 104 | // keep only two first words 105 | address = address.replace( 106 | new RegExp( 107 | "^([^" + 108 | WORD_DELIMITERS + 109 | "]+[" + 110 | WORD_DELIMITERS + 111 | "]+[^" + 112 | WORD_DELIMITERS + 113 | "]+).*" 114 | ), 115 | "$1" 116 | ); 117 | } else { 118 | address = that.highlightMatches( 119 | address, 120 | currentValue, 121 | suggestion, 122 | { 123 | unformattableTokens: ADDRESS_STOPWORDS 124 | } 125 | ); 126 | } 127 | } 128 | 129 | if (formattedInnOGRN || address || formattedFIO) { 130 | value += 131 | '
' + 134 | '' + 137 | (formattedInnOGRN || "") + 138 | "" + 139 | (chooseFormattedField(address, formattedFIO) || "") + 140 | "
"; 141 | } 142 | return value; 143 | }, 144 | formatResultInn: function(suggestion, currentValue) { 145 | var that = this, 146 | inn = suggestion.data && suggestion.data.inn, 147 | innPartsLength = 148 | innPartsLengths[suggestion.data && suggestion.data.type], 149 | innParts, 150 | formattedInn, 151 | rDigit = /\d/; 152 | 153 | if (inn) { 154 | formattedInn = that.highlightMatches(inn, currentValue, suggestion); 155 | if (innPartsLength) { 156 | formattedInn = formattedInn.split(""); 157 | innParts = innPartsLength.map(function(partLength) { 158 | var formattedPart = "", 159 | ch; 160 | 161 | while (partLength && (ch = formattedInn.shift())) { 162 | formattedPart += ch; 163 | if (rDigit.test(ch)) partLength--; 164 | } 165 | 166 | return formattedPart; 167 | }); 168 | formattedInn = 169 | innParts.join( 170 | '' 173 | ) + formattedInn.join(""); 174 | } 175 | 176 | return formattedInn; 177 | } 178 | } 179 | }; 180 | 181 | export { PARTY_TYPE }; 182 | -------------------------------------------------------------------------------- /src/includes/utils.js: -------------------------------------------------------------------------------- 1 | import { collection_util } from "./utils/collection"; 2 | import { func_util } from "./utils/func"; 3 | import { lang_util } from "./utils/lang"; 4 | import { object_util } from "./utils/object"; 5 | import { text_util } from "./utils/text"; 6 | import { jqapi } from "./jqapi"; 7 | import { ajax } from "./ajax"; 8 | 9 | /** 10 | * Возвращает автоинкрементный идентификатор. 11 | * @param {string} prefix - префикс для идентификатора 12 | */ 13 | var generateId = (function() { 14 | var counter = 0; 15 | return function(prefix) { 16 | return (prefix || "") + ++counter; 17 | }; 18 | })(); 19 | 20 | /** 21 | * Утилиты на все случаи жизни. 22 | */ 23 | var utils = { 24 | escapeRegExChars: text_util.escapeRegExChars, 25 | escapeHtml: text_util.escapeHtml, 26 | formatToken: text_util.formatToken, 27 | normalize: text_util.normalize, 28 | reWordExtractor: text_util.getWordExtractorRegExp, 29 | stringEncloses: text_util.stringEncloses, 30 | 31 | addUrlParams: ajax.addUrlParams, 32 | getDefaultContentType: ajax.getDefaultContentType, 33 | getDefaultType: ajax.getDefaultType, 34 | fixURLProtocol: ajax.fixURLProtocol, 35 | serialize: ajax.serialize, 36 | 37 | arrayMinus: collection_util.minus, 38 | arrayMinusWithPartialMatching: collection_util.minusWithPartialMatching, 39 | arraysIntersection: collection_util.intersect, 40 | compact: collection_util.compact, 41 | each: collection_util.each, 42 | makeArray: collection_util.makeArray, 43 | slice: collection_util.slice, 44 | 45 | delay: func_util.delay, 46 | 47 | areSame: object_util.areSame, 48 | compactObject: object_util.compact, 49 | getDeepValue: object_util.getDeepValue, 50 | fieldsNotEmpty: object_util.fieldsAreNotEmpty, 51 | indexBy: object_util.indexObjectsById, 52 | 53 | isArray: lang_util.isArray, 54 | isEmptyObject: lang_util.isEmptyObject, 55 | isFunction: lang_util.isFunction, 56 | isPlainObject: lang_util.isPlainObject, 57 | 58 | uniqueId: generateId 59 | }; 60 | 61 | export { generateId, utils }; 62 | -------------------------------------------------------------------------------- /src/includes/utils/collection.js: -------------------------------------------------------------------------------- 1 | import { lang_util } from "./lang"; 2 | 3 | /** 4 | * Утилиты для работы с коллекциями. 5 | */ 6 | var collection_util = { 7 | /** 8 | * Возвращает массив без пустых элементов 9 | */ 10 | compact: function(array) { 11 | return array.filter(function(el) { 12 | return !!el; 13 | }); 14 | }, 15 | 16 | /** 17 | * Итерирует по элементам массива или полям объекта. 18 | * Ведёт себя как $.each() - прерывает выполнение, если функция-обработчик возвращает false. 19 | * @param {Object|Array} obj - массив или объект 20 | * @param {eachCallback} callback - функция-обработчик 21 | */ 22 | each: function(obj, callback) { 23 | if (Array.isArray(obj)) { 24 | obj.some(function(el, idx) { 25 | return callback(el, idx) === false; 26 | }); 27 | return; 28 | } 29 | Object.keys(obj).some(function(key) { 30 | var value = obj[key]; 31 | return callback(value, key) === false; 32 | }); 33 | }, 34 | 35 | /** 36 | * Пересечение массивов: ([1,2,3,4], [2,4,5,6]) => [2,4] 37 | * Исходные массивы не меняются. 38 | */ 39 | intersect: function(array1, array2) { 40 | var result = []; 41 | if (!Array.isArray(array1) || !Array.isArray(array2)) { 42 | return result; 43 | } 44 | return array1.filter(function(el) { 45 | return array2.indexOf(el) !== -1; 46 | }); 47 | }, 48 | 49 | /** 50 | * Разность массивов: ([1,2,3,4], [2,4,5,6]) => [1,3] 51 | * Исходные массивы не меняются. 52 | */ 53 | minus: function(array1, array2) { 54 | if (!array2 || array2.length === 0) { 55 | return array1; 56 | } 57 | return array1.filter(function(el) { 58 | return array2.indexOf(el) === -1; 59 | }); 60 | }, 61 | 62 | /** 63 | * Обрачивает переданный объект в массив. 64 | * Если передан массив, возвращает его копию. 65 | */ 66 | makeArray: function(arrayLike) { 67 | if (lang_util.isArray(arrayLike)) { 68 | return Array.prototype.slice.call(arrayLike); 69 | } else { 70 | return [arrayLike]; 71 | } 72 | }, 73 | 74 | /** 75 | * Разность массивов с частичным совпадением элементов. 76 | * Если элемент второго массива включает в себя элемент первого, 77 | * элементы считаются равными. 78 | */ 79 | minusWithPartialMatching: function(array1, array2) { 80 | if (!array2 || array2.length === 0) { 81 | return array1; 82 | } 83 | return array1.filter(function(el) { 84 | return !array2.some(function(el2) { 85 | return el2.indexOf(el) === 0; 86 | }); 87 | }); 88 | }, 89 | 90 | /** 91 | * Копирует массив, начиная с указанного элемента. 92 | * @param obj - массив 93 | * @param start - индекс, начиная с которого надо скопировать 94 | */ 95 | slice: function(obj, start) { 96 | return Array.prototype.slice.call(obj, start); 97 | } 98 | }; 99 | 100 | export { collection_util }; 101 | -------------------------------------------------------------------------------- /src/includes/utils/func.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Утилиты для работы с функциями. 3 | */ 4 | var func_util = { 5 | /** 6 | * Выполняет функцию с указанной задержкой. 7 | * @param {Function} handler - функция 8 | * @param {number} delay - задержка в миллисекундах 9 | */ 10 | delay: function(handler, delay) { 11 | return setTimeout(handler, delay || 0); 12 | } 13 | }; 14 | 15 | export { func_util }; 16 | -------------------------------------------------------------------------------- /src/includes/utils/lang.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Утилиты для работы с типами. 3 | */ 4 | var lang_util = { 5 | /** 6 | * Проверяет, является ли аргумент массивом. 7 | */ 8 | isArray: function(array) { 9 | return Array.isArray(array); 10 | }, 11 | 12 | /** 13 | * Проверяет, является ли аргумент функцией. 14 | */ 15 | isFunction: function(it) { 16 | return Object.prototype.toString.call(it) === "[object Function]"; 17 | }, 18 | 19 | /** 20 | * Проверяет, является ли аргумент пустым объектом ({}). 21 | */ 22 | isEmptyObject: function(obj) { 23 | return Object.keys(obj).length === 0 && obj.constructor === Object; 24 | }, 25 | 26 | /** 27 | * Проверяет, является ли аргумент «обычным» объектом 28 | * (не undefiend, не null, не DOM-элемент) 29 | */ 30 | isPlainObject: function(obj) { 31 | if ( 32 | obj === undefined || 33 | typeof obj !== "object" || 34 | obj === null || 35 | obj.nodeType || 36 | obj === obj.window 37 | ) { 38 | return false; 39 | } 40 | if ( 41 | obj.constructor && 42 | !Object.prototype.hasOwnProperty.call( 43 | obj.constructor.prototype, 44 | "isPrototypeOf" 45 | ) 46 | ) { 47 | return false; 48 | } 49 | return true; 50 | } 51 | }; 52 | 53 | export { lang_util }; 54 | -------------------------------------------------------------------------------- /src/includes/utils/object.js: -------------------------------------------------------------------------------- 1 | import { collection_util } from "./collection"; 2 | import { lang_util } from "./lang"; 3 | 4 | /** 5 | * Утилиты для работы с объектами. 6 | */ 7 | var object_util = { 8 | /** 9 | * Сравнивает два объекта по полям, которые присутствуют в обоих 10 | * @returns {boolean} true, если поля совпадают, false в противном случае 11 | */ 12 | areSame: function self(a, b) { 13 | var same = true; 14 | 15 | if (typeof a != typeof b) { 16 | return false; 17 | } 18 | 19 | if (typeof a == "object" && a != null && b != null) { 20 | collection_util.each(a, function(value, i) { 21 | return (same = self(value, b[i])); 22 | }); 23 | return same; 24 | } 25 | 26 | return a === b; 27 | }, 28 | 29 | /** 30 | * Копирует свойства и их значения из исходных объектов в целевой 31 | */ 32 | assign: function(target, varArgs) { 33 | if (typeof Object.assign === "function") { 34 | return Object.assign.apply(null, arguments); 35 | } 36 | if (target == null) { 37 | // TypeError if undefined or null 38 | throw new TypeError("Cannot convert undefined or null to object"); 39 | } 40 | 41 | var to = Object(target); 42 | 43 | for (var index = 1; index < arguments.length; index++) { 44 | var nextSource = arguments[index]; 45 | 46 | if (nextSource != null) { 47 | // Skip over if undefined or null 48 | for (var nextKey in nextSource) { 49 | // Avoid bugs when hasOwnProperty is shadowed 50 | if ( 51 | Object.prototype.hasOwnProperty.call( 52 | nextSource, 53 | nextKey 54 | ) 55 | ) { 56 | to[nextKey] = nextSource[nextKey]; 57 | } 58 | } 59 | } 60 | } 61 | return to; 62 | }, 63 | 64 | /** 65 | * Клонирует объект глубоким копированием 66 | */ 67 | clone: function(obj) { 68 | return JSON.parse(JSON.stringify(obj)); 69 | }, 70 | 71 | /** 72 | * Возвращает копию объекта без пустых полей 73 | * (без undefined, null и '') 74 | * @param obj 75 | */ 76 | compact: function(obj) { 77 | var copy = object_util.clone(obj); 78 | 79 | collection_util.each(copy, function(val, key) { 80 | if (val === null || val === undefined || val === "") { 81 | delete copy[key]; 82 | } 83 | }); 84 | 85 | return copy; 86 | }, 87 | 88 | /** 89 | * Проверяет, что указанные поля в объекте заполнены. 90 | * @param {Object} obj - проверяемый объект 91 | * @param {Array} fields - список названий полей, которые надо проверить 92 | * @returns {boolean} 93 | */ 94 | fieldsAreNotEmpty: function(obj, fields) { 95 | if (!lang_util.isPlainObject(obj)) { 96 | return false; 97 | } 98 | var result = true; 99 | collection_util.each(fields, function(field, i) { 100 | result = !!obj[field]; 101 | return result; 102 | }); 103 | return result; 104 | }, 105 | 106 | /** 107 | * Возвращает вложенное значение по указанному пути 108 | * например, 'data.address.value' 109 | */ 110 | getDeepValue: function self(obj, name) { 111 | var path = name.split("."), 112 | step = path.shift(); 113 | 114 | return ( 115 | obj && (path.length ? self(obj[step], path.join(".")) : obj[step]) 116 | ); 117 | }, 118 | 119 | /** 120 | * Возвращает карту объектов по их идентификаторам. 121 | * Принимает на вход массив объектов и идентифицирующее поле. 122 | * Возвращает карты, ключом в которой является значение идентифицирующего поля, 123 | * а значением — исходный объект. 124 | * Заодно добавляет объектам поле с порядковым номером. 125 | * @param {Array} objectsArray - массив объектов 126 | * @param {string} idField - название идентифицирующего поля 127 | * @param {string} indexField - название поля с порядковым номером 128 | * @return {Object} карта объектов по их идентификаторам 129 | */ 130 | indexObjectsById: function(objectsArray, idField, indexField) { 131 | var result = {}; 132 | 133 | collection_util.each(objectsArray, function(obj, idx) { 134 | var key = obj[idField]; 135 | var val = {}; 136 | 137 | if (indexField) { 138 | val[indexField] = idx; 139 | } 140 | 141 | result[key] = object_util.assign(val, obj); 142 | }); 143 | 144 | return result; 145 | } 146 | }; 147 | 148 | export { object_util }; 149 | -------------------------------------------------------------------------------- /src/includes/utils/text.js: -------------------------------------------------------------------------------- 1 | import { WORD_DELIMITERS, WORD_PARTS_DELIMITERS } from "../constants"; 2 | import { collection_util } from "./collection"; 3 | 4 | /** 5 | * Утилиты для работы с текстом. 6 | */ 7 | 8 | var WORD_SPLITTER = new RegExp("[" + WORD_DELIMITERS + "]+", "g"); 9 | var WORD_PARTS_SPLITTER = new RegExp("[" + WORD_PARTS_DELIMITERS + "]+", "g"); 10 | 11 | var text_util = { 12 | /** 13 | * Заменяет амперсанд, угловые скобки и другие подобные символы 14 | * на HTML-коды 15 | */ 16 | escapeHtml: function(str) { 17 | var map = { 18 | "&": "&", 19 | "<": "<", 20 | ">": ">", 21 | '"': """, 22 | "'": "'", 23 | "/": "/" 24 | }; 25 | 26 | if (str) { 27 | collection_util.each(map, function(html, ch) { 28 | str = str.replace(new RegExp(ch, "g"), html); 29 | }); 30 | } 31 | return str; 32 | }, 33 | 34 | /** 35 | * Эскейпирует символы RegExp-шаблона обратным слешем 36 | * (для передачи в конструктор регулярных выражений) 37 | */ 38 | escapeRegExChars: function(value) { 39 | return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 40 | }, 41 | 42 | /** 43 | * Приводит слово к нижнему регистру и заменяет ё → е 44 | */ 45 | formatToken: function(token) { 46 | return token && token.toLowerCase().replace(/[ёЁ]/g, "е"); 47 | }, 48 | 49 | /** 50 | * Возвращает регулярное выражение для разбивки строки на слова 51 | */ 52 | getWordExtractorRegExp: function() { 53 | return new RegExp( 54 | "([^" + WORD_DELIMITERS + "]*)([" + WORD_DELIMITERS + "]*)", 55 | "g" 56 | ); 57 | }, 58 | 59 | /** 60 | * Вырезает из строки стоп-слова 61 | */ 62 | normalize: function(str, stopwords) { 63 | return text_util.split(str, stopwords).join(" "); 64 | }, 65 | 66 | /** 67 | * Добивает строку указанным символов справа до указанной длины 68 | * @param sourceString исходная строка 69 | * @param targetLength до какой длины добивать 70 | * @param padString каким символом добивать 71 | * @returns строка указанной длины 72 | */ 73 | padEnd: function(sourceString, targetLength, padString) { 74 | if (String.prototype.padEnd) { 75 | return sourceString.padEnd(targetLength, padString); 76 | } 77 | targetLength = targetLength >> 0; //floor if number or convert non-number to 0; 78 | padString = String(typeof padString !== "undefined" ? padString : " "); 79 | if (sourceString.length > targetLength) { 80 | return String(sourceString); 81 | } else { 82 | targetLength = targetLength - sourceString.length; 83 | if (targetLength > padString.length) { 84 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 85 | } 86 | return String(sourceString) + padString.slice(0, targetLength); 87 | } 88 | }, 89 | 90 | /** 91 | * Нормализует строку, разбивает на слова, 92 | * отсеивает стоп-слова из списка. 93 | * Расклеивает буквы и цифры, написанные слитно. 94 | */ 95 | split: function(str, stopwords) { 96 | var cleanStr = str 97 | .toLowerCase() 98 | .replace("ё", "е") 99 | .replace(/(\d+)([а-я]{2,})/g, "$1 $2") 100 | .replace(/([а-я]+)(\d+)/g, "$1 $2"); 101 | 102 | var words = collection_util.compact(cleanStr.split(WORD_SPLITTER)); 103 | if (!words.length) { 104 | return []; 105 | } 106 | var lastWord = words.pop(); 107 | var goodWords = collection_util.minus(words, stopwords); 108 | goodWords.push(lastWord); 109 | return goodWords; 110 | }, 111 | 112 | /** 113 | * Заменяет слова на составные части. 114 | * В отличие от withSubTokens, не сохраняет исходные слова. 115 | */ 116 | splitTokens: function(tokens) { 117 | var result = []; 118 | collection_util.each(tokens, function(token, i) { 119 | var subtokens = token.split(WORD_PARTS_SPLITTER); 120 | result = result.concat(collection_util.compact(subtokens)); 121 | }); 122 | return result; 123 | }, 124 | 125 | /** 126 | * Проверяет, включает ли строка 1 строку 2. 127 | * Если строки равны, возвращает false. 128 | */ 129 | stringEncloses: function(str1, str2) { 130 | return ( 131 | str1.length > str2.length && 132 | str1.toLowerCase().indexOf(str2.toLowerCase()) !== -1 133 | ); 134 | }, 135 | 136 | /** 137 | * Возвращает список слов из строки. 138 | * При этом первыми по порядку идут «предпочтительные» слова 139 | * (те, что не входят в список «нежелательных»). 140 | * Составные слова тоже разбивает на части. 141 | * @param {string} value - строка 142 | * @param {Array} unformattableTokens - «нежелательные» слова 143 | * @return {Array} Массив атомарных слов 144 | */ 145 | tokenize: function(value, unformattableTokens) { 146 | var tokens = collection_util.compact( 147 | text_util.formatToken(value).split(WORD_SPLITTER) 148 | ); 149 | // Move unformattableTokens to the end. 150 | // This will help to apply them only if no other tokens match 151 | var preferredTokens = collection_util.minus( 152 | tokens, 153 | unformattableTokens 154 | ); 155 | var otherTokens = collection_util.minus(tokens, preferredTokens); 156 | tokens = text_util.withSubTokens(preferredTokens.concat(otherTokens)); 157 | return tokens; 158 | }, 159 | 160 | /** 161 | * Разбивает составные слова на части 162 | * и дописывает их к исходному массиву. 163 | * @param {Array} tokens - слова 164 | * @return {Array} Массив атомарных слов 165 | */ 166 | withSubTokens: function(tokens) { 167 | var result = []; 168 | collection_util.each(tokens, function(token, i) { 169 | var subtokens = token.split(WORD_PARTS_SPLITTER); 170 | result.push(token); 171 | if (subtokens.length > 1) { 172 | result = result.concat(collection_util.compact(subtokens)); 173 | } 174 | }); 175 | return result; 176 | } 177 | }; 178 | 179 | export { text_util }; 180 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | import { Suggestions } from "./includes/suggestions"; 4 | 5 | import {} from "./includes/element"; 6 | import {} from "./includes/status"; 7 | import {} from "./includes/geolocation"; 8 | import {} from "./includes/enrich"; 9 | import {} from "./includes/container"; 10 | import {} from "./includes/constraints"; 11 | import {} from "./includes/select"; 12 | import {} from "./includes/bounds"; 13 | import {} from "./includes/promo"; 14 | 15 | import { DATA_ATTR_KEY } from "./includes/constants"; 16 | import { DEFAULT_OPTIONS } from "./includes/default-options"; 17 | 18 | Suggestions.defaultOptions = DEFAULT_OPTIONS; 19 | 20 | Suggestions.version = "%VERSION%"; 21 | 22 | $.Suggestions = Suggestions; 23 | 24 | // Create chainable jQuery plugin: 25 | $.fn.suggestions = function(options, args) { 26 | // If function invoked without argument return 27 | // instance of the first matched element: 28 | if (arguments.length === 0) { 29 | return this.first().data(DATA_ATTR_KEY); 30 | } 31 | 32 | return this.each(function() { 33 | var inputElement = $(this), 34 | instance = inputElement.data(DATA_ATTR_KEY); 35 | 36 | if (typeof options === "string") { 37 | if (instance && typeof instance[options] === "function") { 38 | instance[options](args); 39 | } 40 | } else { 41 | // If instance already exists, destroy it: 42 | if (instance && instance.dispose) { 43 | instance.dispose(); 44 | } 45 | instance = new Suggestions(this, options); 46 | inputElement.data(DATA_ATTR_KEY, instance); 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /test/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | this.helpers = function() { 2 | var helpers = { 3 | isHidden: function(el) { 4 | return el.offsetParent === null; 5 | }, 6 | keydown: function(el, keyCode) { 7 | var event = $.Event("keydown"); 8 | event.keyCode = event.which = keyCode; 9 | $(el).trigger(event); 10 | }, 11 | keyup: function(el, keyCode) { 12 | var event = $.Event("keyup"); 13 | event.keyCode = event.which = keyCode; 14 | $(el).trigger(event); 15 | }, 16 | click: function(el) { 17 | var event = $.Event("click"); 18 | $(el).trigger(event); 19 | }, 20 | responseFor: function(suggestions) { 21 | return [ 22 | 200, 23 | { "Content-type": "application/json" }, 24 | JSON.stringify({ 25 | suggestions: suggestions 26 | }) 27 | ]; 28 | }, 29 | hitEnter: function(el) { 30 | helpers.keydown(el, 13); // code of Enter 31 | }, 32 | fireBlur: function(el) { 33 | $(el).trigger($.Event("blur")); 34 | }, 35 | appendUnrestrictedValue: function(suggestion) { 36 | return $.extend({}, suggestion, { 37 | unrestricted_value: suggestion.value 38 | }); 39 | }, 40 | wrapFormattedValue: function(value, status) { 41 | return ( 42 | '" + 45 | value + 46 | "" 47 | ); 48 | }, 49 | returnStatus: function(server, status) { 50 | var urlPattern = "\\/status\\/(\\w)"; 51 | 52 | if (server.responses) { 53 | server.responses = $.grep(server.responses, function(response) { 54 | return !response.url || response.url.source !== urlPattern; 55 | }); 56 | } 57 | server.respond( 58 | "GET", 59 | new RegExp(urlPattern), 60 | JSON.stringify(status) 61 | ); 62 | }, 63 | returnGoodStatus: function(server) { 64 | helpers.returnStatus(server, { search: true, enrich: true }); 65 | }, 66 | returnPoorStatus: function(server) { 67 | helpers.returnStatus(server, { search: true, enrich: false }); 68 | } 69 | }; 70 | return helpers; 71 | }.call((typeof window != "undefined" && window) || {}); 72 | -------------------------------------------------------------------------------- /test/karma.full.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | browsers: ["PhantomJS"], 4 | frameworks: [ 5 | "jquery-3.3.1", 6 | "jasmine-jquery", 7 | "jasmine", 8 | "jasmine-sinon" 9 | ], 10 | files: [ 11 | "../dist/js/jquery.suggestions.js", 12 | "../dist/css/*.css", 13 | "helpers/helpers.js", 14 | "specs/*.js" 15 | ], 16 | plugins: ["@metahub/karma-jasmine-jquery", "karma-*"], 17 | reporters: ["spec"] 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test/karma.minified.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | browsers: ["PhantomJS"], 4 | frameworks: [ 5 | "jquery-3.3.1", 6 | "jasmine-jquery", 7 | "jasmine", 8 | "jasmine-sinon" 9 | ], 10 | files: [ 11 | "../dist/js/jquery.suggestions.min.js", 12 | "../dist/css/*.css", 13 | "helpers/helpers.js", 14 | "specs/*.js" 15 | ], 16 | plugins: ["@metahub/karma-jasmine-jquery", "karma-*"] 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /test/specs/add_space_on_select_spec.js: -------------------------------------------------------------------------------- 1 | describe("Adding space on selecting", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | describe("For NAME controls", function() { 8 | beforeEach(function() { 9 | $.Suggestions.resetTokens(); 10 | 11 | this.server = sinon.fakeServer.create(); 12 | 13 | this.input = document.createElement("input"); 14 | this.$input = $(this.input).appendTo($body); 15 | this.instance = this.$input 16 | .suggestions({ 17 | serviceUrl: serviceUrl, 18 | type: "NAME" 19 | }) 20 | .suggestions(); 21 | 22 | helpers.returnPoorStatus(this.server); 23 | }); 24 | 25 | afterEach(function() { 26 | this.server.restore(); 27 | this.instance.dispose(); 28 | this.$input.remove(); 29 | }); 30 | 31 | it("Should add SPACE at the end if only NAME specified", function() { 32 | this.input.value = "N"; 33 | this.instance.onValueChange(); 34 | this.server.respond( 35 | helpers.responseFor([ 36 | { 37 | value: "Name", 38 | data: { 39 | surname: null, 40 | name: "Name", 41 | patronymic: null, 42 | gender: "MALE" 43 | } 44 | } 45 | ]) 46 | ); 47 | 48 | this.instance.selectedIndex = 0; 49 | helpers.keydown(this.input, 13); 50 | 51 | expect(this.input.value).toEqual("Name "); 52 | }); 53 | 54 | it("Should add SPACE at the end if only SURNAME specified", function() { 55 | this.input.value = "S"; 56 | this.instance.onValueChange(); 57 | this.server.respond( 58 | helpers.responseFor([ 59 | { 60 | value: "Surname", 61 | data: { 62 | surname: "Surname", 63 | name: null, 64 | patronymic: null, 65 | gender: "MALE" 66 | } 67 | } 68 | ]) 69 | ); 70 | 71 | this.instance.selectedIndex = 0; 72 | helpers.keydown(this.input, 13); 73 | 74 | expect(this.input.value).toEqual("Surname "); 75 | }); 76 | 77 | it("Should add SPACE at the end if only NAME and PATRONYMIC specified", function() { 78 | this.input.value = "N"; 79 | this.instance.onValueChange(); 80 | this.server.respond( 81 | helpers.responseFor([ 82 | { 83 | value: "Name Patronymic", 84 | data: { 85 | surname: null, 86 | name: "Name", 87 | patronymic: "Patronymic", 88 | gender: "MALE" 89 | } 90 | } 91 | ]) 92 | ); 93 | 94 | this.instance.selectedIndex = 0; 95 | helpers.keydown(this.input, 13); 96 | 97 | expect(this.input.value).toEqual("Name Patronymic "); 98 | }); 99 | 100 | it("Should not add SPACE at the end if full name specified", function() { 101 | this.input.value = "S"; 102 | this.instance.onValueChange(); 103 | this.server.respond( 104 | helpers.responseFor([ 105 | { 106 | value: "Surname Name Patronymic", 107 | data: { 108 | surname: "Surname", 109 | name: "Name", 110 | patronymic: "Patronymic", 111 | gender: "MALE" 112 | } 113 | } 114 | ]) 115 | ); 116 | 117 | this.instance.selectedIndex = 0; 118 | helpers.keydown(this.input, 13); 119 | 120 | expect(this.input.value).toEqual("Surname Name Patronymic"); 121 | }); 122 | 123 | it("Should not add SPACE if only part expected", function() { 124 | this.instance.setOptions({ 125 | params: { 126 | parts: ["SURNAME"] 127 | } 128 | }); 129 | this.input.value = "S"; 130 | this.instance.onValueChange(); 131 | this.server.respond( 132 | helpers.responseFor([ 133 | { 134 | value: "Surname", 135 | data: { 136 | surname: "Surname", 137 | name: null, 138 | patronymic: null, 139 | gender: "UNKNOWN" 140 | } 141 | } 142 | ]) 143 | ); 144 | 145 | this.instance.selectedIndex = 0; 146 | helpers.keydown(this.input, 13); 147 | 148 | expect(this.input.value).toEqual("Surname"); 149 | }); 150 | 151 | it("Should not add SPACE if only part expected (params set as function)", function() { 152 | this.instance.setOptions({ 153 | params: function(query) { 154 | return { 155 | parts: ["SURNAME"] 156 | }; 157 | } 158 | }); 159 | this.input.value = "S"; 160 | this.instance.onValueChange(); 161 | this.server.respond( 162 | helpers.responseFor([ 163 | { 164 | value: "Surname", 165 | data: { 166 | surname: "Surname", 167 | name: null, 168 | patronymic: null, 169 | gender: "UNKNOWN" 170 | } 171 | } 172 | ]) 173 | ); 174 | 175 | this.instance.selectedIndex = 0; 176 | helpers.keydown(this.input, 13); 177 | 178 | expect(this.input.value).toEqual("Surname"); 179 | }); 180 | }); 181 | 182 | describe("For ADDRESS controls", function() { 183 | beforeEach(function() { 184 | $.Suggestions.resetTokens(); 185 | 186 | this.server = sinon.fakeServer.create(); 187 | 188 | this.input = document.createElement("input"); 189 | this.$input = $(this.input).appendTo($body); 190 | this.instance = this.$input 191 | .suggestions({ 192 | serviceUrl: serviceUrl, 193 | type: "ADDRESS", 194 | geoLocation: false, 195 | enrichmentEnabled: false 196 | }) 197 | .suggestions(); 198 | 199 | helpers.returnPoorStatus(this.server); 200 | }); 201 | 202 | afterEach(function() { 203 | this.instance.dispose(); 204 | this.server.restore(); 205 | this.$input.remove(); 206 | }); 207 | 208 | it("Should add SPACE at the end if only COUNTRY specified", function() { 209 | this.input.value = "Р"; 210 | this.instance.onValueChange(); 211 | this.server.respond( 212 | helpers.responseFor([ 213 | { 214 | value: "Россия", 215 | data: { 216 | country: "Россия" 217 | } 218 | } 219 | ]) 220 | ); 221 | 222 | this.instance.selectedIndex = 0; 223 | helpers.keydown(this.input, 13); 224 | 225 | expect(this.input.value).toEqual("Россия "); 226 | }); 227 | 228 | it("Should add SPACE at the end if COUNTRY..HOUSE specified", function() { 229 | this.input.value = "Р"; 230 | this.instance.onValueChange(); 231 | this.server.respond( 232 | helpers.responseFor([ 233 | { 234 | value: "Россия, г Москва, ул Арбат, д 1", 235 | data: { 236 | country: "Россия", 237 | city: "Москва", 238 | city_type: "г", 239 | street: "Арбат", 240 | street_type: "ул", 241 | house_type: "д", 242 | house: "1" 243 | } 244 | } 245 | ]) 246 | ); 247 | 248 | this.instance.selectedIndex = 0; 249 | helpers.hitEnter(this.input); 250 | 251 | expect(this.input.value).toEqual( 252 | "Россия, г Москва, ул Арбат, д 1 " 253 | ); 254 | }); 255 | 256 | it("Should not add SPACE at the end if FLAT specified", function() { 257 | this.input.value = "Р"; 258 | this.instance.onValueChange(); 259 | this.server.respond( 260 | helpers.responseFor([ 261 | { 262 | value: "Россия, г Москва, ул Арбат, д 1, кв 22", 263 | data: { 264 | country: "Россия", 265 | city: "Москва", 266 | city_type: "г", 267 | street: "Арбат", 268 | street_type: "ул", 269 | house: "1", 270 | house_type: "д", 271 | flat: "22", 272 | flat_type: "кв" 273 | } 274 | } 275 | ]) 276 | ); 277 | 278 | this.instance.selectedIndex = 0; 279 | helpers.hitEnter(this.input); 280 | 281 | expect(this.input.value).toEqual( 282 | "Россия, г Москва, ул Арбат, д 1, кв 22" 283 | ); 284 | }); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/specs/after_select_spec.js: -------------------------------------------------------------------------------- 1 | describe("After selecting", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url"; 5 | 6 | beforeEach(function() { 7 | $.Suggestions.resetTokens(); 8 | 9 | this.server = sinon.fakeServer.create(); 10 | 11 | this.input = document.createElement("input"); 12 | this.$input = $(this.input).appendTo($("body")); 13 | this.instance = this.$input 14 | .suggestions({ 15 | serviceUrl: serviceUrl, 16 | type: "NAME", 17 | enrichmentEnabled: false 18 | }) 19 | .suggestions(); 20 | 21 | helpers.returnPoorStatus(this.server); 22 | }); 23 | 24 | afterEach(function() { 25 | this.server.restore(); 26 | this.instance.dispose(); 27 | this.$input.remove(); 28 | }); 29 | 30 | it("Should hide dropdown if received suggestions contains only one suggestion equal to current", function() { 31 | var suggestions = [ 32 | { 33 | value: "Some value", 34 | data: null 35 | } 36 | ]; 37 | 38 | // show list 39 | this.input.value = "S"; 40 | this.instance.onValueChange(); 41 | this.server.respond(helpers.responseFor(suggestions)); 42 | 43 | spyOn(this.instance, "hide"); 44 | 45 | // select suggestion from list 46 | this.instance.selectedIndex = 0; 47 | helpers.hitEnter(this.input); 48 | 49 | // list is waiting for being updated 50 | this.server.respond(helpers.responseFor(suggestions)); 51 | 52 | expect(this.instance.hide).toHaveBeenCalled(); 53 | }); 54 | 55 | it("Should hide dropdown if selected NAME suggestion with all fields filled", function() { 56 | var suggestions = [ 57 | { 58 | value: "Surname Name Patronymic", 59 | data: { 60 | surname: "Surname", 61 | name: "Name", 62 | patronymic: "Patronymic", 63 | gender: "MALE" 64 | } 65 | } 66 | ]; 67 | 68 | this.input.value = "S"; 69 | this.instance.onValueChange(); 70 | this.server.respond(helpers.responseFor(suggestions)); 71 | 72 | spyOn(this.instance, "getSuggestions"); 73 | spyOn(this.instance, "hide"); 74 | 75 | this.instance.selectedIndex = 0; 76 | helpers.hitEnter(this.input); 77 | 78 | expect(this.instance.getSuggestions).not.toHaveBeenCalled(); 79 | expect(this.instance.hide).toHaveBeenCalled(); 80 | }); 81 | 82 | it("Should hide dropdown if selected NAME suggestion with name and surname filled for IOF", function() { 83 | var suggestions = [ 84 | { 85 | value: "Николай Александрович", 86 | data: { 87 | surname: "Александрович", 88 | name: "Николай", 89 | patronymic: null, 90 | gender: "MALE" 91 | } 92 | } 93 | ]; 94 | 95 | this.input.value = "Н"; 96 | this.instance.onValueChange(); 97 | this.server.respond(helpers.responseFor(suggestions)); 98 | 99 | spyOn(this.instance, "getSuggestions"); 100 | spyOn(this.instance, "hide"); 101 | 102 | this.instance.selectedIndex = 0; 103 | helpers.hitEnter(this.input); 104 | 105 | expect(this.instance.getSuggestions).not.toHaveBeenCalled(); 106 | expect(this.instance.hide).toHaveBeenCalled(); 107 | }); 108 | 109 | it("Should hide dropdown if selected ADDRESS suggestion with `house` field filled", function() { 110 | var suggestions = [ 111 | { 112 | value: "Россия, г Москва, ул Арбат, дом 10", 113 | data: { 114 | country: "Россия", 115 | city: "Москва", 116 | city_type: "г", 117 | street: "Арбат", 118 | street_type: "ул", 119 | house: "10", 120 | house_type: "дом" 121 | } 122 | } 123 | ]; 124 | 125 | this.instance.setOptions({ 126 | type: "ADDRESS", 127 | geoLocation: false 128 | }); 129 | helpers.returnPoorStatus(this.server); 130 | 131 | this.input.value = "Р"; 132 | this.instance.onValueChange(); 133 | this.server.respond(helpers.responseFor(suggestions)); 134 | 135 | spyOn(this.instance, "getSuggestions"); 136 | spyOn(this.instance, "hide"); 137 | 138 | this.instance.selectedIndex = 0; 139 | helpers.hitEnter(this.input); 140 | 141 | expect(this.instance.getSuggestions).not.toHaveBeenCalled(); 142 | expect(this.instance.hide).toHaveBeenCalled(); 143 | }); 144 | 145 | it("Should do nothing if select same suggestion twice", function() { 146 | var suggestion = { 147 | value: "Some value", 148 | data: {} 149 | }, 150 | options = { 151 | onSelect: $.noop 152 | }; 153 | 154 | spyOn(options, "onSelect"); 155 | 156 | this.instance.setOptions(options); 157 | 158 | // show list 159 | this.input.value = "S"; 160 | this.instance.onValueChange(); 161 | this.server.respond(helpers.responseFor([suggestion])); 162 | 163 | this.instance.setSuggestion(suggestion); 164 | 165 | // select suggestion from list 166 | this.instance.selectedIndex = 0; 167 | helpers.hitEnter(this.input); 168 | 169 | expect(options.onSelect).not.toHaveBeenCalled(); 170 | }); 171 | 172 | it("Should show hint if no suggestions received", function() { 173 | var suggestions = []; 174 | 175 | this.instance.setOptions({ 176 | type: "ADDRESS", 177 | geoLocation: false 178 | }); 179 | helpers.returnPoorStatus(this.server); 180 | spyOn(this.instance, "hide"); 181 | 182 | this.input.value = "Р"; 183 | this.instance.onValueChange(); 184 | this.server.respond(helpers.responseFor(suggestions)); 185 | 186 | var $hint = this.instance.$container.find(".suggestions-hint"); 187 | expect($hint.length).toEqual(1); 188 | expect(this.instance.hide).not.toHaveBeenCalled(); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test/specs/autoselect_spec.js: -------------------------------------------------------------------------------- 1 | describe("Autoselect", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | this.server = sinon.fakeServer.create(); 9 | this.input = document.createElement("input"); 10 | this.$input = $(this.input).appendTo($body); 11 | this.instance = this.$input 12 | .suggestions({ 13 | serviceUrl: serviceUrl, 14 | type: "ADDRESS", 15 | geoLocation: false 16 | }) 17 | .suggestions(); 18 | }); 19 | 20 | afterEach(function() { 21 | this.instance.dispose(); 22 | this.$input.remove(); 23 | this.server.restore(); 24 | }); 25 | 26 | it("Should not autoselect first item by default", function() { 27 | this.instance.selectedIndex = -1; 28 | 29 | this.input.value = "Jam"; 30 | this.instance.onValueChange(); 31 | this.server.respond( 32 | helpers.responseFor(["Jamaica", "Jamaica", "Jamaica"]) 33 | ); 34 | 35 | expect(this.instance.selectedIndex).toBe(-1); 36 | }); 37 | 38 | it("Should autoselect first item if autoSelectFirst set to true", function() { 39 | this.instance.setOptions({ 40 | autoSelectFirst: true 41 | }); 42 | this.instance.selectedIndex = -1; 43 | 44 | this.input.value = "Jam"; 45 | this.instance.onValueChange(); 46 | this.server.respond( 47 | helpers.responseFor(["Jamaica", "Jamaica", "Jamaica"]) 48 | ); 49 | 50 | expect(this.instance.selectedIndex).toBe(0); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/specs/bounds.js: -------------------------------------------------------------------------------- 1 | describe("Bounds", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | 10 | this.server = sinon.fakeServer.create(); 11 | 12 | this.input = document.createElement("input"); 13 | this.$input = $(this.input).appendTo($body); 14 | this.instance = this.$input 15 | .suggestions({ 16 | serviceUrl: serviceUrl, 17 | type: "ADDRESS", 18 | geoLocation: false, 19 | // disable mobile view features 20 | mobileWidth: NaN 21 | }) 22 | .suggestions(); 23 | 24 | helpers.returnGoodStatus(this.server); 25 | this.server.requests.length = 0; 26 | }); 27 | 28 | afterEach(function() { 29 | this.instance.dispose(); 30 | this.$input.remove(); 31 | this.server.restore(); 32 | }); 33 | 34 | it("Should include `bounds` option into request, if it is a range", function() { 35 | this.instance.setOptions({ 36 | bounds: "city-street" 37 | }); 38 | 39 | this.input.value = "Jam"; 40 | this.instance.onValueChange(); 41 | 42 | expect(this.server.requests[0].requestBody).toContain( 43 | '"from_bound":{"value":"city"}' 44 | ); 45 | expect(this.server.requests[0].requestBody).toContain( 46 | '"to_bound":{"value":"street"}' 47 | ); 48 | }); 49 | 50 | it("Should include `bounds` option into request, if it is a single value", function() { 51 | this.instance.setOptions({ 52 | bounds: "city" 53 | }); 54 | 55 | this.input.value = "Jam"; 56 | this.instance.onValueChange(); 57 | 58 | expect(this.server.requests[0].requestBody).toContain( 59 | '"from_bound":{"value":"city"}' 60 | ); 61 | expect(this.server.requests[0].requestBody).toContain( 62 | '"to_bound":{"value":"city"}' 63 | ); 64 | }); 65 | 66 | it("Should include `bounds` option into request, if it is an open range", function() { 67 | this.instance.setOptions({ 68 | bounds: "street-" 69 | }); 70 | 71 | this.input.value = "Jam"; 72 | this.instance.onValueChange(); 73 | 74 | expect(this.server.requests[0].requestBody).toContain( 75 | '"from_bound":{"value":"street"}' 76 | ); 77 | expect(this.server.requests[0].requestBody).not.toContain( 78 | '"to_bound":' 79 | ); 80 | }); 81 | 82 | it("Should treat country as valid single bound", function() { 83 | this.instance.setOptions({ 84 | bounds: "country" 85 | }); 86 | 87 | this.input.value = "Jam"; 88 | this.instance.onValueChange(); 89 | 90 | expect(this.server.requests[0].requestBody).toContain( 91 | '"from_bound":{"value":"country"}' 92 | ); 93 | expect(this.server.requests[0].requestBody).toContain( 94 | '"to_bound":{"value":"country"}' 95 | ); 96 | }); 97 | 98 | it("Should treat country as valid part of range bound", function() { 99 | this.instance.setOptions({ 100 | bounds: "country-city" 101 | }); 102 | 103 | this.input.value = "Jam"; 104 | this.instance.onValueChange(); 105 | 106 | expect(this.server.requests[0].requestBody).toContain( 107 | '"from_bound":{"value":"country"}' 108 | ); 109 | expect(this.server.requests[0].requestBody).toContain( 110 | '"to_bound":{"value":"city"}' 111 | ); 112 | }); 113 | 114 | it("Should modify suggestion according to `bounds`", function() { 115 | this.instance.setOptions({ 116 | bounds: "city-settlement" 117 | }); 118 | 119 | this.instance.setSuggestion({ 120 | value: 121 | "Тульская обл, Узловский р-н, г Узловая, поселок Брусянский, ул Строителей, д 1-бара", 122 | unrestricted_value: 123 | "Тульская обл, Узловский р-н, г Узловая, поселок Брусянский, ул Строителей, д 1-бара", 124 | data: { 125 | country: "Россия", 126 | region_type: "обл", 127 | region_type_full: "область", 128 | region: "Тульская", 129 | region_with_type: "Тульская обл", 130 | area_type: "р-н", 131 | area_type_full: "район", 132 | area: "Узловский", 133 | area_with_type: "Узловский р-н", 134 | city_type: "г", 135 | city_type_full: "город", 136 | city: "Узловая", 137 | city_with_type: "г Узловая", 138 | settlement_type: "п", 139 | settlement_type_full: "поселок", 140 | settlement: "Брусянский", 141 | settlement_with_type: "поселок Брусянский", 142 | street_type: "ул", 143 | street_type_full: "улица", 144 | street: "Строителей", 145 | street_with_type: "ул Строителей", 146 | house_type: "д", 147 | house_type_full: "дом", 148 | house: "1-бара", 149 | kladr_id: "7102200100200310001" 150 | } 151 | }); 152 | 153 | expect(this.$input.val()).toEqual("г Узловая, поселок Брусянский"); 154 | expect(this.instance.selection.data.street).toBeUndefined(); 155 | expect(this.instance.selection.data.kladr_id).toEqual("7102200100200"); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/specs/constraint_location_spec.js: -------------------------------------------------------------------------------- 1 | describe("Constraint Location", function() { 2 | beforeEach(function() { 3 | this.server = sinon.fakeServer.create(); 4 | 5 | this.$input = $(document.createElement("input")).appendTo( 6 | document.body 7 | ); 8 | this.instance = this.$input 9 | .suggestions({ 10 | serviceUrl: "service/url", 11 | type: "ADDRESS", 12 | geoLocation: false 13 | }) 14 | .suggestions(); 15 | }); 16 | 17 | afterEach(function() { 18 | this.instance.dispose(); 19 | this.$input.remove(); 20 | this.server.restore(); 21 | }); 22 | 23 | it("should skip unnecessary fields", function() { 24 | var location = new $.Suggestions.ConstraintLocation( 25 | { 26 | country: "Россия", 27 | region: "Самарская", 28 | region_type: "обл", 29 | postal_code: "445020", 30 | okato: "36440373000" 31 | }, 32 | this.instance 33 | ); 34 | 35 | expect(location.getFields()).toEqual({ 36 | country: "Россия", 37 | region: "Самарская", 38 | postal_code: "445020" 39 | }); 40 | }); 41 | 42 | it("should use kladr id if specified", function() { 43 | var location = new $.Suggestions.ConstraintLocation( 44 | { 45 | city: "Тольятти", 46 | kladr_id: "6300000700000" 47 | }, 48 | this.instance 49 | ); 50 | 51 | expect(Object.keys(location.getFields())).toEqual(["kladr_id"]); 52 | }); 53 | 54 | // наличие фиас параметров если переданы 55 | it("should use fias id if specified", function() { 56 | var location = new $.Suggestions.ConstraintLocation( 57 | { 58 | city: "Тольятти", 59 | kladr_id: "6300000700000", 60 | city_fias_id: "1000000", 61 | street_fias_id: "1000000" 62 | }, 63 | this.instance 64 | ); 65 | 66 | expect(Object.keys(location.getFields())).toEqual([ 67 | "city_fias_id", 68 | "street_fias_id" 69 | ]); 70 | }); 71 | 72 | it("should determine specificity", function() { 73 | var location = new $.Suggestions.ConstraintLocation( 74 | { 75 | city: "Тольятти" 76 | }, 77 | this.instance 78 | ), 79 | expectedSpecificity = 13; 80 | 81 | expect( 82 | this.instance.type.dataComponents[expectedSpecificity].id 83 | ).toEqual("city"); 84 | expect(location.specificity).toEqual(expectedSpecificity); 85 | }); 86 | 87 | it("should determine specificity of kladr_id #1", function() { 88 | var location = new $.Suggestions.ConstraintLocation( 89 | { 90 | kladr_id: "6300000700000" 91 | }, 92 | this.instance 93 | ), 94 | expectedSpecificity = 13; 95 | 96 | expect( 97 | this.instance.type.dataComponents[expectedSpecificity].id 98 | ).toEqual("city"); 99 | expect(location.specificity).toEqual(expectedSpecificity); 100 | expect(location.significantKladr).toEqual("63000007"); 101 | }); 102 | 103 | it("should determine specificity of kladr_id #2", function() { 104 | var location = new $.Suggestions.ConstraintLocation( 105 | { 106 | kladr_id: "5000004000000" 107 | }, 108 | this.instance 109 | ), 110 | expectedSpecificity = 13; 111 | 112 | expect( 113 | this.instance.type.dataComponents[expectedSpecificity].id 114 | ).toEqual("city"); 115 | expect(location.specificity).toEqual(expectedSpecificity); 116 | expect(location.significantKladr).toEqual("50000040"); 117 | }); 118 | 119 | it("should determine specificity of kladr_id #3", function() { 120 | var location = new $.Suggestions.ConstraintLocation( 121 | { 122 | kladr_id: "50000040000016000" 123 | }, 124 | this.instance 125 | ), 126 | expectedSpecificity = 22; 127 | 128 | expect( 129 | this.instance.type.dataComponents[expectedSpecificity].id 130 | ).toEqual("street"); 131 | expect(location.specificity).toEqual(expectedSpecificity); 132 | expect(location.significantKladr).toEqual("500000400000160"); 133 | }); 134 | 135 | it("should determine suggestion data includes", function() { 136 | var location = new $.Suggestions.ConstraintLocation( 137 | { 138 | country: "россия", 139 | region: "самарская" 140 | }, 141 | this.instance 142 | ); 143 | 144 | expect( 145 | location.containsData({ 146 | postal_code: "445000", 147 | country: "Россия", 148 | region_fias_id: "df3d7359-afa9-4aaa-8ff9-197e73906b1c", 149 | region_kladr_id: "6300000000000", 150 | region_with_type: "Самарская обл", 151 | region_type: "обл", 152 | region_type_full: "область", 153 | region: "Самарская", 154 | city_fias_id: "242e87c1-584d-4360-8c4c-aae2fe90048e", 155 | city_kladr_id: "6300000700000", 156 | city_with_type: "г Тольятти", 157 | city_type: "г", 158 | city_type_full: "город", 159 | city: "Тольятти", 160 | city_district_with_type: "Центральный р-н", 161 | city_district_type: "р-н", 162 | city_district_type_full: "район", 163 | city_district: "Центральный", 164 | street_fias_id: "b3631886-22ac-4852-a1cb-c60b222888cf", 165 | street_kladr_id: "63000007000028700", 166 | street_with_type: "ул Ленинградская", 167 | street_type: "ул", 168 | street_type_full: "улица", 169 | street: "Ленинградская", 170 | fias_id: "b3631886-22ac-4852-a1cb-c60b222888cf", 171 | fias_level: "7", 172 | kladr_id: "63000007000028700", 173 | capital_marker: "0", 174 | okato: "36440373000", 175 | oktmo: "36740000", 176 | tax_office: "6324", 177 | geo_lat: "53.505569", 178 | geo_lon: "49.4110871" 179 | }) 180 | ).toBe(true); 181 | expect( 182 | location.containsData({ 183 | postal_code: "445000", 184 | country: "Россия", 185 | region: null 186 | }) 187 | ).toBe(false); 188 | }); 189 | 190 | it("should determine suggestion data includes by kladr_id", function() { 191 | var location = new $.Suggestions.ConstraintLocation( 192 | { 193 | kladr_id: "6300000700000" 194 | }, 195 | this.instance 196 | ); 197 | 198 | expect( 199 | location.containsData({ 200 | postal_code: "445000", 201 | country: "Россия", 202 | region_kladr_id: "6300000000000", 203 | region_with_type: "Самарская обл", 204 | city_kladr_id: "6300000700000", 205 | city_with_type: "г Тольятти", 206 | city_district_with_type: "Центральный р-н", 207 | street_kladr_id: "63000007000028700", 208 | street_with_type: "ул Ленинградская", 209 | fias_id: "b3631886-22ac-4852-a1cb-c60b222888cf", 210 | fias_level: "7", 211 | kladr_id: "63000007000028700" 212 | }) 213 | ).toBe(true); 214 | 215 | // Data without kladr_id 216 | expect( 217 | location.containsData({ 218 | postal_code: "445000", 219 | country: "Россия", 220 | region: "самарская" 221 | }) 222 | ).toBe(false); 223 | 224 | // Data with wrong kladr_id 225 | expect( 226 | location.containsData({ 227 | // Some street from Samara 228 | kladr_id: "63000001000000100" 229 | }) 230 | ).toBe(false); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /test/specs/email_spec.js: -------------------------------------------------------------------------------- 1 | describe("Email", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | 10 | this.server = sinon.fakeServer.create(); 11 | 12 | this.input = document.createElement("input"); 13 | this.$input = $(this.input).appendTo($body); 14 | this.instance = this.$input 15 | .suggestions({ 16 | serviceUrl: serviceUrl, 17 | type: "EMAIL", 18 | // disable mobile view features 19 | mobileWidth: NaN 20 | }) 21 | .suggestions(); 22 | 23 | helpers.returnGoodStatus(this.server); 24 | this.server.requests.length = 0; 25 | }); 26 | 27 | afterEach(function() { 28 | this.instance.dispose(); 29 | this.$input.remove(); 30 | this.server.restore(); 31 | }); 32 | 33 | it("Should not request until @ typed", function() { 34 | this.instance.setOptions({ 35 | suggest_local: false 36 | }); 37 | 38 | this.input.value = "jam"; 39 | this.instance.onValueChange(); 40 | 41 | expect(this.server.requests.length).toEqual(0); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/specs/events_spec.js: -------------------------------------------------------------------------------- 1 | describe("Element events", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url"; 5 | 6 | beforeEach(function() { 7 | $.Suggestions.resetTokens(); 8 | 9 | this.server = sinon.fakeServer.create(); 10 | 11 | this.input = document.createElement("input"); 12 | this.$input = $(this.input).appendTo("body"); 13 | this.instance = this.$input 14 | .suggestions({ 15 | serviceUrl: serviceUrl, 16 | type: "NAME" 17 | }) 18 | .suggestions(); 19 | 20 | helpers.returnGoodStatus(this.server); 21 | }); 22 | 23 | afterEach(function() { 24 | this.instance.dispose(); 25 | this.$input.remove(); 26 | this.server.restore(); 27 | }); 28 | 29 | it("`suggestions-select` should be triggered", function() { 30 | var suggestion = { value: "A", data: "B" }, 31 | eventArgs = []; 32 | 33 | this.$input.on("suggestions-select", function(e, sug) { 34 | eventArgs.push(sug); 35 | }); 36 | 37 | this.input.value = "A"; 38 | this.instance.onValueChange(); 39 | this.server.respond(helpers.responseFor([suggestion])); 40 | this.instance.select(0); 41 | 42 | expect(eventArgs).toEqual([ 43 | helpers.appendUnrestrictedValue(suggestion) 44 | ]); 45 | }); 46 | 47 | it("`suggestions-selectnothing` should be triggered", function() { 48 | var eventArgs = []; 49 | 50 | this.$input.on("suggestions-selectnothing", function(e, val) { 51 | eventArgs.push(val); 52 | }); 53 | 54 | this.instance.selectedIndex = -1; 55 | 56 | this.input.value = "A"; 57 | this.instance.onValueChange(); 58 | helpers.hitEnter(this.input); 59 | 60 | expect(eventArgs).toEqual(["A"]); 61 | }); 62 | 63 | it("`suggestions-invalidateselection` should be triggered", function() { 64 | var suggestion = { value: "A", data: "B" }, 65 | eventArgs = []; 66 | 67 | this.$input.on("suggestions-invalidateselection", function(e, val) { 68 | eventArgs.push(val); 69 | }); 70 | 71 | this.input.value = "A"; 72 | this.instance.onValueChange(); 73 | this.server.respond(helpers.responseFor([suggestion])); 74 | this.instance.select(0); 75 | 76 | this.input.value = "Aaaa"; 77 | this.instance.onValueChange(); 78 | helpers.hitEnter(this.input); 79 | 80 | expect(eventArgs).toEqual([ 81 | helpers.appendUnrestrictedValue(suggestion) 82 | ]); 83 | }); 84 | 85 | it("`suggestions-dispose` should be triggered", function() { 86 | var $parent = $("").appendTo($("body")); 87 | 88 | $parent.suggestions({ 89 | type: "ADDRESS", 90 | serviceUrl: serviceUrl, 91 | geoLocation: false 92 | }); 93 | 94 | spyOn(this.instance, "onParentDispose"); 95 | 96 | this.instance.setOptions({ 97 | constraints: $parent 98 | }); 99 | 100 | $parent.suggestions().dispose(); 101 | $parent.remove(); 102 | 103 | expect(this.instance.onParentDispose).toHaveBeenCalled(); 104 | }); 105 | 106 | it("`suggestions-set` should be triggered", function() { 107 | // this.$input is different with this.instance.el, thought contains same element 108 | var $input = this.instance.el; 109 | spyOn($input, "trigger"); 110 | 111 | this.instance.setSuggestion({ 112 | value: "somethind", 113 | data: {} 114 | }); 115 | 116 | expect($input.trigger).toHaveBeenCalledWith("suggestions-set"); 117 | }); 118 | 119 | it("`suggestions-fixdata` should be triggered", function() { 120 | // this.$input is different with this.instance.el, thought contains same element 121 | var $input = this.instance.el; 122 | spyOn($input, "trigger"); 123 | 124 | this.input.value = "г Москва"; 125 | this.instance.fixData(); 126 | 127 | this.server.respond("GET", /address/, [ 128 | 200, 129 | { "Content-type": "application/json" }, 130 | JSON.stringify([ 131 | { 132 | value: "г Москва", 133 | data: {} 134 | } 135 | ]) 136 | ]); 137 | 138 | expect($input.trigger).toHaveBeenCalledWith("suggestions-fixdata"); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/specs/fias_spec.js: -------------------------------------------------------------------------------- 1 | describe("Fias", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | 10 | this.server = sinon.fakeServer.create(); 11 | 12 | this.input = document.createElement("input"); 13 | this.$input = $(this.input).appendTo($body); 14 | this.instance = this.$input 15 | .suggestions({ 16 | serviceUrl: serviceUrl, 17 | type: "FIAS", 18 | geoLocation: false, 19 | // disable mobile view features 20 | mobileWidth: NaN 21 | }) 22 | .suggestions(); 23 | 24 | helpers.returnGoodStatus(this.server); 25 | this.server.requests.length = 0; 26 | }); 27 | 28 | afterEach(function() { 29 | this.instance.dispose(); 30 | this.$input.remove(); 31 | this.server.restore(); 32 | }); 33 | 34 | it("Should support planning structure in locations", function() { 35 | this.instance.setOptions({ 36 | constraints: { 37 | locations: [{ planning_structure_fias_id: "123" }] 38 | } 39 | }); 40 | 41 | this.input.value = "Jam"; 42 | this.instance.onValueChange(); 43 | 44 | expect(this.server.requests[0].requestBody).toContain( 45 | '"locations":[{"planning_structure_fias_id":"123"}' 46 | ); 47 | }); 48 | 49 | it("Should support planning structure in bounds", function() { 50 | this.instance.setOptions({ 51 | bounds: "planning_structure" 52 | }); 53 | 54 | this.input.value = "Jam"; 55 | this.instance.onValueChange(); 56 | 57 | expect(this.server.requests[0].requestBody).toContain( 58 | '"from_bound":{"value":"planning_structure"}' 59 | ); 60 | expect(this.server.requests[0].requestBody).toContain( 61 | '"to_bound":{"value":"planning_structure"}' 62 | ); 63 | }); 64 | 65 | it("Should not iplocate", function() { 66 | this.server.requests.length = 0; 67 | 68 | this.$input.suggestions({ 69 | serviceUrl: serviceUrl, 70 | type: "FIAS" 71 | }); 72 | 73 | expect(this.server.requests.length).toEqual(0); 74 | }); 75 | 76 | it("Should not enrich", function() { 77 | // select address 78 | this.input.value = "Р"; 79 | this.instance.onValueChange(); 80 | this.server.respond( 81 | helpers.responseFor([ 82 | { 83 | value: "Москва", 84 | data: { 85 | city: "Москва", 86 | qc: null 87 | } 88 | } 89 | ]) 90 | ); 91 | 92 | this.server.requests.length = 0; 93 | this.instance.selectedIndex = 0; 94 | helpers.hitEnter(this.input); 95 | 96 | // request for enriched suggestion not sent 97 | expect(this.server.requests.length).toEqual(0); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/specs/fix_data_spec.js: -------------------------------------------------------------------------------- 1 | describe("FixData", function() { 2 | "use strict"; 3 | 4 | beforeEach(function() { 5 | $.Suggestions.resetTokens(); 6 | 7 | this.server = sinon.fakeServer.create(); 8 | 9 | this.input = document.createElement("input"); 10 | this.$input = $(this.input).appendTo("body"); 11 | this.instance = this.$input 12 | .suggestions({ 13 | type: "ADDRESS" 14 | }) 15 | .suggestions(); 16 | 17 | helpers.returnGoodStatus(this.server); 18 | }); 19 | 20 | afterEach(function() { 21 | this.instance.dispose(); 22 | this.$input.remove(); 23 | this.server.restore(); 24 | }); 25 | 26 | it("should not clear value on fixData", function() { 27 | var value = "Санкт-Петербург, ул. Софийская, д.35, корп.4, кв.81"; 28 | this.input.value = value; 29 | 30 | this.$input.suggestions().fixData(); 31 | this.server.respond(helpers.responseFor([])); 32 | 33 | expect(this.input.value).toEqual(value); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/specs/geolocation_spec.js: -------------------------------------------------------------------------------- 1 | describe("Geolocation", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url"; 5 | 6 | beforeEach(function() { 7 | $.Suggestions.resetLocation(); 8 | $.Suggestions.resetTokens(); 9 | this.server = sinon.fakeServer.create(); 10 | 11 | this.input = document.createElement("input"); 12 | this.$input = $(this.input).appendTo("body"); 13 | this.instance = this.$input 14 | .suggestions({ 15 | serviceUrl: serviceUrl, 16 | type: "ADDRESS" 17 | }) 18 | .suggestions(); 19 | 20 | // First request gets service status info 21 | this.server.requests.shift().respond([ 22 | 200, 23 | { "Content-type": "application/json" }, 24 | JSON.stringify({ 25 | enrich: true, 26 | name: "address", 27 | search: true, 28 | state: "ENABLED" 29 | }) 30 | ]); 31 | this.server.queue.shift(); 32 | }); 33 | 34 | afterEach(function() { 35 | this.instance.dispose(); 36 | this.$input.remove(); 37 | this.server.restore(); 38 | $.Suggestions.resetTokens(); 39 | $.Suggestions.resetLocation(); 40 | }); 41 | 42 | it("Should send geolocation request if no `geoLocation` option specified", function() { 43 | expect(this.server.requests.length).toEqual(1); 44 | expect(this.server.requests[0].url).toContain("iplocate/address"); 45 | }); 46 | 47 | it("Should send geolocation request for party", function() { 48 | $.Suggestions.resetLocation(); 49 | this.server.requests.length = 0; 50 | this.server.respond("GET", /status\/party/, [ 51 | 200, 52 | { "Content-type": "application/json" }, 53 | JSON.stringify({ 54 | enrich: false, 55 | name: "party", 56 | search: true, 57 | state: "ENABLED" 58 | }) 59 | ]); 60 | this.$input.suggestions({ 61 | serviceUrl: serviceUrl, 62 | type: "PARTY" 63 | }); 64 | expect(this.server.requests[1].url).toContain("iplocate/address"); 65 | }); 66 | 67 | it("Should send geolocation request for bank", function() { 68 | $.Suggestions.resetLocation(); 69 | this.server.requests.length = 0; 70 | this.server.respond("GET", /status\/bank/, [ 71 | 200, 72 | { "Content-type": "application/json" }, 73 | JSON.stringify({ 74 | enrich: false, 75 | name: "bank", 76 | search: true, 77 | state: "ENABLED" 78 | }) 79 | ]); 80 | this.$input.suggestions({ 81 | serviceUrl: serviceUrl, 82 | type: "BANK" 83 | }); 84 | expect(this.server.requests[1].url).toContain("iplocate/address"); 85 | }); 86 | 87 | it("Should send location with request", function() { 88 | this.server.respond("GET", /iplocate\/address/, [ 89 | 200, 90 | { "Content-type": "application/json" }, 91 | JSON.stringify({ 92 | location: { 93 | data: { 94 | region: "Москва", 95 | kladr_id: "7700000000000" 96 | }, 97 | value: "1.2.3.4" 98 | } 99 | }) 100 | ]); 101 | 102 | this.input.value = "A"; 103 | this.instance.onValueChange(); 104 | 105 | expect(this.server.requests[1].requestBody).toContain( 106 | '"locations_boost":[{"kladr_id":"7700000000000"}]' 107 | ); 108 | }); 109 | 110 | it("Should not send geolocation request if `geoLocation` set to false", function() { 111 | this.server.requests.length = 0; 112 | 113 | this.$input.suggestions({ 114 | serviceUrl: serviceUrl, 115 | type: "ADDRESS", 116 | geoLocation: false 117 | }); 118 | 119 | expect(this.server.requests.length).toEqual(0); 120 | }); 121 | 122 | it("Should not send geolocation request if `geoLocation` set as object", function() { 123 | this.server.requests.length = 0; 124 | 125 | this.$input.suggestions({ 126 | serviceUrl: serviceUrl, 127 | type: "ADDRESS", 128 | geoLocation: { 129 | kladr_id: 83 130 | } 131 | }); 132 | 133 | expect(this.server.requests.length).toEqual(0); 134 | }); 135 | 136 | it("Should send location set by `geoLocation` option as object", function() { 137 | this.$input.suggestions({ 138 | serviceUrl: serviceUrl, 139 | type: "ADDRESS", 140 | geoLocation: { 141 | kladr_id: "83" 142 | } 143 | }); 144 | 145 | this.$input.val("A"); 146 | this.$input.suggestions("onValueChange"); 147 | 148 | expect(this.server.requests[1].requestBody).toContain( 149 | '"locations_boost":[{"kladr_id":"83"}]' 150 | ); 151 | }); 152 | 153 | it("Should send location set by `geoLocation` option as array", function() { 154 | this.$input.suggestions({ 155 | serviceUrl: serviceUrl, 156 | type: "ADDRESS", 157 | geoLocation: [{ kladr_id: "77" }, { kladr_id: "50" }] 158 | }); 159 | 160 | this.$input.val("A"); 161 | this.$input.suggestions("onValueChange"); 162 | 163 | expect(this.server.requests[1].requestBody).toContain( 164 | '"locations_boost":[{"kladr_id":"77"},{"kladr_id":"50"}]' 165 | ); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/specs/initialization_specs.js: -------------------------------------------------------------------------------- 1 | describe("Initialization", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | /** 8 | * Just a wrapper for a bunch of specs to check that instance is completely initialized 9 | * Wherever called, runs these specs in current environment 10 | */ 11 | function checkInitialized() { 12 | it("Should request service status", function() { 13 | expect(this.server.requests.length).toBe(1); 14 | expect(this.server.requests[0].url).toContain("/status/fio"); 15 | }); 16 | 17 | it("Should create all additional components", function() { 18 | var instance = this.instance; 19 | $.each(["$wrapper", "$container"], function(i, component) { 20 | expect(instance[component].length).toEqual(1); 21 | }); 22 | }); 23 | } 24 | 25 | /** 26 | * Checks that instance has not been completely initialized 27 | */ 28 | function checkNotInitialized() { 29 | it("Should not send anything to server", function() { 30 | expect(this.server.requests.length).toBe(0); 31 | }); 32 | 33 | it("Should not create any additional elements", function() { 34 | expect(this.instance.$wrapper).toBeNull(); 35 | }); 36 | } 37 | 38 | beforeEach(function() { 39 | $.Suggestions.resetTokens(); 40 | 41 | this.server = sinon.fakeServer.create(); 42 | }); 43 | 44 | afterEach(function() { 45 | this.server.restore(); 46 | }); 47 | 48 | describe("visible element", function() { 49 | beforeEach(function() { 50 | this.input = document.createElement("input"); 51 | this.$input = $(this.input).appendTo($body); 52 | this.instance = this.$input 53 | .suggestions({ 54 | serviceUrl: serviceUrl, 55 | type: "NAME" 56 | }) 57 | .suggestions(); 58 | 59 | helpers.returnGoodStatus(this.server); 60 | }); 61 | 62 | afterEach(function() { 63 | this.instance.dispose(); 64 | this.$input.remove(); 65 | }); 66 | 67 | it("Should initialize suggestions options", function() { 68 | expect(this.instance.options.serviceUrl).toEqual(serviceUrl); 69 | }); 70 | 71 | checkInitialized(); 72 | }); 73 | 74 | describe("check defaults", function() { 75 | beforeEach(function() { 76 | this.input = document.createElement("input"); 77 | this.$input = $(this.input).appendTo($body); 78 | this.instance = this.$input 79 | .suggestions({ 80 | type: "NAME" 81 | }) 82 | .suggestions(); 83 | 84 | helpers.returnGoodStatus(this.server); 85 | }); 86 | 87 | afterEach(function() { 88 | this.instance.dispose(); 89 | this.$input.remove(); 90 | }); 91 | 92 | it("serviceUrl", function() { 93 | expect(this.instance.options.serviceUrl).toEqual( 94 | $.Suggestions.defaultOptions.serviceUrl 95 | ); 96 | }); 97 | 98 | checkInitialized(); 99 | }); 100 | 101 | describe("check custom options", function() { 102 | beforeEach(function() { 103 | this.input = document.createElement("input"); 104 | this.$input = $(this.input).appendTo($body); 105 | this.instance = this.$input 106 | .suggestions({ 107 | type: "NAME", 108 | serviceUrl: "http://domain.com" 109 | }) 110 | .suggestions(); 111 | 112 | helpers.returnGoodStatus(this.server); 113 | }); 114 | 115 | afterEach(function() { 116 | this.instance.dispose(); 117 | this.$input.remove(); 118 | }); 119 | 120 | it("serviceUrl", function() { 121 | expect(this.instance.options.serviceUrl).toEqual( 122 | "http://domain.com" 123 | ); 124 | }); 125 | 126 | checkInitialized(); 127 | }); 128 | 129 | describe("hidden element", function() { 130 | // create input, but do not add it to DOM 131 | beforeEach(function() { 132 | jasmine.clock().install(); 133 | 134 | this.input = document.createElement("input"); 135 | this.$input = $(this.input); 136 | this.instance = this.$input 137 | .suggestions({ 138 | serviceUrl: serviceUrl, 139 | type: "NAME" 140 | }) 141 | .suggestions(); 142 | 143 | helpers.returnGoodStatus(this.server); 144 | }); 145 | 146 | afterEach(function() { 147 | this.instance.dispose(); 148 | jasmine.clock().uninstall(); 149 | }); 150 | 151 | it("Should initialize suggestions options", function() { 152 | expect(this.instance.options.serviceUrl).toEqual(serviceUrl); 153 | }); 154 | 155 | checkNotInitialized(); 156 | 157 | describe("after showed", function() { 158 | beforeEach(function() { 159 | this.$input.appendTo($body); 160 | }); 161 | 162 | afterEach(function() { 163 | this.$input.remove(); 164 | }); 165 | 166 | checkNotInitialized(); 167 | 168 | describe("and interacted by keyboard", function() { 169 | beforeEach(function() { 170 | helpers.keydown(this.input, 32); 171 | }); 172 | 173 | checkInitialized(); 174 | }); 175 | 176 | describe("and interacted by mouse", function() { 177 | beforeEach(function() { 178 | this.$input.mouseover(); 179 | }); 180 | 181 | checkInitialized(); 182 | }); 183 | 184 | describe("and `initializeInterval` expired", function() { 185 | beforeEach(function() { 186 | jasmine 187 | .clock() 188 | .tick( 189 | $.Suggestions.defaultOptions.initializeInterval + 1 190 | ); 191 | }); 192 | 193 | checkInitialized(); 194 | }); 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /test/specs/navigation_spec.js: -------------------------------------------------------------------------------- 1 | describe("Keyboard navigation", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body), 6 | suggestions = [ 7 | { value: "Afghanistan", data: "Af" }, 8 | { value: "Albania", data: "Al" }, 9 | { value: "Andorra", data: "An" } 10 | ]; 11 | 12 | beforeEach(function() { 13 | $.Suggestions.resetTokens(); 14 | 15 | this.server = sinon.fakeServer.create(); 16 | 17 | this.input = document.createElement("input"); 18 | this.$input = $(this.input).appendTo($body); 19 | this.instance = this.$input 20 | .suggestions({ 21 | serviceUrl: serviceUrl, 22 | type: "NAME" 23 | }) 24 | .suggestions(); 25 | 26 | helpers.returnGoodStatus(this.server); 27 | }); 28 | 29 | afterEach(function() { 30 | this.instance.dispose(); 31 | this.$input.remove(); 32 | this.server.restore(); 33 | }); 34 | 35 | it("Should select first suggestion on DOWN key in textbox", function() { 36 | this.instance.selectedIndex = -1; 37 | 38 | this.input.value = "A"; 39 | this.instance.onValueChange(); 40 | this.server.respond(helpers.responseFor(suggestions)); 41 | helpers.keydown(this.input, 40); 42 | 43 | expect(this.instance.selectedIndex).toBe(0); 44 | expect(this.input.value).toEqual(suggestions[0].value); 45 | }); 46 | 47 | it("Should select last suggestion on UP key in textbox", function() { 48 | this.instance.selectedIndex = -1; 49 | 50 | this.input.value = "A"; 51 | this.instance.onValueChange(); 52 | this.server.respond(helpers.responseFor(suggestions)); 53 | helpers.keydown(this.input, 38); 54 | 55 | expect(this.instance.selectedIndex).toBe(2); 56 | expect(this.input.value).toEqual(suggestions[2].value); 57 | }); 58 | 59 | it("Should select textbox on DOWN key in last suggestion", function() { 60 | this.instance.selectedIndex = -1; 61 | 62 | this.input.value = "A"; 63 | this.instance.onValueChange(); 64 | this.server.respond(helpers.responseFor(suggestions)); 65 | this.instance.selectedIndex = 2; 66 | helpers.keydown(this.input, 40); 67 | 68 | expect(this.instance.selectedIndex).toBe(-1); 69 | expect(this.input.value).toEqual("A"); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/specs/promo_spec.js: -------------------------------------------------------------------------------- 1 | describe("Promo block", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url"; 5 | var token = "1234"; 6 | 7 | function query(self) { 8 | self.input.value = "ант"; 9 | self.instance.onValueChange(); 10 | self.server.respond( 11 | helpers.responseFor([{ value: "Антон" }, { value: "Антонина" }]) 12 | ); 13 | return self.instance.$container 14 | .get(0) 15 | .querySelector(".suggestions-promo"); 16 | } 17 | 18 | function queryNothing(self) { 19 | self.input.value = "фф"; 20 | self.instance.onValueChange(); 21 | self.server.respond(helpers.responseFor([])); 22 | return self.instance.$container 23 | .get(0) 24 | .querySelector(".suggestions-promo"); 25 | } 26 | 27 | beforeEach(function() { 28 | $.Suggestions.resetTokens(); 29 | this.server = sinon.fakeServer.create(); 30 | this.input = document.createElement("input"); 31 | document.body.appendChild(this.input); 32 | this.instance = $(this.input) 33 | .suggestions({ 34 | serviceUrl: serviceUrl, 35 | type: "NAME", 36 | token: token 37 | }) 38 | .suggestions(); 39 | }); 40 | 41 | afterEach(function() { 42 | this.instance.dispose(); 43 | document.body.removeChild(this.input); 44 | this.server.restore(); 45 | $.Suggestions.resetTokens(); 46 | }); 47 | 48 | it("Should set plan according to server", function() { 49 | expect(this.server.requests.length).toEqual(1); 50 | expect(this.server.requests[0].url).toMatch(/status\/fio/); 51 | this.server.respond([200, { "X-Plan": "FREE" }, '{ "search": true }']); 52 | expect(this.instance.status.plan).toEqual("FREE"); 53 | }); 54 | 55 | it("Should show promo block for free plan", function() { 56 | this.server.respond([200, { "X-Plan": "FREE" }, '{ "search": true }']); 57 | var promo = query(this); 58 | expect(helpers.isHidden(promo)).toBeFalsy(); 59 | }); 60 | 61 | it("Promo link should lead to Dadata", function() { 62 | this.server.respond([200, { "X-Plan": "FREE" }, '{ "search": true }']); 63 | var promo = query(this); 64 | var link = promo.querySelector("a"); 65 | expect(link.href).toEqual( 66 | "https://dadata.ru/suggestions/?utm_source=dadata&utm_medium=module&utm_campaign=suggestions-jquery" 67 | ); 68 | }); 69 | 70 | it("Should NOT show promo block for premium plan", function() { 71 | this.server.respond([ 72 | 200, 73 | { "X-Plan": "MEDIUM" }, 74 | '{ "search": true }' 75 | ]); 76 | var promo = query(this); 77 | expect(helpers.isHidden(promo)).toBeTruthy(); 78 | }); 79 | 80 | it("Should NOT show promo block for standalone suggestions", function() { 81 | this.server.respond([200, { "X-Plan": "NONE" }, '{ "search": true }']); 82 | var promo = query(this); 83 | expect(helpers.isHidden(promo)).toBeTruthy(); 84 | }); 85 | 86 | it("Should NOT show promo block if header is missing", function() { 87 | this.server.respond([200, {}, '{ "search": true }']); 88 | var promo = query(this); 89 | expect(helpers.isHidden(promo)).toBeTruthy(); 90 | }); 91 | 92 | it("Should not show when response is empty", function() { 93 | this.server.respond([200, { "X-Plan": "FREE" }, '{ "search": true }']); 94 | var promo = queryNothing(this); 95 | expect(promo).toBeNull(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/specs/select_nothing_spec.js: -------------------------------------------------------------------------------- 1 | describe("Nothing selected callback", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | fixtures = [ 6 | { value: "Afghanistan", data: "Af" }, 7 | { value: "Albania", data: "Al" }, 8 | { value: "Andorra", data: "An" } 9 | ]; 10 | 11 | beforeEach(function() { 12 | $.Suggestions.resetTokens(); 13 | 14 | this.server = sinon.fakeServer.create(); 15 | 16 | this.input = document.createElement("input"); 17 | this.$input = $(this.input).appendTo("body"); 18 | this.instance = this.$input 19 | .suggestions({ 20 | serviceUrl: serviceUrl, 21 | type: "ADDRESS", 22 | onSelect: $.noop, 23 | geoLocation: false 24 | }) 25 | .suggestions(); 26 | 27 | helpers.returnGoodStatus(this.server); 28 | 29 | this.server.respond(); 30 | this.server.requests.length = 0; 31 | }); 32 | 33 | afterEach(function() { 34 | this.instance.dispose(); 35 | this.$input.remove(); 36 | this.server.restore(); 37 | }); 38 | 39 | it("Should be triggered on ENTER pressed with no suggestions visible", function() { 40 | var options = { 41 | onSelectNothing: function() {} 42 | }; 43 | spyOn(options, "onSelectNothing"); 44 | 45 | this.instance.setOptions(options); 46 | this.instance.selectedIndex = -1; 47 | 48 | this.input.value = "A"; 49 | this.instance.onValueChange(); 50 | helpers.hitEnter(this.input); 51 | 52 | expect(options.onSelectNothing.calls.count()).toEqual(1); 53 | expect(options.onSelectNothing).toHaveBeenCalledWith("A"); 54 | }); 55 | 56 | it("Should be triggered on ENTER pressed with no matching suggestion", function() { 57 | var options = { 58 | onSelectNothing: function() {} 59 | }; 60 | spyOn(options, "onSelectNothing"); 61 | 62 | this.instance.setOptions(options); 63 | this.instance.selectedIndex = -1; 64 | 65 | this.input.value = "A"; 66 | this.instance.onValueChange(); 67 | this.server.respond(helpers.responseFor(fixtures)); 68 | 69 | helpers.hitEnter(this.input); 70 | 71 | expect(options.onSelectNothing.calls.count()).toEqual(1); 72 | expect(options.onSelectNothing).toHaveBeenCalledWith("A"); 73 | }); 74 | 75 | it("Should be triggered when focus lost and no matching suggestion", function() { 76 | var options = { 77 | onSelectNothing: function() {} 78 | }; 79 | spyOn(options, "onSelectNothing"); 80 | 81 | this.instance.setOptions(options); 82 | this.instance.selectedIndex = -1; 83 | 84 | this.input.value = "A"; 85 | this.instance.onValueChange(); 86 | this.server.respond(helpers.responseFor(fixtures)); 87 | 88 | helpers.fireBlur(this.input); 89 | 90 | expect(options.onSelectNothing.calls.count()).toEqual(1); 91 | expect(options.onSelectNothing).toHaveBeenCalledWith("A"); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/specs/select_on_blur_spec.js: -------------------------------------------------------------------------------- 1 | describe("Select on blur", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | 10 | this.server = sinon.fakeServer.create(); 11 | 12 | this.input = document.createElement("input"); 13 | this.$input = $(this.input).appendTo($body); 14 | this.instance = this.$input 15 | .suggestions({ 16 | serviceUrl: serviceUrl, 17 | type: "NAME" 18 | }) 19 | .suggestions(); 20 | 21 | helpers.returnGoodStatus(this.server); 22 | }); 23 | 24 | afterEach(function() { 25 | this.instance.dispose(); 26 | this.$input.remove(); 27 | this.server.restore(); 28 | }); 29 | 30 | it("Should trigger on full match", function() { 31 | var suggestions = [ 32 | { value: "Afghanistan", data: "Af" }, 33 | { value: "Albania", data: "Al" }, 34 | { value: "Andorra", data: "An" } 35 | ], 36 | options = { 37 | onSelect: function() {} 38 | }; 39 | spyOn(options, "onSelect"); 40 | 41 | this.instance.setOptions(options); 42 | this.instance.selectedIndex = -1; 43 | 44 | this.input.value = "Albania"; 45 | this.instance.onValueChange(); 46 | this.server.respond(helpers.responseFor(suggestions)); 47 | 48 | helpers.fireBlur(this.input); 49 | 50 | expect(options.onSelect.calls.count()).toEqual(1); 51 | expect(options.onSelect).toHaveBeenCalledWith( 52 | helpers.appendUnrestrictedValue(suggestions[1]), 53 | false 54 | ); 55 | }); 56 | 57 | it("Should trigger when suggestion is selected manually", function() { 58 | var suggestions = [ 59 | { value: "Afghanistan", data: "Af" }, 60 | { value: "Albania", data: "Al" }, 61 | { value: "Andorra", data: "An" } 62 | ]; 63 | var options = { 64 | onSelect: function() {} 65 | }; 66 | spyOn(options, "onSelect"); 67 | 68 | this.instance.setOptions(options); 69 | 70 | this.input.value = "A"; 71 | this.instance.onValueChange(); 72 | this.server.respond(helpers.responseFor(suggestions)); 73 | 74 | this.instance.selectedIndex = 2; 75 | helpers.fireBlur(this.input); 76 | 77 | expect(options.onSelect.calls.count()).toEqual(1); 78 | expect(options.onSelect).toHaveBeenCalledWith( 79 | helpers.appendUnrestrictedValue(suggestions[2]), 80 | true 81 | ); 82 | }); 83 | 84 | it("Should NOT trigger on partial match", function() { 85 | var suggestions = [{ value: "Jamaica", data: "J" }], 86 | options = { 87 | onSelect: function() {} 88 | }; 89 | spyOn(options, "onSelect"); 90 | 91 | this.instance.setOptions(options); 92 | this.instance.selectedIndex = -1; 93 | 94 | this.input.value = "Jam"; 95 | this.instance.onValueChange(); 96 | this.server.respond(helpers.responseFor(suggestions)); 97 | this.input.blur(); 98 | 99 | expect(options.onSelect).not.toHaveBeenCalled(); 100 | }); 101 | 102 | it("Should NOT trigger when nothing matched", function() { 103 | var suggestions = [{ value: "Jamaica", data: "J" }], 104 | options = { 105 | onSelect: function() {} 106 | }; 107 | spyOn(options, "onSelect"); 108 | 109 | this.instance.setOptions(options); 110 | this.instance.selectedIndex = -1; 111 | 112 | this.input.value = "Alg"; 113 | this.instance.onValueChange(); 114 | this.server.respond(helpers.responseFor(suggestions)); 115 | this.input.blur(); 116 | 117 | expect(options.onSelect).not.toHaveBeenCalled(); 118 | }); 119 | 120 | it("Should NOT trigger when triggerSelectOnBlur is false", function() { 121 | var suggestions = [{ value: "Jamaica", data: "J" }], 122 | options = { 123 | onSelect: function() {}, 124 | triggerSelectOnBlur: false 125 | }; 126 | spyOn(options, "onSelect"); 127 | 128 | this.instance.setOptions(options); 129 | this.input.value = "A"; 130 | this.instance.onValueChange(); 131 | this.server.respond(helpers.responseFor(suggestions)); 132 | 133 | this.instance.selectedIndex = 0; 134 | helpers.fireBlur(this.input); 135 | 136 | expect(options.onSelect).not.toHaveBeenCalled(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/specs/select_on_space.js: -------------------------------------------------------------------------------- 1 | describe("Select on Space", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | $body = $(document.body); 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | 10 | this.server = sinon.fakeServer.create(); 11 | 12 | this.input = document.createElement("input"); 13 | this.$input = $(this.input).appendTo($body); 14 | this.instance = this.$input 15 | .suggestions({ 16 | serviceUrl: serviceUrl, 17 | type: "NAME", 18 | deferRequestBy: 0, 19 | triggerSelectOnSpace: true 20 | }) 21 | .suggestions(); 22 | 23 | helpers.returnGoodStatus(this.server); 24 | }); 25 | 26 | afterEach(function() { 27 | this.instance.dispose(); 28 | this.$input.remove(); 29 | this.server.restore(); 30 | }); 31 | 32 | it("Should trigger when suggestion is selected", function() { 33 | var suggestions = [{ value: "Jamaica", data: "J" }], 34 | options = { 35 | onSelect: function() {} 36 | }; 37 | spyOn(options, "onSelect"); 38 | 39 | this.instance.setOptions(options); 40 | 41 | this.input.value = "Jam"; 42 | this.instance.onValueChange(); 43 | this.server.respond(helpers.responseFor(suggestions)); 44 | 45 | this.instance.selectedIndex = 0; 46 | 47 | helpers.keydown(this.input, 32); 48 | 49 | expect(options.onSelect.calls.count()).toEqual(1); 50 | expect(options.onSelect).toHaveBeenCalledWith( 51 | helpers.appendUnrestrictedValue(suggestions[0]), 52 | true 53 | ); 54 | }); 55 | 56 | it("Should trigger when nothing is selected but there is exact match", function() { 57 | var suggestions = [{ value: "Jamaica", data: "J" }], 58 | options = { 59 | onSelect: function() {} 60 | }; 61 | spyOn(options, "onSelect"); 62 | 63 | this.instance.setOptions(options); 64 | this.instance.selectedIndex = -1; 65 | 66 | this.input.value = "Jamaica"; 67 | this.instance.onValueChange(); 68 | this.server.respond(helpers.responseFor(suggestions)); 69 | 70 | helpers.keydown(this.input, 32); // code of space 71 | 72 | expect(options.onSelect.calls.count()).toEqual(1); 73 | expect(options.onSelect).toHaveBeenCalledWith( 74 | helpers.appendUnrestrictedValue(suggestions[0]), 75 | true 76 | ); 77 | }); 78 | 79 | it("Should NOT trigger when triggerSelectOnSpace = false", function() { 80 | var suggestions = [{ value: "Jamaica", data: "J" }], 81 | options = { 82 | triggerSelectOnSpace: false, 83 | onSelect: function() {} 84 | }; 85 | spyOn(options, "onSelect"); 86 | 87 | this.instance.setOptions(options); 88 | 89 | this.input.value = "Jam"; 90 | this.instance.onValueChange(); 91 | this.server.respond(helpers.responseFor(suggestions)); 92 | 93 | this.instance.selectedIndex = 0; 94 | helpers.keydown(this.input, 32); // code of space 95 | 96 | expect(options.onSelect).not.toHaveBeenCalled(); 97 | }); 98 | 99 | it("Should keep SPACE if selecting has been caused by space", function() { 100 | var suggestions = [ 101 | { 102 | value: "name", 103 | data: { name: "name" } 104 | }, 105 | { 106 | value: "name surname", 107 | data: { name: "name", surname: "surname" } 108 | } 109 | ], 110 | options = { onSelect: $.noop }; 111 | 112 | spyOn(options, "onSelect"); 113 | this.instance.setOptions(options); 114 | 115 | this.input.value = "name"; 116 | this.instance.onValueChange(); 117 | this.server.respond(helpers.responseFor(suggestions)); 118 | 119 | this.instance.selectedIndex = 0; 120 | helpers.keydown(this.input, 32); 121 | 122 | expect(options.onSelect.calls.count()).toEqual(1); 123 | expect(this.input.value).toEqual("name "); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/specs/status_spec.js: -------------------------------------------------------------------------------- 1 | describe("Status features", function() { 2 | "use strict"; 3 | 4 | var serviceUrl = "/some/url", 5 | token = "1234"; 6 | 7 | beforeEach(function() { 8 | $.Suggestions.resetTokens(); 9 | this.server = sinon.fakeServer.create(); 10 | 11 | this.input = document.createElement("input"); 12 | this.$input = $(this.input).appendTo("body"); 13 | this.instance = this.$input 14 | .suggestions({ 15 | serviceUrl: serviceUrl, 16 | type: "NAME", 17 | token: token 18 | }) 19 | .suggestions(); 20 | }); 21 | 22 | afterEach(function() { 23 | this.instance.dispose(); 24 | this.$input.remove(); 25 | this.server.restore(); 26 | $.Suggestions.resetTokens(); 27 | }); 28 | 29 | it("Should send status request with token", function() { 30 | expect(this.server.requests.length).toEqual(1); 31 | expect(this.server.requests[0].url).toMatch(/status\/fio/); 32 | expect(this.server.requests[0].requestHeaders.Authorization).toEqual( 33 | "Token " + token 34 | ); 35 | }); 36 | 37 | it("Should send status request without token", function() { 38 | this.server.requests.length = 0; 39 | this.instance.setOptions({ 40 | token: null 41 | }); 42 | 43 | expect(this.server.requests.length).toEqual(1); 44 | expect(this.server.requests[0].url).toMatch(/status\/fio/); 45 | expect( 46 | this.server.requests[0].requestHeaders.Authorization 47 | ).toBeUndefined(); 48 | }); 49 | 50 | it("Should invoke `onSearchError` callback if status request failed", function() { 51 | var options = { 52 | onSearchError: $.noop, 53 | token: "456" 54 | }; 55 | spyOn(options, "onSearchError"); 56 | this.instance.setOptions(options); 57 | 58 | this.server.respond([401, {}, "Not Authorized"]); 59 | 60 | expect(options.onSearchError).toHaveBeenCalled(); 61 | }); 62 | 63 | it("Should use url param (if it passed) instead of serviceUrl", function() { 64 | this.server.requests.length = 0; 65 | this.instance.setOptions({ 66 | token: null, 67 | url: "http://unchangeable/url" 68 | }); 69 | 70 | expect(this.server.requests.length).toEqual(1); 71 | expect(this.server.requests[0].url).toEqual("http://unchangeable/url"); 72 | }); 73 | 74 | describe("Several instances with the same token", function() { 75 | beforeEach(function() { 76 | this.input2 = document.createElement("input"); 77 | this.$input2 = $(this.input2).appendTo("body"); 78 | this.instance2 = this.$input2 79 | .suggestions({ 80 | serviceUrl: serviceUrl, 81 | type: "NAME", 82 | token: token 83 | }) 84 | .suggestions(); 85 | }); 86 | 87 | afterEach(function() { 88 | this.instance2.dispose(); 89 | this.$input2.remove(); 90 | }); 91 | 92 | it("Should use the same authorization query", function() { 93 | expect(this.server.requests.length).toEqual(1); 94 | }); 95 | 96 | it("Should make another request for controls of different types", function() { 97 | this.instance.setOptions({ 98 | type: "ADDRESS", 99 | geoLocation: false 100 | }); 101 | 102 | expect(this.server.requests.length).toEqual(2); 103 | }); 104 | 105 | it("Should invoke `onSearchError` callback on controls with same type and token", function() { 106 | var options = { 107 | onSearchError: $.noop 108 | }; 109 | spyOn(options, "onSearchError"); 110 | this.instance2.setOptions(options); 111 | 112 | this.server.respond([401, {}, "Not Authorized"]); 113 | 114 | expect(options.onSearchError).toHaveBeenCalled(); 115 | }); 116 | }); 117 | }); 118 | --------------------------------------------------------------------------------