├── .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 | [](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 |
22 |
23 |
61 |
62 |
66 |
67 |
71 |
72 |
76 |
77 |
81 |
82 |
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 |
21 |
22 |
26 |
27 |
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 |
22 |
23 |
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 |
22 |
23 |
31 |
32 |
36 |
37 |
41 |
42 |
46 |
47 |
51 |
52 |
69 |
70 |
75 |
76 |
81 |
82 |
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 |
--------------------------------------------------------------------------------