1/**
2 * WHMCS UI module
3 *
4 * @copyright Copyright (c) WHMCS Limited 2005-2017
5 * @license http://www.whmcs.com/license/ WHMCS Eula
6 */
7(function(module) {
8 if (!WHMCS.hasModule('ui')) {
9 WHMCS.loadModule('ui', module);
10 }
11})({
12/**
13 * Confirmation PopUp
14 */
15confirmation: function () {
16
17 /**
18 * @type {Array} Registered confirmation root selectors
19 */
20 var toggles = [];
21
22 /**
23 * Register/Re-Register all confirmation elements with jQuery
24 * By default all elements of data toggle "confirmation" will be registered
25 *
26 * @param {(string|undefined)} rootSelector
27 * @return {Array} array of registered toggles
28 */
29 this.register = function (rootSelector) {
30 if (typeof rootSelector === 'undefined') {
31 rootSelector = '[data-toggle=confirmation]';
32 }
33 if (toggles.indexOf(rootSelector) < 0) {
34 toggles.push(rootSelector);
35 }
36
37 jQuery(rootSelector).confirmation({
38 rootSelector: rootSelector
39 });
40
41 return toggles;
42 };
43
44 return this;
45},
46
47/**
48 * Data Driven Table
49 */
50dataTable: function () {
51
52 /**
53 * @type {{}}
54 */
55 this.tables = {};
56
57 /**
58 * Register all tables on page with the class "data-driven"
59 */
60 this.register = function () {
61 var self = this;
62 jQuery('table.data-driven').each(function (i, table) {
63 self.getTableById(table.id, undefined);
64 });
65 };
66
67 /**
68 * Get a table by id; create table object on fly as necessary
69 *
70 * @param {string} id
71 * @param {({}|undefined)} options
72 * @returns {DataTable}
73 */
74 this.getTableById = function (id, options) {
75 var self = this;
76 var el = jQuery('#' + id);
77 if (typeof self.tables[id] === 'undefined') {
78 if (typeof options === 'undefined') {
79 options = {
80 dom: '<"listtable"ift>pl',
81 paging: false,
82 lengthChange: false,
83 searching: false,
84 ordering: true,
85 info: false,
86 autoWidth: true,
87 columns: [],
88 lengthMenu: [10, 25, 50, 100, 500, 1000],
89 language: {
90 emptyTable: (el.data('langEmptyTable')) ? el.data('langEmptyTable') : "No records found"
91 }
92 };
93 }
94 jQuery.each(el.data(), function (key, value) {
95 if (typeof value === 'undefined') {
96 return;
97 }
98 if (key === 'ajaxUrl') {
99 options.ajax = {
100 url: value
101 };
102 return;
103 }
104 if (key === 'lengthChange') {
105 options.lengthChange = value;
106 return;
107 }
108 if (key === 'pageLength') {
109 options.pageLength = value;
110 return;
111 }
112 if (key === 'langEmptyTable') {
113 if (typeof options.language === "undefined") {
114 options.language = {};
115 }
116 options.language.emptyTable = value;
117 return
118 }
119 if (key === 'langZeroRecords') {
120 if (typeof options.language === "undefined") {
121 options.language = {};
122 }
123 options.language.zeroRecords = value;
124 return
125 }
126 options.key = value;
127 });
128 jQuery.each(el.find('th'), function() {
129 if (typeof options.columns === "undefined") {
130 options.columns = [];
131 }
132 options.columns.push({data:jQuery(this).data('name')});
133 });
134 self.tables[id] = self.initTable(el, options);
135 } else if (typeof options !== 'undefined') {
136 var oldTable = self.tables[id];
137 var initOpts = oldTable.init();
138 var newOpts = jQuery.extend( initOpts, options);
139 oldTable.destroy();
140 self.tables[id] = self.initTable(el, newOpts);
141 }
142
143 return self.tables[id];
144 };
145
146 this.initTable = function (el, options) {
147 var table = el.DataTable(options);
148 var self = this;
149 if (el.data('on-draw')) {
150 table.on('draw.dt', function (e, settings) {
151 var namedCallback = el.data('on-draw');
152 if (typeof window[namedCallback] === 'function') {
153 window[namedCallback](e, settings);
154 }
155 });
156 } else if (el.data('on-draw-rebind-confirmation')) {
157 table.on('draw.dt', function (e) {
158 self.rebindConfirmation(e);
159 });
160 }
161
162 return table;
163 };
164
165 this.rebindConfirmation = function (e) {
166 var self = this;
167 var tableId = e.target.id;
168 var toggles = WHMCS.ui.confirmation.register();
169 for(var i = 0, len = toggles.length; i < len; i++ ) {
170 jQuery(toggles[i]).on(
171 'confirmed.bs.confirmation',
172 function (e)
173 {
174 e.preventDefault();
175 WHMCS.http.jqClient.post(
176 jQuery(e.target).data('target-url'),
177 {
178 'token': csrfToken
179 }
180 ).done(function (data)
181 {
182 if (data.status === 'success' || data.status === 'okay') {
183 self.getTableById(tableId, undefined).ajax.reload();
184 }
185 });
186
187 }
188 );
189 }
190 };
191
192 return this;
193},
194
195clipboard: function() {
196 this.copy = function(e) {
197 e.preventDefault();
198
199 var trigger = $(e.currentTarget);
200 var contentElement = $(trigger).data('clipboard-target');
201 var container = $(contentElement).parent();
202
203 try {
204 var tempElement = $('<textarea>')
205 .css('position', 'fixed')
206 .css('opacity', '0')
207 .css('width', '1px')
208 .css('height', '1px')
209 .val($(contentElement).val());
210
211 container.append(tempElement);
212 tempElement.focus().select();
213 document.execCommand('copy');
214 } finally {
215 tempElement.remove();
216 }
217
218 trigger.tooltip({
219 trigger: 'click',
220 placement: 'bottom'
221 });
222 WHMCS.ui.toolTip.setTip(trigger, 'Copied!');
223 WHMCS.ui.toolTip.hideTip(trigger);
224 };
225
226 return this;
227},
228
229/**
230 * ToolTip and Clipboard behaviors
231 */
232toolTip: function () {
233 this.setTip = function (btn, message) {
234 var tip = btn.data('bs.tooltip');
235 if (tip.hoverState !== 'in') {
236 tip.hoverState = 'in';
237 }
238 btn.attr('data-original-title', message);
239 tip.show();
240
241 return tip;
242 };
243
244 this.hideTip = function (btn, timeout) {
245 if (!timeout) {
246 timeout = 2000;
247 }
248 return setTimeout(function() {
249 btn.data('bs.tooltip').hide()
250 }, timeout);
251 }
252},
253
254jsonForm: function() {
255 this.managedElements = 'input,textarea,select';
256
257 this.initFields = function (form) {
258 var self = this;
259 $(form).find(self.managedElements).each(function () {
260 var field = this;
261
262 $(field).on('keypress change', function () {
263 if (self.fieldHasError(field)) {
264 self.clearFieldError(field);
265 }
266 });
267 });
268 };
269
270 this.init = function (form) {
271 var self = this;
272
273 self.initFields(form);
274
275 $(form).on('submit', function(e) {
276 e.preventDefault();
277 e.stopPropagation();
278
279 self.clearErrors(form);
280
281 var formModal = $(form).parents('.modal[role="dialog"]').first();
282
283 if ($(formModal).length) {
284 $(formModal).on('show.bs.modal hidden.bs.modal', function() {
285 self.clearErrors(form);
286 });
287
288 /*
289 * Make this optional if the form is used for editing
290 */
291 $(formModal).on('show.bs.modal', function() {
292 $(form)[0].reset();
293 });
294 }
295
296 WHMCS.http.client.post({
297 url: $(form).attr('action'),
298 data: $(form).serializeArray(),
299 })
300 .done(function (response) {
301 self.onSuccess(form, response);
302 })
303 .fail(function (jqXHR) {
304 self.onError(form, jqXHR);
305 })
306 .always(function (data) {
307 self.onRequestComplete(form, data);
308 });
309 });
310 };
311
312 this.initAll = function () {
313 var self = this;
314
315 $('form[data-role="json-form"]').each(function() {
316 var formElement = this;
317 self.init(formElement);
318 });
319 };
320
321 this.markFieldErrors = function (form, fields)
322 {
323 var self = this;
324 var errorMessage = null;
325 var field, fieldLookup;
326
327 for (var fieldName in fields) {
328 if (fields.hasOwnProperty(fieldName)) {
329 errorMessage = fields[fieldName];
330 }
331
332 fieldLookup = self.managedElements.split(',').map(function(element) {
333 return element + '[name="' + fieldName + '"]';
334 }).join(',');
335
336 field = $(form).find(fieldLookup);
337
338 if (errorMessage) {
339 $(field).parents('.form-group').addClass('has-error');
340 $(field).attr('title', errorMessage);
341 $(field).tooltip();
342 }
343 }
344
345 $(form).find('.form-group.has-error input[title]').first().tooltip('show');
346 };
347
348 this.fieldHasError = function (field) {
349 return $(field).parents('.form-group').hasClass('has-error');
350 };
351
352 this.clearFieldError = function (field) {
353 /**
354 * Try dispose first for BS 4, which will raise error
355 * on BS 3 or older, then we use destroy instead
356 */
357 try {
358 $(field).tooltip('dispose');
359 } catch (err) {
360 $(field).tooltip('destroy');
361 }
362 $(field).parents('.form-group').removeClass('has-error');
363 };
364
365 this.onSuccess = function (form, response) {
366 var formOnSuccess = $(form).data('on-success');
367
368 if (typeof formOnSuccess === 'function') {
369 formOnSuccess(response.data);
370 }
371 };
372
373 this.onError = function (form, jqXHR) {
374 if (jqXHR.responseJSON && jqXHR.responseJSON.fields && typeof jqXHR.responseJSON.fields === 'object') {
375 this.markFieldErrors(form, jqXHR.responseJSON.fields);
376 } else {
377 // TODO: replace with client-accessible generic error messaging
378 console.log('Unknown error - please try again later.');
379 }
380
381 var formOnError = $(form).data('on-error');
382
383 if (typeof formOnError === 'function') {
384 formOnError(jqXHR);
385 }
386 };
387
388 this.clearErrors = function (form) {
389 var self = this;
390
391 $(form).find(self.managedElements).each(function () {
392 self.clearFieldError(this);
393 })
394 };
395
396 this.onRequestComplete = function (form, data) {
397 // implement as needed
398 };
399
400 return this;
401},
402
403effects: function () {
404 this.errorShake = function (element) {
405 /**
406 * Shake effect without jQuery UI inspired by Hiren Patel | ninty9notout:
407 * @see https://github.com/ninty9notout/jquery-shake/blob/51f3dcf625970c78505bcac831fd9e28fc85d374/jquery.ui.shake.js
408 */
409 options = options || {};
410 var options = $.extend({
411 direction: "left",
412 distance: 8,
413 times: 3,
414 speed: 90
415 }, options);
416
417 return element.each(function () {
418 var el = $(this), props = {
419 position: el.css("position"),
420 top: el.css("top"),
421 bottom: el.css("bottom"),
422 left: el.css("left"),
423 right: el.css("right")
424 };
425
426 el.css("position", "relative");
427
428 var ref = (options.direction === "up" || options.direction === "down") ? "top" : "left";
429 var motion = (options.direction === "up" || options.direction === "left") ? "pos" : "neg";
430
431 var animation = {}, animation1 = {}, animation2 = {};
432 animation[ref] = (motion === "pos" ? "-=" : "+=") + options.distance;
433 animation1[ref] = (motion === "pos" ? "+=" : "-=") + options.distance * 2;
434 animation2[ref] = (motion === "pos" ? "-=" : "+=") + options.distance * 2;
435
436 el.animate(animation, options.speed);
437 for (var i = 1; i < options.times; i++) {
438 el.animate(animation1, options.speed).animate(animation2, options.speed);
439 }
440
441 el.animate(animation1, options.speed).animate(animation, options.speed / 2, function () {
442 el.css(props);
443 });
444 });
445 };
446
447}
448});
449