PluginProbe
SQL Chart Builder / trunk
SQL Chart Builder vtrunk
3.0.2 3.0.1 trunk 1.0.2 1.0.3 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.7.1 2.3.7.2 2.3.8 3.0.0
sql-chart-builder / asset / admin.js

admin.js in SQL Chart Builder trunk, at asset/admin.js

589 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* SQL Chart Builder 3.0 — admin chart builder UI */
2 jQuery(function ($) {
3 'use strict';
4
5 var $app = $('#gvnsql-app');
6 if (!$app.length) return;
7
8 var cfg = window.gvnSqlBuilder || { tables: [], prefix: 'wp_', ajaxurl: window.ajaxurl, nonce: '' };
9 var $sql = $('#guaven_sqlcharts_code');
10
11 /* ============ tabs ============ */
12 $('#gvnsql-tabs').on('click', '.gvnsql-tab', function () {
13 var tab = $(this).data('tab');
14 $('.gvnsql-tab').removeClass('active');
15 $(this).addClass('active');
16 $('.gvnsql-section').removeClass('active').filter('[data-tab="' + tab + '"]').addClass('active');
17 });
18
19 /* ============ chart type cards + guides ============ */
20 $app.on('change', '.gvnsql-typecard input[type=radio]', function () {
21 var type = $(this).val();
22 $('.gvnsql-typecard').removeClass('selected');
23 $(this).closest('.gvnsql-typecard').addClass('selected');
24 $('.gvnsql-guide').hide().filter('[data-guide="' + type + '"]').show();
25 });
26
27 $app.on('click', '.gvnsql-use-example', function () {
28 var $btn = $(this);
29 if ($.trim($sql.val()) !== '' && !window.confirm('Replace the current SQL query with the example?')) return;
30 $sql.val($btn.data('sql'));
31 $('#guaven_sqlcharts_xarg_s').val($btn.data('x'));
32 $('#guaven_sqlcharts_yarg_s').val($btn.data('y'));
33 if ($('#guaven_sqlcharts_xarg_l').val() === '') $('#guaven_sqlcharts_xarg_l').val($btn.data('x'));
34 if ($('#guaven_sqlcharts_yarg_l').val() === '') $('#guaven_sqlcharts_yarg_l').val($btn.data('yl'));
35 prefetchTablesInQuery();
36 refreshColMapSelects();
37 refreshFilterNameSelects();
38 });
39
40 /* ============ insert-at-caret helper ============ */
41 function insertAtCaret(text) {
42 var el = $sql.get(0);
43 el.focus();
44 var start = el.selectionStart, end = el.selectionEnd, val = el.value;
45 // add a space before if gluing to a word character
46 if (start > 0 && /[\w)]/.test(val.charAt(start - 1)) && !/^[\s,)]/.test(text)) text = ' ' + text;
47 el.value = val.substring(0, start) + text + val.substring(end);
48 el.selectionStart = el.selectionEnd = start + text.length;
49 $sql.trigger('input');
50 }
51
52 /* ============ toolbar ============ */
53 $('#gvnsql-toolbar').on('click', '.gvnsql-kw', function () {
54 insertAtCaret($(this).data('insert'));
55 });
56
57 var $tableSel = $('#gvnsql-table-select'),
58 $colSel = $('#gvnsql-column-select'),
59 $tagSel = $('#gvnsql-tag-select');
60
61 $.each(cfg.tables, function (i, t) {
62 $tableSel.append($('<option>').val(t).text(t));
63 });
64
65 $tableSel.on('change', function () {
66 var t = this.value;
67 if (!t) return;
68 insertAtCaret(t + ' ');
69 loadColumns(t);
70 this.selectedIndex = 0;
71 });
72
73 $colSel.on('change', function () {
74 if (!this.value) return;
75 insertAtCaret(this.value + ' ');
76 this.selectedIndex = 0;
77 });
78
79 var defaultTags = ['{current_user_id}', '{current_user_login}', '{current_user_email}', '{current_user_display_name}'];
80 function rebuildTagSelect() {
81 $tagSel.find('option:not(:first)').remove();
82 var tags = defaultTags.slice();
83 $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () {
84 var name = $.trim(this.value);
85 if (name) tags.push('{' + name + '}');
86 });
87 $.each(tags, function (i, t) { $tagSel.append($('<option>').val(t).text(t)); });
88 }
89 $tagSel.on('change', function () {
90 if (!this.value) return;
91 insertAtCaret(this.value + ' ');
92 this.selectedIndex = 0;
93 });
94
95 /* ============ schema: column fetching + datalist ============ */
96 var columnCache = {}; // table => [ {name,type}, ... ]
97
98 function loadColumns(table, done) {
99 if (columnCache[table]) {
100 refreshColumnUI();
101 if (done) done(columnCache[table]);
102 return;
103 }
104 $.post(cfg.ajaxurl, { action: 'gvnsql_get_columns', nonce: cfg.nonce, table: table }, function (res) {
105 if (res && res.success) {
106 columnCache[table] = res.data;
107 refreshColumnUI();
108 if (done) done(res.data);
109 }
110 });
111 }
112
113 function allKnownColumns() {
114 var names = {};
115 $.each(columnCache, function (table, cols) {
116 $.each(cols, function (i, c) { names[c.name] = true; });
117 });
118 return Object.keys(names);
119 }
120
121 function refreshColumnUI() {
122 var $dl = $('#gvnsql-columns').empty();
123 $colSel.find('option:not(:first)').remove();
124 $.each(columnCache, function (table, cols) {
125 var $group = $('<optgroup>').attr('label', table);
126 $.each(cols, function (i, c) {
127 $dl.append($('<option>').val(c.name));
128 $group.append($('<option>').val(c.name).text(c.name + ' (' + c.type + ')'));
129 });
130 $colSel.append($group);
131 });
132 refreshColMapSelects();
133 }
134
135 function prefetchTablesInQuery() {
136 var text = $sql.val();
137 if (!text) return;
138 var found = 0;
139 $.each(cfg.tables, function (i, t) {
140 if (found >= 6) return false;
141 if (text.indexOf(t) !== -1 && !columnCache[t]) { loadColumns(t); found++; }
142 });
143 }
144 prefetchTablesInQuery();
145 rebuildTagSelect();
146
147 /* ============ SQL query parsing (columns/tags used by the mapping & filter dropdowns) ============ */
148
149 function splitTopLevel(str, sep) {
150 var out = [], depth = 0, cur = '';
151 for (var i = 0; i < str.length; i++) {
152 var ch = str.charAt(i);
153 if (ch === '(') depth++;
154 else if (ch === ')') depth--;
155 if (ch === sep && depth === 0) { out.push(cur); cur = ''; }
156 else cur += ch;
157 }
158 out.push(cur);
159 return out;
160 }
161
162 function parseQueryInfo() {
163 var text = $sql.val() || '';
164 var selectCols = [], whereCols = [], tags = [];
165 $.each(text.split(';'), function (qi, q) {
166 var m = q.match(/select\s+([\s\S]*?)\s+from\b/i);
167 if (m) {
168 $.each(splitTopLevel(m[1], ','), function (i, expr) {
169 expr = $.trim(expr);
170 if (!expr) return;
171 var name = null;
172 var alias = expr.match(/\s(?:as\s+)?`?([a-z_][a-z0-9_]*)`?\s*$/i);
173 if (alias && !/^(asc|desc|distinct)$/i.test(alias[1])) name = alias[1];
174 else {
175 var plain = expr.match(/^`?([a-z_][a-z0-9_]*)`?$/i) || expr.match(/\.`?([a-z_][a-z0-9_]*)`?$/i);
176 if (plain) name = plain[1];
177 }
178 if (name && $.inArray(name, selectCols) === -1) selectCols.push(name);
179 });
180 }
181 var w = q.match(/\bwhere\b([\s\S]*?)(\bgroup\s+by\b|\border\s+by\b|\blimit\b|$)/i);
182 if (w) {
183 var re = /([a-z_][a-z0-9_.]*)\s*(?:=|!=|<>|<=|>=|<|>|\blike\b|\bin\b|\bbetween\b)/gi, mm;
184 while ((mm = re.exec(w[1]))) {
185 var col = mm[1].split('.').pop();
186 if ($.inArray(col, whereCols) === -1) whereCols.push(col);
187 }
188 }
189 var tre = /\{([a-z_][a-z0-9_]*)\}/gi, tm;
190 while ((tm = tre.exec(q))) {
191 if ($.inArray('{' + tm[1] + '}', defaultTags) === -1 && $.inArray(tm[1], tags) === -1) tags.push(tm[1]);
192 }
193 });
194 return { selectCols: selectCols, whereCols: whereCols, tags: tags };
195 }
196
197 /* ============ column mapping dropdowns (X / Y axis) ============ */
198
199 function refreshColMapSelects() {
200 var info = parseQueryInfo();
201 $('.gvnsql-colselect').each(function () {
202 var $sel = $(this);
203 var $input = $('#' + $sel.data('target'));
204 var cur = $.trim($input.val());
205 var opts = info.selectCols.slice();
206 $.each(info.whereCols.concat(allKnownColumns()), function (i, c) {
207 if ($.inArray(c, opts) === -1) opts.push(c);
208 });
209 $sel.empty().append($('<option>').val('').text('— select a column —'));
210 if (info.selectCols.length) {
211 var $g = $('<optgroup label="Columns in your SELECT">');
212 $.each(info.selectCols, function (i, c) { $g.append($('<option>').val(c).text(c)); });
213 $sel.append($g);
214 }
215 var rest = opts.filter(function (c) { return $.inArray(c, info.selectCols) === -1; });
216 if (rest.length) {
217 var $g2 = $('<optgroup label="Other known columns">');
218 $.each(rest, function (i, c) { $g2.append($('<option>').val(c).text(c)); });
219 $sel.append($g2);
220 }
221 $sel.append($('<option>').val('__custom__').text('Custom / type manually…'));
222 if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur);
223 else if (cur) $sel.val('__custom__');
224 else $sel.val('');
225 syncColCustom($sel, opts.length);
226 });
227 }
228
229 function syncColCustom($sel, optCount) {
230 var $input = $('#' + $sel.data('target'));
231 var custom = $sel.val() === '__custom__' || optCount === 0;
232 $input.toggle(custom);
233 $sel.toggle(optCount > 0);
234 }
235
236 $app.on('change', '.gvnsql-colselect', function () {
237 var $sel = $(this);
238 var $input = $('#' + $sel.data('target'));
239 if ($sel.val() === '__custom__') {
240 $input.show().trigger('focus');
241 } else {
242 if ($sel.val() !== '') $input.val($sel.val());
243 $input.hide();
244 }
245 });
246
247 /* ============ SQL autocomplete ============ */
248 var KEYWORDS = ['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'ORDER BY', 'LIMIT', 'INNER JOIN', 'LEFT JOIN',
249 'RIGHT JOIN', 'JOIN', 'ON', 'AS', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS NULL',
250 'IS NOT NULL', 'DISTINCT', 'COUNT(*)', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'HAVING', 'UNION',
251 'DESC', 'ASC', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'SUBSTRING', 'SUBSTR', 'CONCAT',
252 'DATE_FORMAT', 'NOW()', 'CURDATE()', 'YEAR', 'MONTH', 'DAY', 'INTERVAL', 'DATE_SUB', 'DATE_ADD'];
253
254 var $editorWrap = $sql.closest('.gvnsql-editorwrap');
255 var $ac = $('<div class="gvnsql-ac" style="display:none"></div>').appendTo($editorWrap);
256 var acItems = [], acIndex = -1, acWordStart = 0;
257
258 // mirror div used to find the pixel position of the caret inside the textarea
259 var $mirror = $('<div class="gvnsql-mirror" aria-hidden="true"></div>').appendTo($editorWrap);
260 function caretCoordinates() {
261 var el = $sql.get(0);
262 var style = window.getComputedStyle(el);
263 ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing',
264 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
265 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
266 'boxSizing'].forEach(function (p) { $mirror.css(p, style[p]); });
267 $mirror.css('width', $sql.outerWidth() + 'px');
268 var before = el.value.substring(0, el.selectionStart);
269 $mirror.text(before);
270 var $marker = $('<span>​</span>');
271 $mirror.append($marker);
272 var mpos = $marker.position();
273 var lineH = parseInt(style.lineHeight, 10);
274 if (isNaN(lineH)) lineH = Math.round(parseInt(style.fontSize, 10) * 1.4) || 18;
275 return {
276 left: mpos.left,
277 top: mpos.top + lineH - el.scrollTop
278 };
279 }
280
281 function currentWord() {
282 var el = $sql.get(0);
283 var pos = el.selectionStart, val = el.value, start = pos;
284 while (start > 0 && /[\w.{}]/.test(val.charAt(start - 1))) start--;
285 return { start: start, word: val.substring(start, pos) };
286 }
287
288 function buildCandidates(word) {
289 var lower = word.toLowerCase();
290 var out = [];
291
292 // table.column completion
293 if (word.indexOf('.') !== -1) {
294 var parts = word.split('.');
295 var table = parts[0], colPrefix = (parts[1] || '').toLowerCase();
296 if ($.inArray(table, cfg.tables) !== -1) {
297 if (!columnCache[table]) loadColumns(table);
298 $.each(columnCache[table] || [], function (i, c) {
299 if (!colPrefix || c.name.toLowerCase().indexOf(colPrefix) === 0)
300 out.push({ text: table + '.' + c.name, label: table + '.' + c.name, kind: 'column' });
301 });
302 }
303 return out.slice(0, 12);
304 }
305
306 $.each(KEYWORDS, function (i, k) {
307 if (!lower || k.toLowerCase().indexOf(lower) === 0)
308 out.push({ text: k + ' ', label: k, kind: 'keyword' });
309 });
310 $.each(cfg.tables, function (i, t) {
311 if (!lower || t.toLowerCase().indexOf(lower) === 0)
312 out.push({ text: t + ' ', label: t, kind: 'table', table: t });
313 });
314 $.each(allKnownColumns(), function (i, c) {
315 if (!lower || c.toLowerCase().indexOf(lower) === 0)
316 out.push({ text: c + ' ', label: c, kind: 'column' });
317 });
318 $.each(defaultTags, function (i, t) {
319 if (!lower || t.toLowerCase().indexOf('{' + lower) === 0 || t.toLowerCase().indexOf(lower) === 1)
320 out.push({ text: t + ' ', label: t, kind: 'tag' });
321 });
322 $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () {
323 var name = $.trim(this.value);
324 if (name && (!lower || name.toLowerCase().indexOf(lower) === 0 || ('{' + name).toLowerCase().indexOf(lower) === 0))
325 out.push({ text: '{' + name + '} ', label: '{' + name + '}', kind: 'tag' });
326 });
327 return out.slice(0, 12);
328 }
329
330 function showAC(force) {
331 var cw = currentWord();
332 if (!force && cw.word.length < 1) { hideAC(); return; }
333 acItems = buildCandidates(cw.word);
334 if (!acItems.length) { hideAC(); return; }
335 acWordStart = cw.start;
336 acIndex = 0;
337 var html = '';
338 $.each(acItems, function (i, item) {
339 html += '<div class="gvnsql-ac-item' + (i === 0 ? ' active' : '') + '" data-i="' + i + '">' +
340 '<span class="gvnsql-ac-kind gvnsql-ac-kind-' + item.kind + '">' + item.kind.charAt(0).toUpperCase() + '</span>' +
341 $('<span>').text(item.label).html() + '</div>';
342 });
343 $ac.html(html).show();
344 var pos = caretCoordinates();
345 var wrapH = $editorWrap.height();
346 var top = Math.min(pos.top + 4, wrapH - 10);
347 $ac.css({ left: Math.min(pos.left, $editorWrap.width() - $ac.outerWidth() - 8) + 'px', top: top + 'px' });
348 }
349
350 function hideAC() { $ac.hide(); acItems = []; acIndex = -1; }
351
352 function applyAC(i) {
353 if (!acItems[i]) return;
354 var el = $sql.get(0);
355 var item = acItems[i];
356 var val = el.value, pos = el.selectionStart;
357 el.value = val.substring(0, acWordStart) + item.text + val.substring(pos);
358 el.selectionStart = el.selectionEnd = acWordStart + item.text.length;
359 hideAC();
360 el.focus();
361 if (item.kind === 'table') loadColumns(item.table);
362 }
363
364 $sql.on('input', function () { showAC(false); });
365
366 // re-parse the query for the mapping/filter dropdowns while the user types
367 var gvnsqlParseTimer = null;
368 $sql.on('input', function () {
369 clearTimeout(gvnsqlParseTimer);
370 gvnsqlParseTimer = setTimeout(function () {
371 refreshColMapSelects();
372 refreshFilterNameSelects();
373 }, 600);
374 });
375 $sql.on('click', hideAC);
376 $sql.on('blur', function () { setTimeout(hideAC, 200); });
377
378 $sql.on('keydown', function (e) {
379 if (e.ctrlKey && e.code === 'Space') { e.preventDefault(); showAC(true); return; }
380 if (!acItems.length) return;
381 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
382 e.preventDefault();
383 acIndex = (acIndex + (e.key === 'ArrowDown' ? 1 : acItems.length - 1)) % acItems.length;
384 $ac.find('.gvnsql-ac-item').removeClass('active').eq(acIndex).addClass('active');
385 } else if (e.key === 'Enter' || e.key === 'Tab') {
386 e.preventDefault();
387 applyAC(acIndex);
388 } else if (e.key === 'Escape') {
389 hideAC();
390 }
391 });
392
393 $ac.on('mousedown', '.gvnsql-ac-item', function (e) {
394 e.preventDefault();
395 applyAC($(this).data('i'));
396 });
397
398 /* ============ dynamic filters builder ============ */
399 var $filterRows = $('#gvnsql-filters-rows'),
400 $filterRaw = $('#guaven_sqlcharts_variables');
401
402 function filterRowHtml(f) {
403 f = $.extend({ name: '', def: '', label: '', type: 'text' }, f);
404 var types = { text: 'Text', number: 'Number', date: 'Date (with datepicker)', '': 'No input (tag only)' };
405 var opts = '';
406 $.each(types, function (v, l) {
407 opts += '<option value="' + v + '"' + (v === f.type ? ' selected' : '') + '>' + l + '</option>';
408 });
409 return $('<div class="gvnsql-filter-row">' +
410 '<select class="gvnsql-filter-name-select" title="Tag name — suggested from your SQL query"></select>' +
411 '<input type="text" class="gvnsql-filter-name" placeholder="tag name (e.g. fromdate)">' +
412 '<select class="gvnsql-filter-type">' + opts + '</select>' +
413 '<input type="text" class="gvnsql-filter-label" placeholder="Label above chart">' +
414 '<input type="text" class="gvnsql-filter-default" placeholder="Default value">' +
415 '<button type="button" class="button gvnsql-filter-insert" title="Insert {tag} into the SQL query at the cursor">Insert into query</button>' +
416 '<button type="button" class="button-link gvnsql-filter-remove" title="Remove">&times;</button>' +
417 '</div>')
418 .find('.gvnsql-filter-name').val(f.name).end()
419 .find('.gvnsql-filter-label').val(f.label).end()
420 .find('.gvnsql-filter-default').val(f.def).end();
421 }
422
423 function parseFilters(raw) {
424 var out = [];
425 $.each($.trim(raw || '').split('|'), function (i, chunk) {
426 chunk = $.trim(chunk);
427 if (!chunk) return;
428 var p = chunk.split('~').map(function (s) { return $.trim(s); });
429 if (!p[0]) return;
430 out.push({ name: p[0], def: p[1] || '', label: p[2] || '', type: p[3] || '' });
431 });
432 return out;
433 }
434
435 function serializeFilters() {
436 var parts = [];
437 $filterRows.find('.gvnsql-filter-row').each(function () {
438 var $r = $(this);
439 var name = $.trim($r.find('.gvnsql-filter-name').val());
440 if (!name) return;
441 var seg = [name,
442 $.trim($r.find('.gvnsql-filter-default').val()),
443 $.trim($r.find('.gvnsql-filter-label').val())];
444 var type = $r.find('.gvnsql-filter-type').val();
445 if (type) seg.push(type);
446 parts.push(seg.join('~'));
447 });
448 $filterRaw.val(parts.join(' | '));
449 rebuildTagSelect();
450 }
451
452 /* tag-name dropdown per row: suggests {tags} already in the query and WHERE-clause columns */
453 function refreshFilterNameSelects() {
454 var info = parseQueryInfo();
455 $filterRows.find('.gvnsql-filter-row').each(function () {
456 var $row = $(this);
457 var $sel = $row.find('.gvnsql-filter-name-select');
458 var $input = $row.find('.gvnsql-filter-name');
459 var cur = $.trim($input.val());
460 var opts = [];
461 $.each(info.tags.concat(info.whereCols), function (i, t) {
462 if ($.inArray(t, opts) === -1) opts.push(t);
463 });
464 $sel.empty().append($('<option>').val('').text('— choose tag —'));
465 if (info.tags.length) {
466 var $g = $('<optgroup label="{tags} in your query">');
467 $.each(info.tags, function (i, t) { $g.append($('<option>').val(t).text('{' + t + '}')); });
468 $sel.append($g);
469 }
470 var rest = info.whereCols.filter(function (c) { return $.inArray(c, info.tags) === -1; });
471 if (rest.length) {
472 var $g2 = $('<optgroup label="Columns from WHERE part">');
473 $.each(rest, function (i, c) { $g2.append($('<option>').val(c).text(c)); });
474 $sel.append($g2);
475 }
476 $sel.append($('<option>').val('__custom__').text('Custom name…'));
477 if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur);
478 else if (cur) $sel.val('__custom__');
479 else $sel.val('');
480 var custom = $sel.val() === '__custom__' || opts.length === 0;
481 $input.toggle(custom);
482 $sel.toggle(opts.length > 0);
483 });
484 }
485
486 $filterRows.on('change', '.gvnsql-filter-name-select', function () {
487 var $row = $(this).closest('.gvnsql-filter-row');
488 var $input = $row.find('.gvnsql-filter-name');
489 if (this.value === '__custom__') {
490 $input.show().trigger('focus');
491 } else {
492 if (this.value !== '') $input.val(this.value);
493 $input.hide();
494 }
495 serializeFilters();
496 });
497
498 $.each(parseFilters($filterRaw.val()), function (i, f) {
499 $filterRows.append(filterRowHtml(f));
500 });
501 rebuildTagSelect();
502 refreshFilterNameSelects();
503
504 $('#gvnsql-add-filter').on('click', function () {
505 $filterRows.append(filterRowHtml({}));
506 refreshFilterNameSelects();
507 });
508
509 $filterRows.on('input change', 'input,select', serializeFilters);
510 $filterRows.on('click', '.gvnsql-filter-remove', function () {
511 $(this).closest('.gvnsql-filter-row').remove();
512 serializeFilters();
513 });
514 $filterRows.on('click', '.gvnsql-filter-insert', function () {
515 var name = $.trim($(this).closest('.gvnsql-filter-row').find('.gvnsql-filter-name').val());
516 if (!name) { window.alert('Give the filter a tag name first.'); return; }
517 insertAtCaret('{' + name + '} ');
518 $('.gvnsql-tab[data-tab="data"]').trigger('click');
519 });
520
521 $('#gvnsql-raw-toggle').on('click', function () {
522 $('.gvnsql-raw').toggle();
523 });
524 // raw textarea edited manually -> rebuild rows
525 $filterRaw.on('change', function () {
526 $filterRows.empty();
527 $.each(parseFilters($filterRaw.val()), function (i, f) {
528 $filterRows.append(filterRowHtml(f));
529 });
530 rebuildTagSelect();
531 });
532
533 /* ============ colors builder ============ */
534 var $colorsRows = $('#gvnsql-colors-rows'),
535 $colorsHidden = $('#guaven_sqlcharts_colors');
536
537 function colorRow(value) {
538 return $('<span class="gvnsql-color-item">' +
539 '<input type="color" value="' + value + '">' +
540 '<button type="button" class="button-link gvnsql-color-remove" title="Remove">&times;</button>' +
541 '</span>');
542 }
543
544 var colorsRawMode = false;
545
546 function serializeColors() {
547 if (colorsRawMode) return; // legacy non-hex value: leave it untouched
548 var vals = [];
549 $colorsRows.find('input[type=color]').each(function () { vals.push(this.value.toUpperCase()); });
550 $colorsHidden.val(vals.join(','));
551 }
552
553 var existingColors = $.trim($colorsHidden.val());
554 if (existingColors) {
555 var tokens = existingColors.split(',').map(function (c) { return $.trim(c); }).filter(function (c) { return c !== ''; });
556 colorsRawMode = tokens.some(function (c) { return !/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(c); });
557 if (colorsRawMode) {
558 // legacy format we can't map to color pickers (e.g. "255,0,0") — expose it as editable text
559 $colorsHidden.attr('type', 'text').addClass('gvnsql-colors-rawinput');
560 $('#gvnsql-add-color').hide();
561 } else {
562 $.each(tokens, function (i, c) {
563 if (/^#[0-9a-f]{3}$/i.test(c)) c = '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
564 $colorsRows.append(colorRow(c.toUpperCase()));
565 });
566 }
567 }
568
569 $('#gvnsql-add-color').on('click', function () {
570 // keep in sync with guaven_sqlcharts_default_palette() in functions.php
571 var palette = ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F', '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'];
572 $colorsRows.append(colorRow(palette[$colorsRows.children().length % palette.length]));
573 serializeColors();
574 });
575 $colorsRows.on('input change', 'input[type=color]', serializeColors);
576 $colorsRows.on('click', '.gvnsql-color-remove', function () {
577 $(this).closest('.gvnsql-color-item').remove();
578 serializeColors();
579 });
580
581 /* make sure everything is serialized before WP submits the post form */
582 $('#post').on('submit', function () {
583 serializeFilters();
584 serializeColors();
585 });
586
587 refreshColMapSelects();
588 });
589