sharkey-web 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);