| 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">×</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">×</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 |
|