sharkey-web 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.md +19 -0
  5. data/README.md +188 -0
  6. data/Rakefile +8 -0
  7. data/bin/sharkey-web +9 -0
  8. data/config.ru +3 -0
  9. data/lib/sharkey.rb +12 -0
  10. data/lib/sharkey/app.rb +526 -0
  11. data/lib/sharkey/importerexporter.rb +79 -0
  12. data/lib/sharkey/models.rb +295 -0
  13. data/lib/sharkey/public/css/loading.gif +0 -0
  14. data/lib/sharkey/public/css/magicsuggest.css +232 -0
  15. data/lib/sharkey/public/css/nprogress.css +74 -0
  16. data/lib/sharkey/public/css/styles.css +263 -0
  17. data/lib/sharkey/public/css/ui.fancytree.css +545 -0
  18. data/lib/sharkey/public/data/sentences.txt +5 -0
  19. data/lib/sharkey/public/fonts/Quadrata.eot +0 -0
  20. data/lib/sharkey/public/fonts/Quadrata.svg +613 -0
  21. data/lib/sharkey/public/fonts/Quadrata.ttf +0 -0
  22. data/lib/sharkey/public/fonts/Quadrata.woff +0 -0
  23. data/lib/sharkey/public/fonts/Quadrata.zip +0 -0
  24. data/lib/sharkey/public/images/loader.gif +0 -0
  25. data/lib/sharkey/public/images/sharkey-logo.png +0 -0
  26. data/lib/sharkey/public/images/sharkey.png +0 -0
  27. data/lib/sharkey/public/js/ajaxmanager.js +67 -0
  28. data/lib/sharkey/public/js/keybindings.js +92 -0
  29. data/lib/sharkey/public/js/lib/bootstrap.min.js +6 -0
  30. data/lib/sharkey/public/js/lib/jquery-1.9.1.min.js +5 -0
  31. data/lib/sharkey/public/js/lib/jquery-ui.js +16150 -0
  32. data/lib/sharkey/public/js/lib/jquery.bootstrap-autohidingnavbar.js +213 -0
  33. data/lib/sharkey/public/js/lib/jquery.fancytree-all.js +6424 -0
  34. data/lib/sharkey/public/js/lib/jquery.tagcloud.js +92 -0
  35. data/lib/sharkey/public/js/lib/magicsuggest.js +1468 -0
  36. data/lib/sharkey/public/js/lib/mousetrap.min.js +9 -0
  37. data/lib/sharkey/public/js/lib/nprogress.js +476 -0
  38. data/lib/sharkey/public/js/page-add-link-autofill.js +102 -0
  39. data/lib/sharkey/public/js/page-add-link.js +156 -0
  40. data/lib/sharkey/public/js/page-categories.js +348 -0
  41. data/lib/sharkey/public/js/page-edit-link.js +103 -0
  42. data/lib/sharkey/public/js/page-links.js +54 -0
  43. data/lib/sharkey/public/js/page-settings.js +93 -0
  44. data/lib/sharkey/public/js/page-tagcloud.js +35 -0
  45. data/lib/sharkey/public/js/page-tags.js +287 -0
  46. data/lib/sharkey/public/js/scripts.js +147 -0
  47. data/lib/sharkey/public/themes/amelia/style.css +7 -0
  48. data/lib/sharkey/public/themes/bootstrap/style.css +5785 -0
  49. data/lib/sharkey/public/themes/cerulean/style.css +7 -0
  50. data/lib/sharkey/public/themes/cosmo/style.css +7 -0
  51. data/lib/sharkey/public/themes/cyborg/style.css +7 -0
  52. data/lib/sharkey/public/themes/darkly/style.css +7 -0
  53. data/lib/sharkey/public/themes/facebook-like/README.md +6 -0
  54. data/lib/sharkey/public/themes/facebook-like/style.css +6085 -0
  55. data/lib/sharkey/public/themes/flatly/style.css +7 -0
  56. data/lib/sharkey/public/themes/fonts/glyphicons-halflings-regular.eot +0 -0
  57. data/lib/sharkey/public/themes/fonts/glyphicons-halflings-regular.svg +229 -0
  58. data/lib/sharkey/public/themes/fonts/glyphicons-halflings-regular.ttf +0 -0
  59. data/lib/sharkey/public/themes/fonts/glyphicons-halflings-regular.woff +0 -0
  60. data/lib/sharkey/public/themes/holo-like/README.md +5 -0
  61. data/lib/sharkey/public/themes/holo-like/style.css +5997 -0
  62. data/lib/sharkey/public/themes/journal/style.css +7 -0
  63. data/lib/sharkey/public/themes/lumen/style.css +7 -0
  64. data/lib/sharkey/public/themes/readable/style.css +7 -0
  65. data/lib/sharkey/public/themes/simplex/style.css +7 -0
  66. data/lib/sharkey/public/themes/slate/style.css +7 -0
  67. data/lib/sharkey/public/themes/spacelab/style.css +7 -0
  68. data/lib/sharkey/public/themes/superhero/style.css +7 -0
  69. data/lib/sharkey/public/themes/united/style.css +7 -0
  70. data/lib/sharkey/public/themes/yeti/style.css +7 -0
  71. data/lib/sharkey/setting.rb +74 -0
  72. data/lib/sharkey/version.rb +5 -0
  73. data/lib/sharkey/views/404.slim +4 -0
  74. data/lib/sharkey/views/about.slim +7 -0
  75. data/lib/sharkey/views/add_link.slim +157 -0
  76. data/lib/sharkey/views/categories.slim +101 -0
  77. data/lib/sharkey/views/category.slim +22 -0
  78. data/lib/sharkey/views/centered.slim +49 -0
  79. data/lib/sharkey/views/dashboard.slim +107 -0
  80. data/lib/sharkey/views/dashboard_index.slim +26 -0
  81. data/lib/sharkey/views/edit_link.slim +121 -0
  82. data/lib/sharkey/views/help.slim +74 -0
  83. data/lib/sharkey/views/keybindings.slim +58 -0
  84. data/lib/sharkey/views/link.slim +30 -0
  85. data/lib/sharkey/views/links.slim +22 -0
  86. data/lib/sharkey/views/navbar.slim +68 -0
  87. data/lib/sharkey/views/settings.slim +74 -0
  88. data/lib/sharkey/views/settings_index.slim +220 -0
  89. data/lib/sharkey/views/single_category.slim +37 -0
  90. data/lib/sharkey/views/single_link.slim +95 -0
  91. data/lib/sharkey/views/single_tag.slim +33 -0
  92. data/lib/sharkey/views/tag.slim +22 -0
  93. data/lib/sharkey/views/tagcloud.slim +54 -0
  94. data/lib/sharkey/views/tags.slim +99 -0
  95. data/sharkey-web.gemspec +44 -0
  96. metadata +324 -0
@@ -0,0 +1,92 @@
1
+ /*!
2
+ * jquery.tagcloud.js
3
+ * A Simple Tag Cloud Plugin for JQuery
4
+ *
5
+ * https://github.com/addywaddy/jquery.tagcloud.js
6
+ * created by Adam Groves
7
+ */
8
+ (function($) {
9
+
10
+ /*global jQuery*/
11
+ "use strict";
12
+
13
+ var compareWeights = function(a, b)
14
+ {
15
+ return a - b;
16
+ };
17
+
18
+ // Converts hex to an RGB array
19
+ var toRGB = function(code) {
20
+ if (code.length === 4) {
21
+ code = code.replace(/(\w)(\w)(\w)/gi, "\$1\$1\$2\$2\$3\$3");
22
+ }
23
+ var hex = /(\w{2})(\w{2})(\w{2})/.exec(code);
24
+ return [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)];
25
+ };
26
+
27
+ // Converts an RGB array to hex
28
+ var toHex = function(ary) {
29
+ return "#" + jQuery.map(ary, function(i) {
30
+ var hex = i.toString(16);
31
+ hex = (hex.length === 1) ? "0" + hex : hex;
32
+ return hex;
33
+ }).join("");
34
+ };
35
+
36
+ var colorIncrement = function(color, range) {
37
+ return jQuery.map(toRGB(color.end), function(n, i) {
38
+ return (n - toRGB(color.start)[i])/range;
39
+ });
40
+ };
41
+
42
+ var tagColor = function(color, increment, weighting) {
43
+ var rgb = jQuery.map(toRGB(color.start), function(n, i) {
44
+ var ref = Math.round(n + (increment[i] * weighting));
45
+ if (ref > 255) {
46
+ ref = 255;
47
+ } else {
48
+ if (ref < 0) {
49
+ ref = 0;
50
+ }
51
+ }
52
+ return ref;
53
+ });
54
+ return toHex(rgb);
55
+ };
56
+
57
+ $.fn.tagcloud = function(options) {
58
+
59
+ var opts = $.extend({}, $.fn.tagcloud.defaults, options);
60
+ var tagWeights = this.map(function(){
61
+ return $(this).attr("rel");
62
+ });
63
+ tagWeights = jQuery.makeArray(tagWeights).sort(compareWeights);
64
+ var lowest = tagWeights[0];
65
+ var highest = tagWeights.pop();
66
+ var range = highest - lowest;
67
+ if(range === 0) {range = 1;}
68
+ // Sizes
69
+ var fontIncr, colorIncr;
70
+ if (opts.size) {
71
+ fontIncr = (opts.size.end - opts.size.start)/range;
72
+ }
73
+ // Colors
74
+ if (opts.color) {
75
+ colorIncr = colorIncrement (opts.color, range);
76
+ }
77
+ return this.each(function() {
78
+ var weighting = $(this).attr("rel") - lowest;
79
+ if (opts.size) {
80
+ $(this).css({"font-size": opts.size.start + (weighting * fontIncr) + opts.size.unit});
81
+ }
82
+ if (opts.color) {
83
+ $(this).css({"color": tagColor(opts.color, colorIncr, weighting)});
84
+ }
85
+ });
86
+ };
87
+
88
+ $.fn.tagcloud.defaults = {
89
+ size: {start: 14, end: 18, unit: "pt"}
90
+ };
91
+
92
+ })(jQuery);
@@ -0,0 +1,1468 @@
1
+ /**
2
+ * Multiple Selection Component for Bootstrap
3
+ * Check nicolasbize.github.io/magicsuggest/ for latest updates.
4
+ *
5
+ * Author: Nicolas Bize
6
+ * Created: Feb 8th 2013
7
+ * Last Updated: Jun 3rd 2014
8
+ * Version: 2.0.5
9
+ * Licence: MagicSuggest is licenced under MIT licence (http://opensource.org/licenses/MIT)
10
+ */
11
+ (function($)
12
+ {
13
+ "use strict";
14
+ var MagicSuggest = function(element, options)
15
+ {
16
+ var ms = this;
17
+
18
+ /**
19
+ * Initializes the MagicSuggest component
20
+ */
21
+ var defaults = {
22
+ /********** CONFIGURATION PROPERTIES ************/
23
+ /**
24
+ * Restricts or allows the user to validate typed entries.
25
+ * Defaults to true.
26
+ */
27
+ allowFreeEntries: true,
28
+
29
+ /**
30
+ * A custom CSS class to apply to the field's underlying element.
31
+ */
32
+ cls: '',
33
+
34
+ /**
35
+ * JSON Data source used to populate the combo box. 3 options are available here:
36
+ * No Data Source (default)
37
+ * When left null, the combo box will not suggest anything. It can still enable the user to enter
38
+ * multiple entries if allowFreeEntries is * set to true (default).
39
+ * Static Source
40
+ * You can pass an array of JSON objects, an array of strings or even a single CSV string as the
41
+ * data source.For ex. data: [* {id:0,name:"Paris"}, {id: 1, name: "New York"}]
42
+ * You can also pass any json object with the results property containing the json array.
43
+ * Url
44
+ * You can pass the url from which the component will fetch its JSON data.Data will be fetched
45
+ * using a POST ajax request that will * include the entered text as 'query' parameter. The results
46
+ * fetched from the server can be:
47
+ * - an array of JSON objects (ex: [{id:...,name:...},{...}])
48
+ * - a string containing an array of JSON objects ready to be parsed (ex: "[{id:...,name:...},{...}]")
49
+ * - a JSON object whose data will be contained in the results property
50
+ * (ex: {results: [{id:...,name:...},{...}]
51
+ * Function
52
+ * You can pass a function which returns an array of JSON objects (ex: [{id:...,name:...},{...}])
53
+ * The function can return the JSON data or it can use the first argument as function to handle the data.
54
+ * Only one (callback function or return value) is needed for the function to succeed.
55
+ * See the following example:
56
+ * function (response) { var myjson = [{name: 'test', id: 1}]; response(myjson); return myjson; }
57
+ */
58
+ data: null,
59
+
60
+ /**
61
+ * Additional parameters to the ajax call
62
+ */
63
+ dataUrlParams: {},
64
+
65
+ /**
66
+ * Start the component in a disabled state.
67
+ */
68
+ disabled: false,
69
+
70
+ /**
71
+ * name of JSON object property displayed in the combo list
72
+ */
73
+ displayField: 'name',
74
+
75
+ /**
76
+ * Set to false if you only want mouse interaction. In that case the combo will
77
+ * automatically expand on focus.
78
+ */
79
+ editable: true,
80
+
81
+ /**
82
+ * Set starting state for combo.
83
+ */
84
+ expanded: false,
85
+
86
+ /**
87
+ * Automatically expands combo on focus.
88
+ */
89
+ expandOnFocus: false,
90
+
91
+ /**
92
+ * JSON property by which the list should be grouped
93
+ */
94
+ groupBy: null,
95
+
96
+ /**
97
+ * Set to true to hide the trigger on the right
98
+ */
99
+ hideTrigger: false,
100
+
101
+ /**
102
+ * Set to true to highlight search input within displayed suggestions
103
+ */
104
+ highlight: true,
105
+
106
+ /**
107
+ * A custom ID for this component
108
+ */
109
+ id: null,
110
+
111
+ /**
112
+ * A class that is added to the info message appearing on the top-right part of the component
113
+ */
114
+ infoMsgCls: '',
115
+
116
+ /**
117
+ * Additional parameters passed out to the INPUT tag. Enables usage of AngularJS's custom tags for ex.
118
+ */
119
+ inputCfg: {},
120
+
121
+ /**
122
+ * The class that is applied to show that the field is invalid
123
+ */
124
+ invalidCls: 'ms-inv',
125
+
126
+ /**
127
+ * Set to true to filter data results according to case. Useless if the data is fetched remotely
128
+ */
129
+ matchCase: false,
130
+
131
+ /**
132
+ * Once expanded, the combo's height will take as much room as the # of available results.
133
+ * In case there are too many results displayed, this will fix the drop down height.
134
+ */
135
+ maxDropHeight: 290,
136
+
137
+ /**
138
+ * Defines how long the user free entry can be. Set to null for no limit.
139
+ */
140
+ maxEntryLength: null,
141
+
142
+ /**
143
+ * A function that defines the helper text when the max entry length has been surpassed.
144
+ */
145
+ maxEntryRenderer: function(v) {
146
+ return 'Please reduce your entry by ' + v + ' character' + (v > 1 ? 's':'');
147
+ },
148
+
149
+ /**
150
+ * The maximum number of results displayed in the combo drop down at once.
151
+ */
152
+ maxSuggestions: null,
153
+
154
+ /**
155
+ * The maximum number of items the user can select if multiple selection is allowed.
156
+ * Set to null to remove the limit.
157
+ */
158
+ maxSelection: 10,
159
+
160
+ /**
161
+ * A function that defines the helper text when the max selection amount has been reached. The function has a single
162
+ * parameter which is the number of selected elements.
163
+ */
164
+ maxSelectionRenderer: function(v) {
165
+ return 'You cannot choose more than ' + v + ' item' + (v > 1 ? 's':'');
166
+ },
167
+
168
+ /**
169
+ * The method used by the ajax request.
170
+ */
171
+ method: 'POST',
172
+
173
+ /**
174
+ * The minimum number of characters the user must type before the combo expands and offers suggestions.
175
+ */
176
+ minChars: 0,
177
+
178
+ /**
179
+ * A function that defines the helper text when not enough letters are set. The function has a single
180
+ * parameter which is the difference between the required amount of letters and the current one.
181
+ */
182
+ minCharsRenderer: function(v) {
183
+ return 'Please type ' + v + ' more character' + (v > 1 ? 's':'');
184
+ },
185
+
186
+ /**
187
+ * Whether or not sorting / filtering should be done remotely or locally.
188
+ * Use either 'local' or 'remote'
189
+ */
190
+ mode: 'local',
191
+
192
+ /**
193
+ * The name used as a form element.
194
+ */
195
+ name: null,
196
+
197
+ /**
198
+ * The text displayed when there are no suggestions.
199
+ */
200
+ noSuggestionText: 'No suggestions',
201
+
202
+ /**
203
+ * The default placeholder text when nothing has been entered
204
+ */
205
+ placeholder: 'Type or click here',
206
+
207
+ /**
208
+ * If a single suggestion comes out, it is preselected.
209
+ */
210
+ autoSelect: true,
211
+
212
+ /**
213
+ * A function used to define how the items will be presented in the combo
214
+ */
215
+ renderer: null,
216
+
217
+ /**
218
+ * Whether or not this field should be required
219
+ */
220
+ required: false,
221
+
222
+ /**
223
+ * Set to true to render selection as comma separated string
224
+ */
225
+ resultAsString: false,
226
+
227
+ /**
228
+ * Name of JSON object property that represents the list of suggested objects
229
+ */
230
+ resultsField: 'results',
231
+
232
+ /**
233
+ * A custom CSS class to add to a selected item
234
+ */
235
+ selectionCls: '',
236
+
237
+ /**
238
+ * Where the selected items will be displayed. Only 'right', 'bottom' and 'inner' are valid values
239
+ */
240
+ selectionPosition: 'inner',
241
+
242
+ /**
243
+ * A function used to define how the items will be presented in the tag list
244
+ */
245
+ selectionRenderer: null,
246
+
247
+ /**
248
+ * Set to true to stack the selectioned items when positioned on the bottom
249
+ * Requires the selectionPosition to be set to 'bottom'
250
+ */
251
+ selectionStacked: false,
252
+
253
+ /**
254
+ * Direction used for sorting. Only 'asc' and 'desc' are valid values
255
+ */
256
+ sortDir: 'asc',
257
+
258
+ /**
259
+ * name of JSON object property for local result sorting.
260
+ * Leave null if you do not wish the results to be ordered or if they are already ordered remotely.
261
+ */
262
+ sortOrder: null,
263
+
264
+ /**
265
+ * If set to true, suggestions will have to start by user input (and not simply contain it as a substring)
266
+ */
267
+ strictSuggest: false,
268
+
269
+ /**
270
+ * Custom style added to the component container.
271
+ */
272
+ style: '',
273
+
274
+ /**
275
+ * If set to true, the combo will expand / collapse when clicked upon
276
+ */
277
+ toggleOnClick: false,
278
+
279
+
280
+ /**
281
+ * Amount (in ms) between keyboard registers.
282
+ */
283
+ typeDelay: 400,
284
+
285
+ /**
286
+ * If set to true, tab won't blur the component but will be registered as the ENTER key
287
+ */
288
+ useTabKey: false,
289
+
290
+ /**
291
+ * If set to true, using comma will validate the user's choice
292
+ */
293
+ useCommaKey: true,
294
+
295
+
296
+ /**
297
+ * Determines whether or not the results will be displayed with a zebra table style
298
+ */
299
+ useZebraStyle: false,
300
+
301
+ /**
302
+ * initial value for the field
303
+ */
304
+ value: null,
305
+
306
+ /**
307
+ * name of JSON object property that represents its underlying value
308
+ */
309
+ valueField: 'id',
310
+
311
+ /**
312
+ * regular expression to validate the values against
313
+ */
314
+ vregex: null,
315
+
316
+ /**
317
+ * type to validate against
318
+ */
319
+ vtype: null
320
+ };
321
+
322
+ var conf = $.extend({},options);
323
+ var cfg = $.extend(true, {}, defaults, conf);
324
+
325
+ /********** PUBLIC METHODS ************/
326
+ /**
327
+ * Add one or multiple json items to the current selection
328
+ * @param items - json object or array of json objects
329
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
330
+ */
331
+ this.addToSelection = function(items, isSilent)
332
+ {
333
+ if (!cfg.maxSelection || _selection.length < cfg.maxSelection) {
334
+ if (!$.isArray(items)) {
335
+ items = [items];
336
+ }
337
+ var valuechanged = false;
338
+ $.each(items, function(index, json) {
339
+ if ($.inArray(json[cfg.valueField], ms.getValue()) === -1) {
340
+ _selection.push(json);
341
+ valuechanged = true;
342
+ }
343
+ });
344
+ if(valuechanged === true) {
345
+ self._renderSelection();
346
+ this.empty();
347
+ if (isSilent !== true) {
348
+ $(this).trigger('selectionchange', [this, this.getSelection()]);
349
+ }
350
+ }
351
+ }
352
+ this.input.attr('placeholder', (cfg.selectionPosition === 'inner' && this.getValue().length > 0) ? '' : cfg.placeholder);
353
+ };
354
+
355
+ /**
356
+ * Clears the current selection
357
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
358
+ */
359
+ this.clear = function(isSilent)
360
+ {
361
+ this.removeFromSelection(_selection.slice(0), isSilent); // clone array to avoid concurrency issues
362
+ };
363
+
364
+ /**
365
+ * Collapse the drop down part of the combo
366
+ */
367
+ this.collapse = function()
368
+ {
369
+ if (cfg.expanded === true) {
370
+ this.combobox.detach();
371
+ cfg.expanded = false;
372
+ $(this).trigger('collapse', [this]);
373
+ }
374
+ };
375
+
376
+ /**
377
+ * Set the component in a disabled state.
378
+ */
379
+ this.disable = function()
380
+ {
381
+ this.container.addClass('ms-ctn-disabled');
382
+ cfg.disabled = true;
383
+ ms.input.attr('disabled', true);
384
+ };
385
+
386
+ /**
387
+ * Empties out the combo user text
388
+ */
389
+ this.empty = function(){
390
+ this.input.val('');
391
+ };
392
+
393
+ /**
394
+ * Set the component in a enable state.
395
+ */
396
+ this.enable = function()
397
+ {
398
+ this.container.removeClass('ms-ctn-disabled');
399
+ cfg.disabled = false;
400
+ ms.input.attr('disabled', false);
401
+ };
402
+
403
+ /**
404
+ * Expand the drop drown part of the combo.
405
+ */
406
+ this.expand = function()
407
+ {
408
+ if (!cfg.expanded && (this.input.val().length >= cfg.minChars || this.combobox.children().size() > 0)) {
409
+ this.combobox.appendTo(this.container);
410
+ self._processSuggestions();
411
+ cfg.expanded = true;
412
+ $(this).trigger('expand', [this]);
413
+ }
414
+ };
415
+
416
+ /**
417
+ * Retrieve component enabled status
418
+ */
419
+ this.isDisabled = function()
420
+ {
421
+ return cfg.disabled;
422
+ };
423
+
424
+ /**
425
+ * Checks whether the field is valid or not
426
+ * @return {boolean}
427
+ */
428
+ this.isValid = function()
429
+ {
430
+ var valid = cfg.required === false || _selection.length > 0;
431
+ if(cfg.vtype || cfg.vregex){
432
+ $.each(_selection, function(index, item){
433
+ valid = valid && self._validateSingleItem(item[cfg.displayField]);
434
+ });
435
+ }
436
+ return valid;
437
+ };
438
+
439
+ /**
440
+ * Gets the data params for current ajax request
441
+ */
442
+ this.getDataUrlParams = function()
443
+ {
444
+ return cfg.dataUrlParams;
445
+ };
446
+
447
+ /**
448
+ * Gets the name given to the form input
449
+ */
450
+ this.getName = function()
451
+ {
452
+ return cfg.name;
453
+ };
454
+
455
+ /**
456
+ * Retrieve an array of selected json objects
457
+ * @return {Array}
458
+ */
459
+ this.getSelection = function()
460
+ {
461
+ return _selection;
462
+ };
463
+
464
+ /**
465
+ * Retrieve the current text entered by the user
466
+ */
467
+ this.getRawValue = function(){
468
+ return ms.input.val();
469
+ };
470
+
471
+ /**
472
+ * Retrieve an array of selected values
473
+ */
474
+ this.getValue = function()
475
+ {
476
+ return $.map(_selection, function(o) {
477
+ return o[cfg.valueField];
478
+ });
479
+ };
480
+
481
+ /**
482
+ * Remove one or multiples json items from the current selection
483
+ * @param items - json object or array of json objects
484
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
485
+ */
486
+ this.removeFromSelection = function(items, isSilent)
487
+ {
488
+ if (!$.isArray(items)) {
489
+ items = [items];
490
+ }
491
+ var valuechanged = false;
492
+ $.each(items, function(index, json) {
493
+ var i = $.inArray(json[cfg.valueField], ms.getValue());
494
+ if (i > -1) {
495
+ _selection.splice(i, 1);
496
+ valuechanged = true;
497
+ }
498
+ });
499
+ if (valuechanged === true) {
500
+ self._renderSelection();
501
+ if(isSilent !== true){
502
+ $(this).trigger('selectionchange', [this, this.getSelection()]);
503
+ }
504
+ if(cfg.expandOnFocus){
505
+ ms.expand();
506
+ }
507
+ if(cfg.expanded) {
508
+ self._processSuggestions();
509
+ }
510
+ }
511
+ this.input.attr('placeholder', (cfg.selectionPosition === 'inner' && this.getValue().length > 0) ? '' : cfg.placeholder);
512
+ };
513
+
514
+ /**
515
+ * Get current data
516
+ */
517
+ this.getData = function(){
518
+ return _cbData;
519
+ };
520
+
521
+ /**
522
+ * Set up some combo data after it has been rendered
523
+ * @param data
524
+ */
525
+ this.setData = function(data){
526
+ cfg.data = data;
527
+ self._processSuggestions();
528
+ };
529
+
530
+ /**
531
+ * Sets the name for the input field so it can be fetched in the form
532
+ * @param name
533
+ */
534
+ this.setName = function(name){
535
+ cfg.name = name;
536
+ if(name){
537
+ cfg.name += name.indexOf('[]') > 0 ? '' : '[]';
538
+ }
539
+ if(ms._valueContainer){
540
+ $.each(ms._valueContainer.children(), function(i, el){
541
+ el.name = cfg.name;
542
+ });
543
+ }
544
+ };
545
+
546
+ /**
547
+ * Sets the current selection with the JSON items provided
548
+ * @param items
549
+ */
550
+ this.setSelection = function(items){
551
+ this.clear();
552
+ this.addToSelection(items);
553
+ };
554
+
555
+ /**
556
+ * Sets a value for the combo box. Value must be an array of values with data type matching valueField one.
557
+ * @param data
558
+ */
559
+ this.setValue = function(values)
560
+ {
561
+ var items = [];
562
+
563
+ $.each(values, function(index, value) {
564
+ // first try to see if we have the full objects from our data set
565
+ var found = false;
566
+ $.each(_cbData, function(i,item){
567
+ if(item[cfg.valueField] == value){
568
+ items.push(item);
569
+ found = true;
570
+ return false;
571
+ }
572
+ });
573
+ if(!found){
574
+ if(typeof(value) === 'object'){
575
+ items.push(value);
576
+ } else {
577
+ var json = {};
578
+ json[cfg.valueField] = value;
579
+ json[cfg.displayField] = value;
580
+ items.push(json);
581
+ }
582
+ }
583
+ });
584
+ if(items.length > 0) {
585
+ this.addToSelection(items);
586
+ }
587
+ };
588
+
589
+ /**
590
+ * Sets data params for subsequent ajax requests
591
+ * @param params
592
+ */
593
+ this.setDataUrlParams = function(params)
594
+ {
595
+ cfg.dataUrlParams = $.extend({},params);
596
+ };
597
+
598
+ /********** PRIVATE ************/
599
+ var _selection = [], // selected objects
600
+ _comboItemHeight = 0, // height for each combo item.
601
+ _timer,
602
+ _hasFocus = false,
603
+ _groups = null,
604
+ _cbData = [],
605
+ _ctrlDown = false,
606
+ KEYCODES = {
607
+ BACKSPACE: 8,
608
+ TAB: 9,
609
+ ENTER: 13,
610
+ CTRL: 17,
611
+ ESC: 27,
612
+ SPACE: 32,
613
+ UPARROW: 38,
614
+ DOWNARROW: 40,
615
+ COMMA: 188
616
+ };
617
+
618
+ var self = {
619
+
620
+ /**
621
+ * Empties the result container and refills it with the array of json results in input
622
+ * @private
623
+ */
624
+ _displaySuggestions: function(data) {
625
+ ms.combobox.show();
626
+ ms.combobox.empty();
627
+
628
+ var resHeight = 0, // total height taken by displayed results.
629
+ nbGroups = 0;
630
+
631
+ if(_groups === null) {
632
+ self._renderComboItems(data);
633
+ resHeight = _comboItemHeight * data.length;
634
+ }
635
+ else {
636
+ for(var grpName in _groups) {
637
+ nbGroups += 1;
638
+ $('<div/>', {
639
+ 'class': 'ms-res-group',
640
+ html: grpName
641
+ }).appendTo(ms.combobox);
642
+ self._renderComboItems(_groups[grpName].items, true);
643
+ }
644
+ var _groupItemHeight = ms.combobox.find('.ms-res-group').outerHeight();
645
+ if(_groupItemHeight !== null) {
646
+ var tmpResHeight = nbGroups * _groupItemHeight;
647
+ resHeight = (_comboItemHeight * data.length) + tmpResHeight;
648
+ } else {
649
+ resHeight = _comboItemHeight * (data.length + nbGroups);
650
+ }
651
+ }
652
+
653
+ if(resHeight < ms.combobox.height() || resHeight <= cfg.maxDropHeight) {
654
+ ms.combobox.height(resHeight);
655
+ }
656
+ else if(resHeight >= ms.combobox.height() && resHeight > cfg.maxDropHeight) {
657
+ ms.combobox.height(cfg.maxDropHeight);
658
+ }
659
+
660
+ if(data.length === 1 && cfg.autoSelect === true) {
661
+ ms.combobox.children().filter(':last').addClass('ms-res-item-active');
662
+ }
663
+
664
+ if(data.length === 0 && ms.getRawValue() !== "") {
665
+ self._updateHelper(cfg.noSuggestionText);
666
+ ms.collapse();
667
+ }
668
+
669
+ if(data.length === 0){
670
+ ms.combobox.hide();
671
+ }
672
+ },
673
+
674
+ /**
675
+ * Returns an array of json objects from an array of strings.
676
+ * @private
677
+ */
678
+ _getEntriesFromStringArray: function(data) {
679
+ var json = [];
680
+ $.each(data, function(index, s) {
681
+ var entry = {};
682
+ entry[cfg.displayField] = entry[cfg.valueField] = $.trim(s);
683
+ json.push(entry);
684
+ });
685
+ return json;
686
+ },
687
+
688
+ /**
689
+ * Replaces html with highlighted html according to case
690
+ * @param html
691
+ * @private
692
+ */
693
+ _highlightSuggestion: function(html) {
694
+ var q = ms.input.val();
695
+ if(q.length === 0) {
696
+ return html; // nothing entered as input
697
+ }
698
+
699
+ if(cfg.matchCase === true) {
700
+ html = html.replace(new RegExp('(' + q + ')(?!([^<]+)?>)','g'), '<em>$1</em>');
701
+ }
702
+ else {
703
+ html = html.replace(new RegExp('(' + q + ')(?!([^<]+)?>)','gi'), '<em>$1</em>');
704
+ }
705
+ return html;
706
+ },
707
+
708
+ /**
709
+ * Moves the selected cursor amongst the list item
710
+ * @param dir - 'up' or 'down'
711
+ * @private
712
+ */
713
+ _moveSelectedRow: function(dir) {
714
+ if(!cfg.expanded) {
715
+ ms.expand();
716
+ }
717
+ var list, start, active, scrollPos;
718
+ list = ms.combobox.find(".ms-res-item");
719
+ if(dir === 'down') {
720
+ start = list.eq(0);
721
+ }
722
+ else {
723
+ start = list.filter(':last');
724
+ }
725
+ active = ms.combobox.find('.ms-res-item-active:first');
726
+ if(active.length > 0) {
727
+ if(dir === 'down') {
728
+ start = active.nextAll('.ms-res-item').first();
729
+ if(start.length === 0) {
730
+ start = list.eq(0);
731
+ }
732
+ scrollPos = ms.combobox.scrollTop();
733
+ ms.combobox.scrollTop(0);
734
+ if(start[0].offsetTop + start.outerHeight() > ms.combobox.height()) {
735
+ ms.combobox.scrollTop(scrollPos + _comboItemHeight);
736
+ }
737
+ }
738
+ else {
739
+ start = active.prevAll('.ms-res-item').first();
740
+ if(start.length === 0) {
741
+ start = list.filter(':last');
742
+ ms.combobox.scrollTop(_comboItemHeight * list.length);
743
+ }
744
+ if(start[0].offsetTop < ms.combobox.scrollTop()) {
745
+ ms.combobox.scrollTop(ms.combobox.scrollTop() - _comboItemHeight);
746
+ }
747
+ }
748
+ }
749
+ list.removeClass("ms-res-item-active");
750
+ start.addClass("ms-res-item-active");
751
+ },
752
+
753
+ /**
754
+ * According to given data and query, sort and add suggestions in their container
755
+ * @private
756
+ */
757
+ _processSuggestions: function(source) {
758
+ var json = null, data = source || cfg.data;
759
+ if(data !== null) {
760
+ if(typeof(data) === 'function'){
761
+ data = data.call(ms, ms.getRawValue());
762
+ }
763
+ if(typeof(data) === 'string') { // get results from ajax
764
+ $(ms).trigger('beforeload', [ms]);
765
+ var params = $.extend({query: ms.input.val()}, cfg.dataUrlParams);
766
+ $.ajax({
767
+ type: cfg.method,
768
+ url: data,
769
+ data: params,
770
+ success: function(asyncData){
771
+ json = typeof(asyncData) === 'string' ? JSON.parse(asyncData) : asyncData;
772
+ self._processSuggestions(json);
773
+ $(ms).trigger('load', [ms, json]);
774
+ if(self._asyncValues){
775
+ ms.setValue(typeof(self._asyncValues) === 'string' ? JSON.parse(self._asyncValues) : self._asyncValues);
776
+ self._renderSelection();
777
+ delete(self._asyncValues);
778
+ }
779
+ },
780
+ error: function(){
781
+ throw("Could not reach server");
782
+ }
783
+ });
784
+ return;
785
+ } else { // results from local array
786
+ if(data.length > 0 && typeof(data[0]) === 'string') { // results from array of strings
787
+ _cbData = self._getEntriesFromStringArray(data);
788
+ } else { // regular json array or json object with results property
789
+ _cbData = data[cfg.resultsField] || data;
790
+ }
791
+ }
792
+ var sortedData = cfg.mode === 'remote' ? _cbData : self._sortAndTrim(_cbData);
793
+ self._displaySuggestions(self._group(sortedData));
794
+
795
+ }
796
+ },
797
+
798
+ /**
799
+ * Render the component to the given input DOM element
800
+ * @private
801
+ */
802
+ _render: function(el) {
803
+ ms.setName(cfg.name); // make sure the form name is correct
804
+ // holds the main div, will relay the focus events to the contained input element.
805
+ ms.container = $('<div/>', {
806
+ 'class': 'ms-ctn form-control ' + (cfg.resultAsString ? 'ms-as-string ' : '') + cfg.cls +
807
+ (cfg.disabled === true ? ' ms-ctn-disabled' : '') +
808
+ (cfg.editable === true ? '' : ' ms-ctn-readonly') +
809
+ (cfg.hideTrigger === false ? '' : ' ms-no-trigger'),
810
+ style: cfg.style,
811
+ id: cfg.id
812
+ });
813
+ ms.container.focus($.proxy(handlers._onFocus, this));
814
+ ms.container.blur($.proxy(handlers._onBlur, this));
815
+ ms.container.keydown($.proxy(handlers._onKeyDown, this));
816
+ ms.container.keyup($.proxy(handlers._onKeyUp, this));
817
+
818
+ // holds the input field
819
+ ms.input = $('<input/>', $.extend({
820
+ type: 'text',
821
+ 'class': cfg.editable === true ? '' : ' ms-input-readonly',
822
+ readonly: !cfg.editable,
823
+ placeholder: cfg.placeholder,
824
+ disabled: cfg.disabled
825
+ }, cfg.inputCfg));
826
+
827
+ ms.input.focus($.proxy(handlers._onInputFocus, this));
828
+ ms.input.click($.proxy(handlers._onInputClick, this));
829
+
830
+ // holds the suggestions. will always be placed on focus
831
+ ms.combobox = $('<div/>', {
832
+ 'class': 'ms-res-ctn dropdown-menu'
833
+ }).height(cfg.maxDropHeight);
834
+
835
+ // bind the onclick and mouseover using delegated events (needs jQuery >= 1.7)
836
+ ms.combobox.on('click', 'div.ms-res-item', $.proxy(handlers._onComboItemSelected, this));
837
+ ms.combobox.on('mouseover', 'div.ms-res-item', $.proxy(handlers._onComboItemMouseOver, this));
838
+
839
+ ms.selectionContainer = $('<div/>', {
840
+ 'class': 'ms-sel-ctn'
841
+ });
842
+ ms.selectionContainer.click($.proxy(handlers._onFocus, this));
843
+
844
+ if(cfg.selectionPosition === 'inner') {
845
+ ms.selectionContainer.append(ms.input);
846
+ }
847
+ else {
848
+ ms.container.append(ms.input);
849
+ }
850
+
851
+ ms.helper = $('<span/>', {
852
+ 'class': 'ms-helper ' + cfg.infoMsgCls
853
+ });
854
+ self._updateHelper();
855
+ ms.container.append(ms.helper);
856
+
857
+
858
+ // Render the whole thing
859
+ $(el).replaceWith(ms.container);
860
+
861
+ switch(cfg.selectionPosition) {
862
+ case 'bottom':
863
+ ms.selectionContainer.insertAfter(ms.container);
864
+ if(cfg.selectionStacked === true) {
865
+ ms.selectionContainer.width(ms.container.width());
866
+ ms.selectionContainer.addClass('ms-stacked');
867
+ }
868
+ break;
869
+ case 'right':
870
+ ms.selectionContainer.insertAfter(ms.container);
871
+ ms.container.css('float', 'left');
872
+ break;
873
+ default:
874
+ ms.container.append(ms.selectionContainer);
875
+ break;
876
+ }
877
+
878
+
879
+ // holds the trigger on the right side
880
+ if(cfg.hideTrigger === false) {
881
+ ms.trigger = $('<div/>', {
882
+ 'class': 'ms-trigger',
883
+ html: '<div class="ms-trigger-ico"></div>'
884
+ });
885
+ ms.trigger.click($.proxy(handlers._onTriggerClick, this));
886
+ ms.container.append(ms.trigger);
887
+ }
888
+
889
+ // do not perform an initial call if we are using ajax unless we have initial values
890
+ if(cfg.value !== null){
891
+ if(typeof(cfg.data) === 'string'){
892
+ self._asyncValues = cfg.value;
893
+ self._processSuggestions();
894
+ } else {
895
+ self._processSuggestions();
896
+ ms.setValue(cfg.value);
897
+ self._renderSelection();
898
+ }
899
+
900
+ }
901
+
902
+ $("body").click(function(e) {
903
+ if(ms.container.hasClass('ms-ctn-focus') &&
904
+ ms.container.has(e.target).length === 0 &&
905
+ e.target.className.indexOf('ms-res-item') < 0 &&
906
+ e.target.className.indexOf('ms-close-btn') < 0 &&
907
+ ms.container[0] !== e.target) {
908
+ handlers._onBlur();
909
+ }
910
+ });
911
+
912
+ if(cfg.expanded === true) {
913
+ cfg.expanded = false;
914
+ ms.expand();
915
+ }
916
+ },
917
+
918
+ _renderComboItems: function(items, isGrouped) {
919
+ var ref = this, html = '';
920
+ $.each(items, function(index, value) {
921
+ var displayed = cfg.renderer !== null ? cfg.renderer.call(ref, value) : value[cfg.displayField];
922
+ var resultItemEl = $('<div/>', {
923
+ 'class': 'ms-res-item ' + (isGrouped ? 'ms-res-item-grouped ':'') +
924
+ (index % 2 === 1 && cfg.useZebraStyle === true ? 'ms-res-odd' : ''),
925
+ html: cfg.highlight === true ? self._highlightSuggestion(displayed) : displayed,
926
+ 'data-json': JSON.stringify(value)
927
+ });
928
+ resultItemEl.click($.proxy(handlers._onComboItemSelected, ref));
929
+ resultItemEl.mouseover($.proxy(handlers._onComboItemMouseOver, ref));
930
+ html += $('<div/>').append(resultItemEl).html();
931
+ });
932
+ ms.combobox.append(html);
933
+ _comboItemHeight = ms.combobox.find('.ms-res-item:first').outerHeight();
934
+ },
935
+
936
+ /**
937
+ * Renders the selected items into their container.
938
+ * @private
939
+ */
940
+ _renderSelection: function() {
941
+ var ref = this, w = 0, inputOffset = 0, items = [],
942
+ asText = cfg.resultAsString === true && !_hasFocus;
943
+
944
+ ms.selectionContainer.find('.ms-sel-item').remove();
945
+ if(ms._valueContainer !== undefined) {
946
+ ms._valueContainer.remove();
947
+ }
948
+
949
+ $.each(_selection, function(index, value){
950
+
951
+ var selectedItemEl, delItemEl,
952
+ selectedItemHtml = cfg.selectionRenderer !== null ? cfg.selectionRenderer.call(ref, value) : value[cfg.displayField];
953
+
954
+ var validCls = self._validateSingleItem(value[cfg.displayField]) ? '' : ' ms-sel-invalid';
955
+
956
+ // tag representing selected value
957
+ if(asText === true) {
958
+ selectedItemEl = $('<div/>', {
959
+ 'class': 'ms-sel-item ms-sel-text ' + cfg.selectionCls + validCls,
960
+ html: selectedItemHtml + (index === (_selection.length - 1) ? '' : ',')
961
+ }).data('json', value);
962
+ }
963
+ else {
964
+ selectedItemEl = $('<div/>', {
965
+ 'class': 'ms-sel-item ' + cfg.selectionCls + validCls,
966
+ html: selectedItemHtml
967
+ }).data('json', value);
968
+
969
+ if(cfg.disabled === false){
970
+ // small cross img
971
+ delItemEl = $('<span/>', {
972
+ 'class': 'ms-close-btn'
973
+ }).data('json', value).appendTo(selectedItemEl);
974
+
975
+ delItemEl.click($.proxy(handlers._onTagTriggerClick, ref));
976
+ }
977
+ }
978
+
979
+ items.push(selectedItemEl);
980
+ });
981
+ ms.selectionContainer.prepend(items);
982
+
983
+ // store the values, behaviour of multiple select
984
+ ms._valueContainer = $('<div/>', {
985
+ style: 'display: none;'
986
+ });
987
+ $.each(ms.getValue(), function(i, val){
988
+ var el = $('<input/>', {
989
+ type: 'hidden',
990
+ name: cfg.name,
991
+ value: val
992
+ });
993
+ el.appendTo(ms._valueContainer);
994
+ });
995
+ ms._valueContainer.appendTo(ms.selectionContainer);
996
+
997
+ if(cfg.selectionPosition === 'inner') {
998
+ ms.input.width(0);
999
+ inputOffset = ms.input.offset().left - ms.selectionContainer.offset().left;
1000
+ w = ms.container.width() - inputOffset - 42;
1001
+ ms.input.width(w);
1002
+ }
1003
+
1004
+ if(_selection.length === cfg.maxSelection){
1005
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1006
+ } else {
1007
+ ms.helper.hide();
1008
+ }
1009
+ },
1010
+
1011
+ /**
1012
+ * Select an item either through keyboard or mouse
1013
+ * @param item
1014
+ * @private
1015
+ */
1016
+ _selectItem: function(item) {
1017
+ if(cfg.maxSelection === 1){
1018
+ _selection = [];
1019
+ }
1020
+ ms.addToSelection(item.data('json'));
1021
+ item.removeClass('ms-res-item-active');
1022
+ if(cfg.expandOnFocus === false || _selection.length === cfg.maxSelection){
1023
+ ms.collapse();
1024
+ }
1025
+ if(!_hasFocus){
1026
+ ms.input.focus();
1027
+ } else if(_hasFocus && (cfg.expandOnFocus || _ctrlDown)){
1028
+ self._processSuggestions();
1029
+ if(_ctrlDown){
1030
+ ms.expand();
1031
+ }
1032
+ }
1033
+ },
1034
+
1035
+ /**
1036
+ * Sorts the results and cut them down to max # of displayed results at once
1037
+ * @private
1038
+ */
1039
+ _sortAndTrim: function(data) {
1040
+ var q = ms.getRawValue(),
1041
+ filtered = [],
1042
+ newSuggestions = [],
1043
+ selectedValues = ms.getValue();
1044
+ // filter the data according to given input
1045
+ if(q.length > 0) {
1046
+ $.each(data, function(index, obj) {
1047
+ var name = obj[cfg.displayField];
1048
+ if((cfg.matchCase === true && name.indexOf(q) > -1) ||
1049
+ (cfg.matchCase === false && name.toLowerCase().indexOf(q.toLowerCase()) > -1)) {
1050
+ if(cfg.strictSuggest === false || name.toLowerCase().indexOf(q.toLowerCase()) === 0) {
1051
+ filtered.push(obj);
1052
+ }
1053
+ }
1054
+ });
1055
+ }
1056
+ else {
1057
+ filtered = data;
1058
+ }
1059
+ // take out the ones that have already been selected
1060
+ $.each(filtered, function(index, obj) {
1061
+ if($.inArray(obj[cfg.valueField], selectedValues) === -1) {
1062
+ newSuggestions.push(obj);
1063
+ }
1064
+ });
1065
+ // sort the data
1066
+ if(cfg.sortOrder !== null) {
1067
+ newSuggestions.sort(function(a,b) {
1068
+ if(a[cfg.sortOrder] < b[cfg.sortOrder]) {
1069
+ return cfg.sortDir === 'asc' ? -1 : 1;
1070
+ }
1071
+ if(a[cfg.sortOrder] > b[cfg.sortOrder]) {
1072
+ return cfg.sortDir === 'asc' ? 1 : -1;
1073
+ }
1074
+ return 0;
1075
+ });
1076
+ }
1077
+ // trim it down
1078
+ if(cfg.maxSuggestions && cfg.maxSuggestions > 0) {
1079
+ newSuggestions = newSuggestions.slice(0, cfg.maxSuggestions);
1080
+ }
1081
+ return newSuggestions;
1082
+
1083
+ },
1084
+
1085
+ _group: function(data){
1086
+ // build groups
1087
+ if(cfg.groupBy !== null) {
1088
+ _groups = {};
1089
+
1090
+ $.each(data, function(index, value) {
1091
+ var props = cfg.groupBy.indexOf('.') > -1 ? cfg.groupBy.split('.') : cfg.groupBy;
1092
+ var prop = value[cfg.groupBy];
1093
+ if(typeof(props) != 'string'){
1094
+ prop = value;
1095
+ while(props.length > 0){
1096
+ prop = prop[props.shift()];
1097
+ }
1098
+ }
1099
+ if(_groups[prop] === undefined) {
1100
+ _groups[prop] = {title: prop, items: [value]};
1101
+ }
1102
+ else {
1103
+ _groups[prop].items.push(value);
1104
+ }
1105
+ });
1106
+ }
1107
+ return data;
1108
+ },
1109
+
1110
+ /**
1111
+ * Update the helper text
1112
+ * @private
1113
+ */
1114
+ _updateHelper: function(html) {
1115
+ ms.helper.html(html);
1116
+ if(!ms.helper.is(":visible")) {
1117
+ ms.helper.fadeIn();
1118
+ }
1119
+ },
1120
+
1121
+ /**
1122
+ * Validate an item against vtype or vregex
1123
+ * @private
1124
+ */
1125
+ _validateSingleItem: function(value){
1126
+ if(cfg.vregex !== null && cfg.vregex instanceof RegExp){
1127
+ return cfg.vregex.test(value);
1128
+ } else if(cfg.vtype !== null) {
1129
+ switch(cfg.vtype){
1130
+ case 'alpha':
1131
+ return (/^[a-zA-Z_]+$/).test(value);
1132
+ case 'alphanum':
1133
+ return (/^[a-zA-Z0-9_]+$/).test(value);
1134
+ case 'email':
1135
+ return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/).test(value);
1136
+ case 'url':
1137
+ return (/(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i).test(value);
1138
+ case 'ipaddress':
1139
+ return (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/).test(value);
1140
+ }
1141
+ }
1142
+ return true;
1143
+ }
1144
+ };
1145
+
1146
+ var handlers = {
1147
+ /**
1148
+ * Triggered when blurring out of the component
1149
+ * @private
1150
+ */
1151
+ _onBlur: function() {
1152
+ ms.container.removeClass('ms-ctn-focus');
1153
+ ms.collapse();
1154
+ _hasFocus = false;
1155
+ if(ms.getRawValue() !== '' && cfg.allowFreeEntries === true){
1156
+ var obj = {};
1157
+ obj[cfg.displayField] = obj[cfg.valueField] = ms.getRawValue().trim();
1158
+ ms.addToSelection(obj);
1159
+ }
1160
+ self._renderSelection();
1161
+
1162
+ if(ms.isValid() === false) {
1163
+ ms.container.addClass(cfg.invalidCls);
1164
+ }
1165
+
1166
+ else if(ms.input.val() !== '' && cfg.allowFreeEntries === false) {
1167
+ ms.empty();
1168
+ self._updateHelper('');
1169
+ }
1170
+
1171
+ $(ms).trigger('blur', [ms]);
1172
+ },
1173
+
1174
+ /**
1175
+ * Triggered when hovering an element in the combo
1176
+ * @param e
1177
+ * @private
1178
+ */
1179
+ _onComboItemMouseOver: function(e) {
1180
+ ms.combobox.children().removeClass('ms-res-item-active');
1181
+ $(e.currentTarget).addClass('ms-res-item-active');
1182
+ },
1183
+
1184
+ /**
1185
+ * Triggered when an item is chosen from the list
1186
+ * @param e
1187
+ * @private
1188
+ */
1189
+ _onComboItemSelected: function(e) {
1190
+ self._selectItem($(e.currentTarget));
1191
+ },
1192
+
1193
+ /**
1194
+ * Triggered when focusing on the container div. Will focus on the input field instead.
1195
+ * @private
1196
+ */
1197
+ _onFocus: function() {
1198
+ ms.input.focus();
1199
+ },
1200
+
1201
+ /**
1202
+ * Triggered when clicking on the input text field
1203
+ * @private
1204
+ */
1205
+ _onInputClick: function(){
1206
+ if (ms.isDisabled() === false && _hasFocus) {
1207
+ if (cfg.toggleOnClick === true) {
1208
+ if (cfg.expanded){
1209
+ ms.collapse();
1210
+ } else {
1211
+ ms.expand();
1212
+ }
1213
+ }
1214
+ }
1215
+ },
1216
+
1217
+ /**
1218
+ * Triggered when focusing on the input text field.
1219
+ * @private
1220
+ */
1221
+ _onInputFocus: function() {
1222
+ if(ms.isDisabled() === false && !_hasFocus) {
1223
+ _hasFocus = true;
1224
+ ms.container.addClass('ms-ctn-focus');
1225
+ ms.container.removeClass(cfg.invalidCls);
1226
+
1227
+ var curLength = ms.getRawValue().length;
1228
+ if(cfg.expandOnFocus === true){
1229
+ ms.expand();
1230
+ }
1231
+
1232
+ if(_selection.length === cfg.maxSelection) {
1233
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1234
+ } else if(curLength < cfg.minChars) {
1235
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - curLength));
1236
+ }
1237
+
1238
+ self._renderSelection();
1239
+ $(ms).trigger('focus', [ms]);
1240
+ }
1241
+ },
1242
+
1243
+ /**
1244
+ * Triggered when the user presses a key while the component has focus
1245
+ * This is where we want to handle all keys that don't require the user input field
1246
+ * since it hasn't registered the key hit yet
1247
+ * @param e keyEvent
1248
+ * @private
1249
+ */
1250
+ _onKeyDown: function(e) {
1251
+ // check how tab should be handled
1252
+ var active = ms.combobox.find('.ms-res-item-active:first'),
1253
+ freeInput = ms.input.val();
1254
+ $(ms).trigger('keydown', [ms, e]);
1255
+
1256
+ if(e.keyCode === KEYCODES.TAB && (cfg.useTabKey === false ||
1257
+ (cfg.useTabKey === true && active.length === 0 && ms.input.val().length === 0))) {
1258
+ handlers._onBlur();
1259
+ return;
1260
+ }
1261
+ switch(e.keyCode) {
1262
+ case KEYCODES.BACKSPACE:
1263
+ if(freeInput.length === 0 && ms.getSelection().length > 0 && cfg.selectionPosition === 'inner') {
1264
+ _selection.pop();
1265
+ self._renderSelection();
1266
+ $(ms).trigger('selectionchange', [ms, ms.getSelection()]);
1267
+ ms.input.attr('placeholder', (cfg.selectionPosition === 'inner' && this.getValue().length > 0) ? '' : cfg.placeholder);
1268
+ ms.input.focus();
1269
+ e.preventDefault();
1270
+ }
1271
+ break;
1272
+ case KEYCODES.TAB:
1273
+ case KEYCODES.ESC:
1274
+ case KEYCODES.ENTER:
1275
+ case KEYCODES.COMMA:
1276
+ e.preventDefault();
1277
+ break;
1278
+ case KEYCODES.CTRL:
1279
+ _ctrlDown = true;
1280
+ break;
1281
+ case KEYCODES.DOWNARROW:
1282
+ e.preventDefault();
1283
+ self._moveSelectedRow("down");
1284
+ break;
1285
+ case KEYCODES.UPARROW:
1286
+ e.preventDefault();
1287
+ self._moveSelectedRow("up");
1288
+ break;
1289
+ default:
1290
+ if(_selection.length === cfg.maxSelection) {
1291
+ e.preventDefault();
1292
+ }
1293
+ break;
1294
+ }
1295
+ },
1296
+
1297
+ /**
1298
+ * Triggered when a key is released while the component has focus
1299
+ * @param e
1300
+ * @private
1301
+ */
1302
+ _onKeyUp: function(e) {
1303
+ var freeInput = ms.getRawValue(),
1304
+ inputValid = $.trim(ms.input.val()).length > 0 &&
1305
+ (!cfg.maxEntryLength || $.trim(ms.input.val()).length <= cfg.maxEntryLength),
1306
+ selected,
1307
+ obj = {};
1308
+
1309
+ $(ms).trigger('keyup', [ms, e]);
1310
+
1311
+ clearTimeout(_timer);
1312
+
1313
+ // collapse if escape, but keep focus.
1314
+ if(e.keyCode === KEYCODES.ESC && cfg.expanded) {
1315
+ ms.combobox.hide();
1316
+ }
1317
+ // ignore a bunch of keys
1318
+ if((e.keyCode === KEYCODES.TAB && cfg.useTabKey === false) || (e.keyCode > KEYCODES.ENTER && e.keyCode < KEYCODES.SPACE)) {
1319
+ if(e.keyCode === KEYCODES.CTRL){
1320
+ _ctrlDown = false;
1321
+ }
1322
+ return;
1323
+ }
1324
+ switch(e.keyCode) {
1325
+ case KEYCODES.UPARROW:
1326
+ case KEYCODES.DOWNARROW:
1327
+ e.preventDefault();
1328
+ break;
1329
+ case KEYCODES.ENTER:
1330
+ case KEYCODES.TAB:
1331
+ case KEYCODES.COMMA:
1332
+ if(e.keyCode !== KEYCODES.COMMA || cfg.useCommaKey === true) {
1333
+ e.preventDefault();
1334
+ if(cfg.expanded === true){ // if a selection is performed, select it and reset field
1335
+ selected = ms.combobox.find('.ms-res-item-active:first');
1336
+ if(selected.length > 0) {
1337
+ self._selectItem(selected);
1338
+ return;
1339
+ }
1340
+ }
1341
+ // if no selection or if freetext entered and free entries allowed, add new obj to selection
1342
+ if(inputValid === true && cfg.allowFreeEntries === true) {
1343
+ obj[cfg.displayField] = obj[cfg.valueField] = freeInput.trim();
1344
+ ms.addToSelection(obj);
1345
+ ms.collapse(); // reset combo suggestions
1346
+ ms.input.focus();
1347
+ }
1348
+ break;
1349
+ }
1350
+ default:
1351
+ if(_selection.length === cfg.maxSelection){
1352
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1353
+ }
1354
+ else {
1355
+ if(freeInput.length < cfg.minChars) {
1356
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - freeInput.length));
1357
+ if(cfg.expanded === true) {
1358
+ ms.collapse();
1359
+ }
1360
+ }
1361
+ else if(cfg.maxEntryLength && freeInput.length > cfg.maxEntryLength) {
1362
+ self._updateHelper(cfg.maxEntryRenderer.call(this, freeInput.length - cfg.maxEntryLength));
1363
+ if(cfg.expanded === true) {
1364
+ ms.collapse();
1365
+ }
1366
+ }
1367
+ else {
1368
+ ms.helper.hide();
1369
+ if(cfg.minChars <= freeInput.length){
1370
+ _timer = setTimeout(function() {
1371
+ if(cfg.expanded === true) {
1372
+ self._processSuggestions();
1373
+ } else {
1374
+ ms.expand();
1375
+ }
1376
+ }, cfg.typeDelay);
1377
+ }
1378
+ }
1379
+ }
1380
+ break;
1381
+ }
1382
+ },
1383
+
1384
+ /**
1385
+ * Triggered when clicking upon cross for deletion
1386
+ * @param e
1387
+ * @private
1388
+ */
1389
+ _onTagTriggerClick: function(e) {
1390
+ ms.removeFromSelection($(e.currentTarget).data('json'));
1391
+ },
1392
+
1393
+ /**
1394
+ * Triggered when clicking on the small trigger in the right
1395
+ * @private
1396
+ */
1397
+ _onTriggerClick: function() {
1398
+ if(ms.isDisabled() === false && !(cfg.expandOnFocus === true && _selection.length === cfg.maxSelection)) {
1399
+ $(ms).trigger('triggerclick', [ms]);
1400
+ if(cfg.expanded === true) {
1401
+ ms.collapse();
1402
+ } else {
1403
+ var curLength = ms.getRawValue().length;
1404
+ if(curLength >= cfg.minChars){
1405
+ ms.input.focus();
1406
+ ms.expand();
1407
+ } else {
1408
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - curLength));
1409
+ }
1410
+ }
1411
+ }
1412
+ }
1413
+ };
1414
+
1415
+ // startup point
1416
+ if(element !== null) {
1417
+ self._render(element);
1418
+ }
1419
+ };
1420
+
1421
+ $.fn.magicSuggest = function(options) {
1422
+ var obj = $(this);
1423
+
1424
+ if(obj.size() === 1 && obj.data('magicSuggest')) {
1425
+ return obj.data('magicSuggest');
1426
+ }
1427
+
1428
+ obj.each(function(i) {
1429
+ // assume $(this) is an element
1430
+ var cntr = $(this);
1431
+
1432
+ // Return early if this element already has a plugin instance
1433
+ if(cntr.data('magicSuggest')){
1434
+ return;
1435
+ }
1436
+
1437
+ if(this.nodeName.toLowerCase() === 'select'){ // rendering from select
1438
+ options.data = [];
1439
+ options.value = [];
1440
+ $.each(this.children, function(index, child){
1441
+ if(child.nodeName && child.nodeName.toLowerCase() === 'option'){
1442
+ options.data.push({id: child.value, name: child.text});
1443
+ if($(child).attr('selected')){
1444
+ options.value.push(child.value);
1445
+ }
1446
+ }
1447
+ });
1448
+ }
1449
+
1450
+ var def = {};
1451
+ // set values from DOM container element
1452
+ $.each(this.attributes, function(i, att){
1453
+ def[att.name] = att.name === 'value' && att.value !== '' ? JSON.parse(att.value) : att.value;
1454
+ });
1455
+
1456
+ var field = new MagicSuggest(this, $.extend([], $.fn.magicSuggest.defaults, options, def));
1457
+ cntr.data('magicSuggest', field);
1458
+ field.container.data('magicSuggest', field);
1459
+ });
1460
+
1461
+ if(obj.size() === 1) {
1462
+ return obj.data('magicSuggest');
1463
+ }
1464
+ return obj;
1465
+ };
1466
+
1467
+ $.fn.magicSuggest.defaults = {};
1468
+ })(jQuery);