/* SQL Chart Builder 3.0 — admin chart builder UI */ jQuery(function ($) { 'use strict'; var $app = $('#gvnsql-app'); if (!$app.length) return; var cfg = window.gvnSqlBuilder || { tables: [], prefix: 'wp_', ajaxurl: window.ajaxurl, nonce: '' }; var $sql = $('#guaven_sqlcharts_code'); /* ============ tabs ============ */ $('#gvnsql-tabs').on('click', '.gvnsql-tab', function () { var tab = $(this).data('tab'); $('.gvnsql-tab').removeClass('active'); $(this).addClass('active'); $('.gvnsql-section').removeClass('active').filter('[data-tab="' + tab + '"]').addClass('active'); }); /* ============ chart type cards + guides ============ */ $app.on('change', '.gvnsql-typecard input[type=radio]', function () { var type = $(this).val(); $('.gvnsql-typecard').removeClass('selected'); $(this).closest('.gvnsql-typecard').addClass('selected'); $('.gvnsql-guide').hide().filter('[data-guide="' + type + '"]').show(); }); $app.on('click', '.gvnsql-use-example', function () { var $btn = $(this); if ($.trim($sql.val()) !== '' && !window.confirm('Replace the current SQL query with the example?')) return; $sql.val($btn.data('sql')); $('#guaven_sqlcharts_xarg_s').val($btn.data('x')); $('#guaven_sqlcharts_yarg_s').val($btn.data('y')); if ($('#guaven_sqlcharts_xarg_l').val() === '') $('#guaven_sqlcharts_xarg_l').val($btn.data('x')); if ($('#guaven_sqlcharts_yarg_l').val() === '') $('#guaven_sqlcharts_yarg_l').val($btn.data('yl')); prefetchTablesInQuery(); refreshColMapSelects(); refreshFilterNameSelects(); }); /* ============ insert-at-caret helper ============ */ function insertAtCaret(text) { var el = $sql.get(0); el.focus(); var start = el.selectionStart, end = el.selectionEnd, val = el.value; // add a space before if gluing to a word character if (start > 0 && /[\w)]/.test(val.charAt(start - 1)) && !/^[\s,)]/.test(text)) text = ' ' + text; el.value = val.substring(0, start) + text + val.substring(end); el.selectionStart = el.selectionEnd = start + text.length; $sql.trigger('input'); } /* ============ toolbar ============ */ $('#gvnsql-toolbar').on('click', '.gvnsql-kw', function () { insertAtCaret($(this).data('insert')); }); var $tableSel = $('#gvnsql-table-select'), $colSel = $('#gvnsql-column-select'), $tagSel = $('#gvnsql-tag-select'); $.each(cfg.tables, function (i, t) { $tableSel.append($('').val(t).text(t)); }); $tableSel.on('change', function () { var t = this.value; if (!t) return; insertAtCaret(t + ' '); loadColumns(t); this.selectedIndex = 0; }); $colSel.on('change', function () { if (!this.value) return; insertAtCaret(this.value + ' '); this.selectedIndex = 0; }); var defaultTags = ['{current_user_id}', '{current_user_login}', '{current_user_email}', '{current_user_display_name}']; function rebuildTagSelect() { $tagSel.find('option:not(:first)').remove(); var tags = defaultTags.slice(); $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () { var name = $.trim(this.value); if (name) tags.push('{' + name + '}'); }); $.each(tags, function (i, t) { $tagSel.append($('').val(t).text(t)); }); } $tagSel.on('change', function () { if (!this.value) return; insertAtCaret(this.value + ' '); this.selectedIndex = 0; }); /* ============ schema: column fetching + datalist ============ */ var columnCache = {}; // table => [ {name,type}, ... ] function loadColumns(table, done) { if (columnCache[table]) { refreshColumnUI(); if (done) done(columnCache[table]); return; } $.post(cfg.ajaxurl, { action: 'gvnsql_get_columns', nonce: cfg.nonce, table: table }, function (res) { if (res && res.success) { columnCache[table] = res.data; refreshColumnUI(); if (done) done(res.data); } }); } function allKnownColumns() { var names = {}; $.each(columnCache, function (table, cols) { $.each(cols, function (i, c) { names[c.name] = true; }); }); return Object.keys(names); } function refreshColumnUI() { var $dl = $('#gvnsql-columns').empty(); $colSel.find('option:not(:first)').remove(); $.each(columnCache, function (table, cols) { var $group = $('').attr('label', table); $.each(cols, function (i, c) { $dl.append($('').val(c.name)); $group.append($('').val(c.name).text(c.name + ' (' + c.type + ')')); }); $colSel.append($group); }); refreshColMapSelects(); } function prefetchTablesInQuery() { var text = $sql.val(); if (!text) return; var found = 0; $.each(cfg.tables, function (i, t) { if (found >= 6) return false; if (text.indexOf(t) !== -1 && !columnCache[t]) { loadColumns(t); found++; } }); } prefetchTablesInQuery(); rebuildTagSelect(); /* ============ SQL query parsing (columns/tags used by the mapping & filter dropdowns) ============ */ function splitTopLevel(str, sep) { var out = [], depth = 0, cur = ''; for (var i = 0; i < str.length; i++) { var ch = str.charAt(i); if (ch === '(') depth++; else if (ch === ')') depth--; if (ch === sep && depth === 0) { out.push(cur); cur = ''; } else cur += ch; } out.push(cur); return out; } function parseQueryInfo() { var text = $sql.val() || ''; var selectCols = [], whereCols = [], tags = []; $.each(text.split(';'), function (qi, q) { var m = q.match(/select\s+([\s\S]*?)\s+from\b/i); if (m) { $.each(splitTopLevel(m[1], ','), function (i, expr) { expr = $.trim(expr); if (!expr) return; var name = null; var alias = expr.match(/\s(?:as\s+)?`?([a-z_][a-z0-9_]*)`?\s*$/i); if (alias && !/^(asc|desc|distinct)$/i.test(alias[1])) name = alias[1]; else { var plain = expr.match(/^`?([a-z_][a-z0-9_]*)`?$/i) || expr.match(/\.`?([a-z_][a-z0-9_]*)`?$/i); if (plain) name = plain[1]; } if (name && $.inArray(name, selectCols) === -1) selectCols.push(name); }); } var w = q.match(/\bwhere\b([\s\S]*?)(\bgroup\s+by\b|\border\s+by\b|\blimit\b|$)/i); if (w) { var re = /([a-z_][a-z0-9_.]*)\s*(?:=|!=|<>|<=|>=|<|>|\blike\b|\bin\b|\bbetween\b)/gi, mm; while ((mm = re.exec(w[1]))) { var col = mm[1].split('.').pop(); if ($.inArray(col, whereCols) === -1) whereCols.push(col); } } var tre = /\{([a-z_][a-z0-9_]*)\}/gi, tm; while ((tm = tre.exec(q))) { if ($.inArray('{' + tm[1] + '}', defaultTags) === -1 && $.inArray(tm[1], tags) === -1) tags.push(tm[1]); } }); return { selectCols: selectCols, whereCols: whereCols, tags: tags }; } /* ============ column mapping dropdowns (X / Y axis) ============ */ function refreshColMapSelects() { var info = parseQueryInfo(); $('.gvnsql-colselect').each(function () { var $sel = $(this); var $input = $('#' + $sel.data('target')); var cur = $.trim($input.val()); var opts = info.selectCols.slice(); $.each(info.whereCols.concat(allKnownColumns()), function (i, c) { if ($.inArray(c, opts) === -1) opts.push(c); }); $sel.empty().append($('').val('').text('— select a column —')); if (info.selectCols.length) { var $g = $(''); $.each(info.selectCols, function (i, c) { $g.append($('').val(c).text(c)); }); $sel.append($g); } var rest = opts.filter(function (c) { return $.inArray(c, info.selectCols) === -1; }); if (rest.length) { var $g2 = $(''); $.each(rest, function (i, c) { $g2.append($('').val(c).text(c)); }); $sel.append($g2); } $sel.append($('').val('__custom__').text('Custom / type manually…')); if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur); else if (cur) $sel.val('__custom__'); else $sel.val(''); syncColCustom($sel, opts.length); }); } function syncColCustom($sel, optCount) { var $input = $('#' + $sel.data('target')); var custom = $sel.val() === '__custom__' || optCount === 0; $input.toggle(custom); $sel.toggle(optCount > 0); } $app.on('change', '.gvnsql-colselect', function () { var $sel = $(this); var $input = $('#' + $sel.data('target')); if ($sel.val() === '__custom__') { $input.show().trigger('focus'); } else { if ($sel.val() !== '') $input.val($sel.val()); $input.hide(); } }); /* ============ SQL autocomplete ============ */ var KEYWORDS = ['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'ORDER BY', 'LIMIT', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'JOIN', 'ON', 'AS', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS NULL', 'IS NOT NULL', 'DISTINCT', 'COUNT(*)', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'HAVING', 'UNION', 'DESC', 'ASC', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'SUBSTRING', 'SUBSTR', 'CONCAT', 'DATE_FORMAT', 'NOW()', 'CURDATE()', 'YEAR', 'MONTH', 'DAY', 'INTERVAL', 'DATE_SUB', 'DATE_ADD']; var $editorWrap = $sql.closest('.gvnsql-editorwrap'); var $ac = $('').appendTo($editorWrap); var acItems = [], acIndex = -1, acWordStart = 0; // mirror div used to find the pixel position of the caret inside the textarea var $mirror = $('').appendTo($editorWrap); function caretCoordinates() { var el = $sql.get(0); var style = window.getComputedStyle(el); ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'boxSizing'].forEach(function (p) { $mirror.css(p, style[p]); }); $mirror.css('width', $sql.outerWidth() + 'px'); var before = el.value.substring(0, el.selectionStart); $mirror.text(before); var $marker = $(''); $mirror.append($marker); var mpos = $marker.position(); var lineH = parseInt(style.lineHeight, 10); if (isNaN(lineH)) lineH = Math.round(parseInt(style.fontSize, 10) * 1.4) || 18; return { left: mpos.left, top: mpos.top + lineH - el.scrollTop }; } function currentWord() { var el = $sql.get(0); var pos = el.selectionStart, val = el.value, start = pos; while (start > 0 && /[\w.{}]/.test(val.charAt(start - 1))) start--; return { start: start, word: val.substring(start, pos) }; } function buildCandidates(word) { var lower = word.toLowerCase(); var out = []; // table.column completion if (word.indexOf('.') !== -1) { var parts = word.split('.'); var table = parts[0], colPrefix = (parts[1] || '').toLowerCase(); if ($.inArray(table, cfg.tables) !== -1) { if (!columnCache[table]) loadColumns(table); $.each(columnCache[table] || [], function (i, c) { if (!colPrefix || c.name.toLowerCase().indexOf(colPrefix) === 0) out.push({ text: table + '.' + c.name, label: table + '.' + c.name, kind: 'column' }); }); } return out.slice(0, 12); } $.each(KEYWORDS, function (i, k) { if (!lower || k.toLowerCase().indexOf(lower) === 0) out.push({ text: k + ' ', label: k, kind: 'keyword' }); }); $.each(cfg.tables, function (i, t) { if (!lower || t.toLowerCase().indexOf(lower) === 0) out.push({ text: t + ' ', label: t, kind: 'table', table: t }); }); $.each(allKnownColumns(), function (i, c) { if (!lower || c.toLowerCase().indexOf(lower) === 0) out.push({ text: c + ' ', label: c, kind: 'column' }); }); $.each(defaultTags, function (i, t) { if (!lower || t.toLowerCase().indexOf('{' + lower) === 0 || t.toLowerCase().indexOf(lower) === 1) out.push({ text: t + ' ', label: t, kind: 'tag' }); }); $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () { var name = $.trim(this.value); if (name && (!lower || name.toLowerCase().indexOf(lower) === 0 || ('{' + name).toLowerCase().indexOf(lower) === 0)) out.push({ text: '{' + name + '} ', label: '{' + name + '}', kind: 'tag' }); }); return out.slice(0, 12); } function showAC(force) { var cw = currentWord(); if (!force && cw.word.length < 1) { hideAC(); return; } acItems = buildCandidates(cw.word); if (!acItems.length) { hideAC(); return; } acWordStart = cw.start; acIndex = 0; var html = ''; $.each(acItems, function (i, item) { html += '' + '' + item.kind.charAt(0).toUpperCase() + '' + $('').text(item.label).html() + ''; }); $ac.html(html).show(); var pos = caretCoordinates(); var wrapH = $editorWrap.height(); var top = Math.min(pos.top + 4, wrapH - 10); $ac.css({ left: Math.min(pos.left, $editorWrap.width() - $ac.outerWidth() - 8) + 'px', top: top + 'px' }); } function hideAC() { $ac.hide(); acItems = []; acIndex = -1; } function applyAC(i) { if (!acItems[i]) return; var el = $sql.get(0); var item = acItems[i]; var val = el.value, pos = el.selectionStart; el.value = val.substring(0, acWordStart) + item.text + val.substring(pos); el.selectionStart = el.selectionEnd = acWordStart + item.text.length; hideAC(); el.focus(); if (item.kind === 'table') loadColumns(item.table); } $sql.on('input', function () { showAC(false); }); // re-parse the query for the mapping/filter dropdowns while the user types var gvnsqlParseTimer = null; $sql.on('input', function () { clearTimeout(gvnsqlParseTimer); gvnsqlParseTimer = setTimeout(function () { refreshColMapSelects(); refreshFilterNameSelects(); }, 600); }); $sql.on('click', hideAC); $sql.on('blur', function () { setTimeout(hideAC, 200); }); $sql.on('keydown', function (e) { if (e.ctrlKey && e.code === 'Space') { e.preventDefault(); showAC(true); return; } if (!acItems.length) return; if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); acIndex = (acIndex + (e.key === 'ArrowDown' ? 1 : acItems.length - 1)) % acItems.length; $ac.find('.gvnsql-ac-item').removeClass('active').eq(acIndex).addClass('active'); } else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); applyAC(acIndex); } else if (e.key === 'Escape') { hideAC(); } }); $ac.on('mousedown', '.gvnsql-ac-item', function (e) { e.preventDefault(); applyAC($(this).data('i')); }); /* ============ dynamic filters builder ============ */ var $filterRows = $('#gvnsql-filters-rows'), $filterRaw = $('#guaven_sqlcharts_variables'); function filterRowHtml(f) { f = $.extend({ name: '', def: '', label: '', type: 'text' }, f); var types = { text: 'Text', number: 'Number', date: 'Date (with datepicker)', '': 'No input (tag only)' }; var opts = ''; $.each(types, function (v, l) { opts += '' + l + ''; }); return $('' + '' + '' + '' + opts + '' + '' + '' + 'Insert into query' + '×' + '') .find('.gvnsql-filter-name').val(f.name).end() .find('.gvnsql-filter-label').val(f.label).end() .find('.gvnsql-filter-default').val(f.def).end(); } function parseFilters(raw) { var out = []; $.each($.trim(raw || '').split('|'), function (i, chunk) { chunk = $.trim(chunk); if (!chunk) return; var p = chunk.split('~').map(function (s) { return $.trim(s); }); if (!p[0]) return; out.push({ name: p[0], def: p[1] || '', label: p[2] || '', type: p[3] || '' }); }); return out; } function serializeFilters() { var parts = []; $filterRows.find('.gvnsql-filter-row').each(function () { var $r = $(this); var name = $.trim($r.find('.gvnsql-filter-name').val()); if (!name) return; var seg = [name, $.trim($r.find('.gvnsql-filter-default').val()), $.trim($r.find('.gvnsql-filter-label').val())]; var type = $r.find('.gvnsql-filter-type').val(); if (type) seg.push(type); parts.push(seg.join('~')); }); $filterRaw.val(parts.join(' | ')); rebuildTagSelect(); } /* tag-name dropdown per row: suggests {tags} already in the query and WHERE-clause columns */ function refreshFilterNameSelects() { var info = parseQueryInfo(); $filterRows.find('.gvnsql-filter-row').each(function () { var $row = $(this); var $sel = $row.find('.gvnsql-filter-name-select'); var $input = $row.find('.gvnsql-filter-name'); var cur = $.trim($input.val()); var opts = []; $.each(info.tags.concat(info.whereCols), function (i, t) { if ($.inArray(t, opts) === -1) opts.push(t); }); $sel.empty().append($('').val('').text('— choose tag —')); if (info.tags.length) { var $g = $(''); $.each(info.tags, function (i, t) { $g.append($('').val(t).text('{' + t + '}')); }); $sel.append($g); } var rest = info.whereCols.filter(function (c) { return $.inArray(c, info.tags) === -1; }); if (rest.length) { var $g2 = $(''); $.each(rest, function (i, c) { $g2.append($('').val(c).text(c)); }); $sel.append($g2); } $sel.append($('').val('__custom__').text('Custom name…')); if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur); else if (cur) $sel.val('__custom__'); else $sel.val(''); var custom = $sel.val() === '__custom__' || opts.length === 0; $input.toggle(custom); $sel.toggle(opts.length > 0); }); } $filterRows.on('change', '.gvnsql-filter-name-select', function () { var $row = $(this).closest('.gvnsql-filter-row'); var $input = $row.find('.gvnsql-filter-name'); if (this.value === '__custom__') { $input.show().trigger('focus'); } else { if (this.value !== '') $input.val(this.value); $input.hide(); } serializeFilters(); }); $.each(parseFilters($filterRaw.val()), function (i, f) { $filterRows.append(filterRowHtml(f)); }); rebuildTagSelect(); refreshFilterNameSelects(); $('#gvnsql-add-filter').on('click', function () { $filterRows.append(filterRowHtml({})); refreshFilterNameSelects(); }); $filterRows.on('input change', 'input,select', serializeFilters); $filterRows.on('click', '.gvnsql-filter-remove', function () { $(this).closest('.gvnsql-filter-row').remove(); serializeFilters(); }); $filterRows.on('click', '.gvnsql-filter-insert', function () { var name = $.trim($(this).closest('.gvnsql-filter-row').find('.gvnsql-filter-name').val()); if (!name) { window.alert('Give the filter a tag name first.'); return; } insertAtCaret('{' + name + '} '); $('.gvnsql-tab[data-tab="data"]').trigger('click'); }); $('#gvnsql-raw-toggle').on('click', function () { $('.gvnsql-raw').toggle(); }); // raw textarea edited manually -> rebuild rows $filterRaw.on('change', function () { $filterRows.empty(); $.each(parseFilters($filterRaw.val()), function (i, f) { $filterRows.append(filterRowHtml(f)); }); rebuildTagSelect(); }); /* ============ colors builder ============ */ var $colorsRows = $('#gvnsql-colors-rows'), $colorsHidden = $('#guaven_sqlcharts_colors'); function colorRow(value) { return $('' + '' + '×' + ''); } var colorsRawMode = false; function serializeColors() { if (colorsRawMode) return; // legacy non-hex value: leave it untouched var vals = []; $colorsRows.find('input[type=color]').each(function () { vals.push(this.value.toUpperCase()); }); $colorsHidden.val(vals.join(',')); } var existingColors = $.trim($colorsHidden.val()); if (existingColors) { var tokens = existingColors.split(',').map(function (c) { return $.trim(c); }).filter(function (c) { return c !== ''; }); colorsRawMode = tokens.some(function (c) { return !/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(c); }); if (colorsRawMode) { // legacy format we can't map to color pickers (e.g. "255,0,0") — expose it as editable text $colorsHidden.attr('type', 'text').addClass('gvnsql-colors-rawinput'); $('#gvnsql-add-color').hide(); } else { $.each(tokens, function (i, c) { if (/^#[0-9a-f]{3}$/i.test(c)) c = '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3]; $colorsRows.append(colorRow(c.toUpperCase())); }); } } $('#gvnsql-add-color').on('click', function () { // keep in sync with guaven_sqlcharts_default_palette() in functions.php var palette = ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F', '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC']; $colorsRows.append(colorRow(palette[$colorsRows.children().length % palette.length])); serializeColors(); }); $colorsRows.on('input change', 'input[type=color]', serializeColors); $colorsRows.on('click', '.gvnsql-color-remove', function () { $(this).closest('.gvnsql-color-item').remove(); serializeColors(); }); /* make sure everything is serialized before WP submits the post form */ $('#post').on('submit', function () { serializeFilters(); serializeColors(); }); refreshColMapSelects(); });