1/**
2 * Selectize module
3 *
4 * Most basic usage:
5 * `WHMCS.selectize.register('#mySelect');`
6 * - This will selectize the <select id="mySelect"></select>. See
7 * .register() for more specifics.
8 *
9 * Pre-made usage:
10 * `WHMCS.selectize.clientSearch();`
11 * - selectize all '.selectize-client-search'
12 *
13 * `WHMCS.selectize.users(selector, options)`
14 * - selectize a given selector with a static array of options (user objects)
15 *
16 */
17(function(module) {
18 if (!WHMCS.hasModule('selectize')) {
19 WHMCS.loadModule('selectize', module);
20 }
21})(
22function () {
23 /**
24 * Search-on-type client select & click "#goButton" on 'change' event
25 * - will bind to <select> with '.selectize-client-search'
26 * - <select> needs data-search-url attribute for 'load' event
27 *
28 * @returns {Selectize}
29 */
30 this.clientSearch = function () {
31 var itemDecorator = function(item, escape) {
32 if (typeof dropdownSelectClient === "function") {
33 if (jQuery(".selectize-dropdown-content > div").length === 0) {
34 // updates DOM for admin/supporttickets.php
35 dropdownSelectClient(
36 escape(item.id),
37 escape(item.name)
38 + (item.companyname ? ' (' + escape(item.companyname) + ')' : '')
39 + (item.id > 0 ? ' - #' + escape(item.id) : ''),
40 escape(item.email)
41 );
42 }
43 }
44 return '<div class="client-name"><span class="name">' + escape(item.name) +
45 (item.companyname ? ' (' + escape(item.companyname) + ')' : '') +
46 (item.id > 0 ? ' - #' + escape(item.id) : '') + '</span></div>';
47 };
48
49 var selector ='.selectize-client-search';
50 var selectElement = jQuery(selector);
51
52 var module = this;
53 var selectized = [];
54 selectElement.each(function (){
55 var element = $(this);
56 var configuration = {
57 'valueField': element.data('value-field'),
58 allowEmptyOption: (element.data('allow-empty-option') === 1),
59 'labelField': 'name', //legacy? shouldn't be required with render
60 'render': {
61 item: itemDecorator
62 },
63 optgroupField: 'status',
64 optgroupLabelField: 'name',
65 optgroupValueField: 'id',
66 optgroups: [
67 {$order: 1, id: 'active', name: element.data('active-label')},
68 {$order: 2, id: 'inactive', name: element.data('inactive-label')}
69 ],
70 'load': module.builder.onLoadEvent(
71 element.data('search-url'),
72 function (query) {
73 return {
74 dropdownsearchq: query,
75 clientId: instance.currentValue,
76 showNoneOption: (element.data('allow-empty-option') === 1),
77 };
78 }
79 ),
80 'onChange': function(value) {
81 // Updates DOM for admin/supporttickets.php
82 if (value && typeof dropdownSelectClient === 'function') {
83 value = parseInt(value);
84 var newSelection = jQuery(".selectize-dropdown-content div[data-value|='" + value + "']");
85 dropdownSelectClient(
86 value,
87 newSelection.children("span.name").text(),
88 newSelection.children("span.email").text()
89 );
90 }
91 }
92 };
93
94 var instance = module.clients(element, undefined, configuration);
95
96 instance.on('change', module.builder.onChangeEvent(instance, '#goButton'));
97
98 return selectized.push(instance);
99 });
100
101 if (selectized.length > 1) {
102 return selectized;
103 }
104
105 return selectized[0];
106
107 };
108
109 this.userSearch = function () {
110 var itemDecorator = function(item, escape) {
111 var idAppend = '',
112 isNumeric = !isNaN(item.id);
113
114 if (isNumeric && item.id > 0) {
115 idAppend = ' - #' + escape(item.id);
116 }
117 return '<div><span class="name">' + escape(item.name) + idAppend + '</span></div>';
118 };
119
120 var selector ='.selectize-user-search';
121 var selectElement = jQuery(selector);
122
123 var module = this;
124 var selectized = [];
125 selectElement.each(function (){
126 var element = $(this);
127 var configuration = {
128 'valueField': element.data('value-field'),
129 'labelField': 'name',
130 'render': {
131 item: itemDecorator
132 },
133 'preload': false,
134 'load': module.builder.onLoadEvent(
135 element.data('search-url'),
136 function (query) {
137 return {
138 token: csrfToken,
139 search: query
140 };
141 }
142 )
143 };
144
145 var instance = module.users(selector, undefined, configuration);
146
147 return selectized.push(instance);
148 });
149
150 if (selectized.length > 1) {
151 return selectized;
152 }
153
154 return selectized[0];
155
156 };
157
158 this.serviceSearch = function () {
159 var itemDecorator = function(item) {
160 var newDiv = $('<div>');
161 if (item.color) {
162 newDiv.css('background-color', item.color);
163 }
164 newDiv.append(
165 $('<span>').attr('class', 'name').text(item.name)
166 )
167 return newDiv;
168 };
169
170 var selector ='.selectize-service-search';
171 var selectElement = jQuery(selector);
172
173 var module = this;
174 var selectized = [];
175 selectElement.each(function (){
176 var element = $(this);
177 var configuration = {
178 'valueField': 'id',
179 'labelField': 'name',
180 'render': {
181 item: itemDecorator
182 },
183 'preload': true,
184 'load': module.builder.onLoadEvent(
185 element.data('search-url'),
186 function (query) {
187 return {
188 token: csrfToken,
189 search: query
190 };
191 }
192 )
193 };
194
195 var instance = module.services(selector, undefined, configuration);
196
197 return selectized.push(instance);
198 });
199
200 if (selectized.length > 1) {
201 return selectized;
202 }
203
204 return selectized[0];
205 };
206
207 this.productSearch = function() {
208 var selector = '.selectize-product-search',
209 selectElement = jQuery(selector),
210 module = this,
211 selectized = [],
212 itemDecorator = function(data, escape) {
213 var newDiv = jQuery('<div>'),
214 newSpan = jQuery('<span>').attr('class', 'name').text(escape(data.name));
215 newDiv.append(newSpan);
216 return newDiv;
217 };
218
219 selectElement.each(function() {
220 var element = jQuery(this),
221 configuration = {
222 'valueField': 'id',
223 'labelField': 'name',
224 'render': {
225 item: itemDecorator
226 },
227 optgroupField: 'groupid',
228 optgroupLabelField: 'name',
229 optgroupValueField: 'id',
230 'preload': true,
231 'load': module.builder.onLoadEvent(
232 element.data('search-url'),
233 function (query) {
234 return {
235 token: csrfToken,
236 search: query,
237 };
238 }
239 ),
240 'onLoad': function(data) {
241 var instance = this,
242 listItems = jQuery('.product-recommendations-wrapper li');
243 data.forEach(function(item) {
244 if (listItems.find('input[value="' + item.id + '"]').length) {
245 instance.removeOption(item.id);
246 return;
247 }
248 instance.addOptionGroup(item.groupid, {
249 $order: item.order,
250 name: item.group,
251 });
252 });
253 },
254 'onBlur': function() {
255 this.clear();
256 },
257 'onItemAdd': function(value) {
258 var listItems = jQuery('.product-recommendations-wrapper li'),
259 existingItems = listItems.find('input[value="' + value + '"]').length,
260 recommendationAlert = jQuery('div.recommendation-alert'),
261 isChanged = false;
262 if (value && existingItems < 1) {
263 var newSelection = jQuery(".selectize-dropdown-content div[data-value|='" + value + "']"),
264 clonableItem = jQuery('.product-recommendations-wrapper .clonable-item'),
265 parentList = clonableItem.closest('ul'),
266 clonedItem = clonableItem.clone().removeClass('hidden clonable-item');
267 clonedItem.find('a span.recommendation-name').text(
268 newSelection.siblings('div.optgroup-header').text() +
269 ' - ' +
270 newSelection.children("span.name").text()
271 );
272 jQuery('<input>').attr({
273 type: 'hidden',
274 name: 'productRecommendations[]',
275 value: value
276 }).appendTo(clonedItem);
277 clonedItem.find('input').val(value);
278 clonedItem.appendTo(parentList);
279 instance.removeOption(value);
280 isChanged = true;
281 }
282 if (listItems.length > 0) {
283 jQuery('.product-recommendations-wrapper .placeholder-list-item').addClass('hidden');
284 isChanged = true;
285 }
286 if (isChanged && recommendationAlert.not(':visible')) {
287 jQuery('.recommendation-alert').removeClass('hidden');
288 }
289 }
290 },
291 instance = module.products(selector, undefined, configuration);
292
293 return selectized.push(instance);
294 });
295
296 if (selectized.length > 1) {
297 return selectized;
298 }
299
300 return selectized[0];
301 };
302
303 /**
304 * Generic selectize of users
305 * - no 'change' or 'load' events
306 *
307 * @param selector
308 * @param options
309 * @param configuration
310 * @returns {Selectize}
311 */
312 this.clients = function (selector, options, configuration) {
313 var instance = this.register(
314 selector,
315 options,
316 WHMCS.selectize.optionDecorator.client,
317 configuration
318 );
319
320 instance.settings.searchField = ['name', 'email', 'companyname'];
321
322 return instance;
323 };
324
325 this.users = function (selector, options, configuration) {
326 var instance = this.register(
327 selector,
328 options,
329 WHMCS.selectize.optionDecorator.user,
330 configuration
331 );
332
333 instance.settings.searchField = ['name', 'email'];
334
335 return instance;
336 };
337
338 this.services = function (selector, options, configuration) {
339 var instance = this.register(
340 selector,
341 options,
342 WHMCS.selectize.optionDecorator.service,
343 configuration
344 );
345
346 instance.settings.searchField = ['name', 'noResults'];
347
348 return instance;
349 };
350
351 this.billingContacts = function (selector, options, configuration) {
352 var instance = this.register(
353 selector,
354 options,
355 WHMCS.selectize.optionDecorator.billingContact,
356 configuration
357 );
358
359 instance.settings.searchField = ['name', 'email', 'companyname', 'address'];
360
361 return instance;
362 };
363
364 this.payMethods = function (selector, options, configuration) {
365 var instance = this.register(
366 selector,
367 options,
368 WHMCS.selectize.optionDecorator.payMethod,
369 configuration
370 );
371
372 instance.settings.searchField = ['description', 'shortAccountNumber', 'type', 'payMethodType'];
373
374 return instance;
375 };
376
377 this.products = function (selector, options, configuration) {
378 var instance = this.register(
379 selector,
380 options,
381 WHMCS.selectize.optionDecorator.product,
382 configuration
383 );
384
385 instance.settings.lockOptgroupOrder = true;
386 instance.settings.searchField = ['id', 'name', 'noResults'];
387
388 return instance;
389 };
390
391 this.html = function (selector, options, configuration) {
392 var instance = this.register(
393 selector,
394 options,
395 function(item, escape) {
396 return '<div class="item">' + item.html + '</div>';
397 },
398 configuration
399 );
400
401 instance.settings.searchField = ['html'];
402
403 return instance;
404 };
405
406 this.simple = function (selector, options, configuration) {
407 var instance = this.register(
408 selector,
409 options,
410 function(item, escape) {
411 return '<div class="item">' + item.value + '</div>';
412 },
413 configuration
414 );
415
416 instance.settings.searchField = ['value'];
417
418 return instance;
419 };
420 /**
421 * Arguments:
422 * selector
423 * CSS selector of the <select> element to selectize
424 *
425 * options
426 * The second argument is a JS array of objects that will be decorated
427 * into <option>s.
428 *
429 * decorator
430 * The third argument is the option decorator. By default, it will
431 * decorate using the userDecorator. Value can be a global function,
432 * lambda, or fq function. This argument will _not_ be applied when
433 * configuration supplies the .render.item or .render.option properties
434 *
435 * configuration
436 * configuration settings to use during Selectize initialization
437 *
438 *
439 * Some Assumptions & Default settings:
440 * settings.valueField and settings.labelField
441 * These are set to 'id' by default; change as needed
442 *
443 * settings.searchField
444 * Is empty by default; change as needed
445 *
446 * option and item decoration
447 * this.optionDecorator.user will be applied by default if nothing is
448 * supplied (by means of the decorator arg or within the configuration arg)
449 *
450 * @copyright Copyright (c) WHMCS Limited 2005-2018
451 * @license http://www.whmcs.com/license/ WHMCS Eula
452 */
453 this.register = function (selector, options, decorator, configuration) {
454 var self = this;
455 var element = jQuery(selector);
456
457 var instance = self.builder.init(element, configuration);
458
459 // add item & option decorator if not provided in configuration
460 var itemDecorator = self.builder.itemDecorator(decorator);
461 if (typeof configuration === "undefined") {
462 instance.settings.render.item = itemDecorator;
463 instance.settings.render.option = itemDecorator;
464 } else if (typeof configuration.render === "undefined") {
465 instance.settings.render.item = itemDecorator;
466 instance.settings.render.option = itemDecorator;
467 } else {
468 if (typeof configuration.render.item === "undefined") {
469 instance.settings.render.item = itemDecorator;
470 }
471 if (typeof configuration.render.option === "undefined") {
472 instance.settings.render.option = itemDecorator;
473 }
474 }
475
476 this.builder.addOptions(instance, options);
477
478
479 return instance;
480 };
481
482 this.optionDecorator = {
483 client: function(item, escape) {
484 var name = escape(item.name),
485 companyname = '',
486 descriptor = '',
487 email = '';
488
489 if (item.companyname) {
490 companyname = ' (' + escape(item.companyname) + ')';
491 }
492
493 if (typeof item.descriptor === "undefined") {
494 descriptor = (item.id > 0 ? ' - #' + escape(item.id) : '');
495 } else {
496 descriptor = escape(item.descriptor);
497 }
498
499 if (item.email) {
500 email = '<span class="email">' + escape(item.email) + '</span>';
501 }
502
503 return '<div>'
504 + '<span class="name">' + name + companyname + descriptor + '</span>'
505 + email
506 + '</div>';
507 },
508 user: function(item, escape) {
509 var name = escape(item.name),
510 descriptor = '',
511 email = '',
512 isNumericId = !isNaN(item.id);
513
514 if (typeof item.descriptor === "undefined") {
515 descriptor = (isNumericId && item.id > 0 ? ' - #' + escape(item.id) : '');
516 } else {
517 descriptor = escape(item.descriptor);
518 }
519
520 if (isNumericId && item.id > 0 && item.email) {
521 email = '<span class="email">' + escape(item.email) + '</span>';
522 }
523
524 return '<div>'
525 + '<span class="name">' + name + descriptor + '</span>'
526 + email
527 + '</div>';
528 },
529 billingContact: function(item, escape) {
530 var name = escape(item.name),
531 companyname = '',
532 descriptor = '',
533 email = '',
534 address = '';
535
536 if (item.companyname) {
537 companyname = ' (' + escape(item.companyname) + ')';
538 }
539
540 if (typeof item.descriptor === "undefined") {
541 descriptor = (item.id > 0 ? ' - #' + escape(item.id) : '');
542 } else {
543 descriptor = escape(item.descriptor);
544 }
545
546 if (item.email) {
547 email = '<span class="email">' + escape(item.email) + '</span>';
548 }
549
550 if (item.address) {
551 address = '<span class="email">' + escape(item.address) + '</span>';
552 }
553
554 return '<div>'
555 + '<span class="name">' + name + companyname + descriptor + '</span>'
556 + email
557 + address
558 + '</div>';
559 },
560 payMethod: function(item, escape) {
561 var brandIcon = '',
562 description = '',
563 isDefault = '',
564 shortAccountNumber = '',
565 detail1 = '';
566
567 if (item.brandIcon) {
568 brandIcon = '<i class="' + item.brandIcon + '"></i>';
569 }
570 if (item.isDefault) {
571 isDefault = ' <i class="fal fa-user-check"></i>';
572 }
573
574 if (item.description) {
575 description = item.description;
576 }
577 if (item.shortAccountNumber) {
578 if (description.indexOf(item.shortAccountNumber) === -1) {
579 shortAccountNumber = '(' + escape(item.shortAccountNumber) + ')';
580 }
581 }
582
583 if (item.detail1) {
584 detail1 = '<span class="mouse">' + escape(item.detail1) + '</span>';
585 }
586
587 return '<div>'
588 + '<span class="name"> '
589 + brandIcon + ' '
590 + description + ' '
591 + shortAccountNumber + ' '
592 + ' ' + detail1 + ' '
593 + isDefault
594 + '</span>'
595 + '</div>';
596 },
597 service: function (item, escape) {
598 var color = '';
599 if (item.color) {
600 color = ' style="background-color: ' + item.color + ';"';
601 }
602 return '<div' + color + '><span class="name">' + escape(item.name) + '</span>'
603 + (item.noResults ? '<span class="email">' + escape(item.noResults) + '</span>' : '') +
604 '</div>';
605 },
606 product: function (item, escape) {
607 return '<div><span class="name">' + escape(item.name) + '</span>'
608 + (item.noResults ? '<span class="email">' + escape(item.noResults) + '</span>' : '') +
609 '</div>';
610 }
611 };
612 this.builder = {
613 init: function (element, configuration)
614 {
615 var merged,
616 defaults = {
617 plugins: ['whmcs_no_results'],
618 valueField: 'id',
619 labelField: 'id',
620 create: false,
621 maxItems: 1,
622 preload: 'focus'
623 };
624
625 if (typeof configuration === "undefined") {
626 configuration = {};
627 }
628 merged = jQuery.extend({}, defaults, configuration);
629
630 var thisSelectize = element.selectize(merged);
631 /**
632 * selectize assigns any items to an array. In order to be able to
633 * run additional functions on this (like auto-submit and clear).
634 *
635 * @link https://github.com/brianreavis/selectize.js/blob/master/examples/api.html
636 */
637 thisSelectize = thisSelectize[0].selectize;
638
639 thisSelectize.currentValue = '';
640
641 thisSelectize.on('focus', function () {
642 thisSelectize.currentValue = thisSelectize.getValue();
643 thisSelectize.clear();
644 });
645 thisSelectize.on('blur', function () {
646 var thisValue = thisSelectize.getValue(),
647 isNumeric = !(isNaN(thisValue)),
648 minValue = 1;
649 if (element.data('allow-empty-option') === 1) {
650 minValue = 0;
651 }
652 if (
653 thisValue === ''
654 || (isNumeric && (thisValue < minValue))
655 ) {
656 thisSelectize.setValue(thisSelectize.currentValue);
657 }
658 });
659
660 return thisSelectize;
661 },
662 addOptions: function (selectize, options) {
663 if (typeof options !== "undefined" && options.length) {
664 selectize.addOption(options);
665 }
666 },
667 itemDecorator: function (decorator) {
668 if (typeof decorator === "function") {
669 return decorator;
670 } else if (typeof decorator === "undefined") {
671 return WHMCS.selectize.optionDecorator.user;
672 }
673 },
674 onLoadEvent: function (searchUrl, dataCallback) {
675 return function (query, callback) {
676 jQuery.ajax({
677 url: searchUrl,
678 type: 'POST',
679 dataType: 'json',
680 data: dataCallback(query),
681 error: function () {
682 callback();
683 },
684 success: function (res) {
685 callback(res);
686 }
687 });
688 };
689 },
690 onChangeEvent: function (instance, onChangeSelector) {
691 var onChange;
692 if (typeof onChangeSelector !== "undefined") {
693 onChange = function (value) {
694 var changeSelector = jQuery(onChangeSelector);
695 if (changeSelector.length) {
696 if (
697 !(isNaN(instance.currentValue))
698 && instance.currentValue > 0
699 && (value.length && value !== instance.currentValue)
700 ) {
701 changeSelector.click();
702 }
703 }
704 }
705 }
706
707 return onChange;
708 }
709 };
710
711 return this;
712});
713