class nuSelectObject { constructor() { this.boxID = ''; this.table = ''; this.joins = []; this.boxes = []; this.tempTables = []; this.tempJoins = []; } addBox(tableName, id) { this.table = tableName; const tableSchema = parent.nuFORM.tableSchema; const fieldNames = tableSchema[tableName].names; const fieldTypes = tableSchema[tableName].types; const boxIdSuffix = arguments.length === 1 ? nuID() : String(id).substring(3); this.boxID = 'box' + boxIdSuffix; this.scrollID = 'scroll' + boxIdSuffix; const boxWidth = this.boxWidth(tableSchema, tableName); const boxElement = document.createElement('div'); this.boxes.push(this.boxID); boxElement.setAttribute('id', this.boxID); $('body').append(boxElement); $('#' + this.boxID).css({ 'width': boxWidth, 'height': Math.min(20 + (fieldNames.length * 20), 190), 'top': 25 + (25 * $('.nuBox').length), 'left': 22 + (22 * $('.nuBox').length), 'position': 'absolute', 'border': 'solid grey 1px', 'overflow': 'hidden', 'padding-top': '5px', 'background-color': '#c5c3c3', 'z-index': 5, }) .addClass('nuBox nuDragNoSelect nuBoxHeader nuBoxShadow'); const scrollElement = document.createElement('div'); scrollElement.setAttribute('id', this.scrollID); $('#' + this.boxID).append(scrollElement); $('#' + scrollElement.id).css({ 'width': boxWidth, 'height': Math.min(20 + (fieldNames.length * 20), 175), 'top': 22, 'left': 0, 'overflow': 'scroll', 'overflow-x': 'hidden', 'line-height': 1, }) .addClass('nuDragNoSelect nuBoxHeader'); const tableNameElement = document.createElement('div'); tableNameElement.setAttribute('id', 'tablename' + this.boxID); $('#' + this.boxID).append(tableNameElement); $('#' + tableNameElement.id) .css({ 'position': 'absolute', 'width': 280, 'height': 15, 'top': 2, 'left': 0, 'padding-left': 22, 'text-align': 'left', 'border': 'none', 'font-weight': 'bold', 'background-color': '#c5c3c3', }) .html(tableName) .addClass('nuDragNoSelect nuTableName nuBoxTitle'); const checkAllBox = document.createElement('input'); checkAllBox.setAttribute('id', 'checkall' + this.boxID); $('#' + this.boxID).append(checkAllBox); $('#' + checkAllBox.id) .css({ 'position': 'absolute', 'width': 20, 'top': 2, 'left': -1, }) .attr('type', 'checkbox') .attr('onchange', 'window.nuSQL.buildSQL("table","' + this.boxID + '")') .prop('checked', true); const aliasInput = document.createElement('input'); aliasInput.setAttribute('id', 'alias' + this.boxID); aliasInput.setAttribute('title', 'Alias'); $('#' + this.boxID).append(aliasInput); $('#' + aliasInput.id) .css({ 'position': 'absolute', 'width': 30, 'top': 2, 'right': 18, 'background-color': '#c5c3c3', }) .change(function () { nuSQL.buildSQL(); }); fieldNames.forEach((fieldName, index) => { this.boxRow(index, fieldName, fieldTypes[index], boxWidth); }); const closeBoxElement = document.createElement('div'); closeBoxElement.setAttribute('id', 'nuBoxClose' + this.boxID); $('#' + this.boxID).append(closeBoxElement); $('#' + closeBoxElement.id).css({ 'width': 16, 'height': 15, 'top': 3, 'right': 1, 'position': 'absolute', 'color': 'black', 'text-align': 'center', }) .html('') .addClass('nuDragNoSelect nuButtonHover nuClose'); } buildSQL(context, boxId) { if (parent.$('#sse_edit').val() == 1) { return; } nuAngle(); const selectClause = this.buildSelect(context, boxId); const fromClause = this.buildFrom(); const clauses = this.buildClauses(); parent.$('#sse_sql') .val(`${selectClause}${fromClause}${clauses}\n`) .trigger("change"); parent.$('#sse_json') .val(this.buildJSON()) .trigger("change"); if (parent.$('#nuSaveToTextareaButton').length == 1) { parent.$('#nuSaveToTextareaButton').hide(); parent.$('#nuSaveButton').show(); } } buildSelect(checkType, boxID) { if (checkType === 'field') { $('#checkall' + boxID).prop('checked', false); } if (checkType === 'table') { $('.checkfield.' + boxID).prop('checked', false); } let selectClauses = []; for (let i = 0; i < this.boxes.length; i++) { let currentBoxID = this.boxes[i]; if ($('#' + currentBoxID).length === 1) { let tableName = $('#tablename' + currentBoxID).html(); let aliasName = $('#alias' + currentBoxID).val(); let fullTableName = this.justAlias(tableName, aliasName); if ($('#checkall' + currentBoxID).is(':checked')) { selectClauses.push(fullTableName + '.*'); } else { $('.checkfield.' + currentBoxID).each(function () { let fieldID = 'field' + this.id.substr(6); if ($(this).is(':checked')) { let fieldBoxID = String(this.id).split('_')[2]; let fieldAlias = $('#alias' + fieldBoxID).val(); let fieldName = $('#' + fieldID).html(); if (!fieldAlias) { selectClauses.push(fullTableName + '.' + fieldName); } else { selectClauses.push(fullTableName + '.' + fieldName + ' AS ' + fullTableName + '_' + fieldName); } } }); } } } let sqlSelect = "SELECT\n " + selectClauses.join(',\n ') + "\n"; return sqlSelect; } buildFrom() { this.tempTables = this.usedTables(); this.tempJoins = this.getJoinObjects(); // current visible joins const tableOrder = (table1, table2) => table2.joins.length - table1.joins.length; for (let tableIndex = 0; tableIndex < this.tempTables.length; tableIndex++) { if (this.tempTables[tableIndex].used !== -1) { const sortedTables = this.tempTables.sort(tableOrder); let hasMoreJoins = true; let initialAlias = this.tempTables[tableIndex].alias; let lastAliasUsed = this.fromAlias(sortedTables[0].table, sortedTables[0].alias); let definedAliases = [lastAliasUsed, initialAlias]; let joinDetails; while (hasMoreJoins) { [hasMoreJoins, joinDetails] = this.getJoinObject(definedAliases); if (hasMoreJoins) { const joinType = joinDetails.type === 'LEFT' ? "\n LEFT JOIN " : "\n JOIN "; let currentAlias = this.justAlias(joinDetails.tables[0], joinDetails.aliases[0]); let tableAlias; if (definedAliases.indexOf(lastAliasUsed) === -1 || definedAliases.indexOf(currentAlias) === -1) { tableAlias = this.buildAlias(joinDetails.tables[0], joinDetails.aliases[0]); lastAliasUsed = this.justAlias(joinDetails.tables[0], joinDetails.aliases[0]); } else { tableAlias = this.buildAlias(joinDetails.tables[1], joinDetails.aliases[1]); lastAliasUsed = this.justAlias(joinDetails.tables[1], joinDetails.aliases[1]); } definedAliases.push(lastAliasUsed); definedAliases.push(currentAlias); this.markTableAsUsed(joinDetails.tables[0], joinDetails.aliases[0]); this.markTableAsUsed(joinDetails.tables[1], joinDetails.aliases[1]); const joinConditions = joinDetails.joins.join(' AND '); this.tempTables[tableIndex].joins.push(joinType + tableAlias + ' ON ' + joinConditions); } } } } const finalSortedTables = this.tempTables.sort(tableOrder); const finalJoins = []; for (let i = 0; i < finalSortedTables.length; i++) { if (finalSortedTables[i].joins.length > 0 || finalSortedTables[i].used !== -1) { const aliasDefinition = this.fromAlias(finalSortedTables[i].table, finalSortedTables[i].alias); const joins = finalSortedTables[i].joins.join(""); finalJoins.push("\n " + aliasDefinition + joins); } } return "\nFROM" + finalJoins.join(''); } markTableAsUsed(table, alias) { for (let i = 0; i < this.tempTables.length; i++) { if (this.tempTables[i].table === table || this.tempTables[i].alias === alias) { this.tempTables[i].used = -1; return; } } } usedTables() { const tablesWithUsage = []; const context = this; this.tempJoins = this.getJoinObjects(); // current visible joins $('.nuBox').each(function () { const boxId = $(this)[0].id; const tableName = $('#tablename' + boxId).html(); const aliasName = $('#alias' + boxId).val(); let usageCount = 0; for (const joinKey in context.joins) { const join = context.joins[joinKey]; if (join.fromalias === aliasName || join.fromtable === tableName || join.toalias === aliasName || join.totable === tableName) { usageCount++; } } tablesWithUsage.push({ 'table': tableName, 'alias': context.justAlias(tableName, aliasName), 'used': usageCount, 'joins': [] }); }); const compareUsage = (firstTable, secondTable) => firstTable.used < secondTable.used; tablesWithUsage.sort(compareUsage); return tablesWithUsage; } getJoinObject(aliasList) { const tempJoins = this.tempJoins; const aliases = []; for (let i = 0; i < aliasList.length; i++) { const splitAlias = aliasList[i].split(' '); aliases.push(splitAlias[0], splitAlias[splitAlias.length - 1]); } for (let i = 0; i < tempJoins.length; i++) { const joinObject = tempJoins[i]; const firstAlias = this.justAlias(joinObject.tables[0], joinObject.aliases[0]); const secondAlias = this.justAlias(joinObject.tables[1], joinObject.aliases[1]); if (aliases.indexOf(firstAlias) !== -1 || aliases.indexOf(secondAlias) !== -1) { this.tempJoins.splice(i, 1); return [true, joinObject]; } } return [false, {}]; } getJoinObjects() { const joinData = this.joins; const joinMap = {}; const joinList = []; for (const key in joinData) { const join = joinData[key]; const fromTableAlias = this.justAlias(join.fromtable, join.fromalias); const toTableAlias = this.justAlias(join.totable, join.toalias); const joinCondition = `${fromTableAlias}.${join.fromfield} = ${toTableAlias}.${join.tofield}`; const joinId = [fromTableAlias, toTableAlias].sort().join('--'); if (joinMap[joinId] === undefined) { joinMap[joinId] = { 'tables': [join.fromtable, join.totable], 'aliases': [join.fromalias, join.toalias], 'type': join.join, 'joins': [joinCondition], 'used': false }; } else { joinMap[joinId].joins.push(joinCondition); if (join.type === 'LEFT') { join.type = 'LEFT'; } } } for (const key in joinMap) { joinList.push(joinMap[key]); } return joinList; } fromAlias(t, a) { return a === t ? t : `${t} AS ${a}`; } buildAlias(t, a) { return a === '' ? t : `${t} AS ${a}`; } justAlias(t, a) { return a === '' ? t : a; } refreshJoins(relationships) { this.joins = []; for (let relationshipKey in relationships) { let [fromField, toField] = relationshipKey.split('--'); let fromTable = fromField.split('_')[2]; let toTable = toField.split('_')[2]; let relationshipObject = { 'from': fromField, 'fromtable': $('#tablename' + fromTable).html(), 'fromalias': $('#alias' + fromTable).val(), 'fromfield': $('#' + fromField).html(), 'to': toField, 'totable': $('#tablename' + toTable).html(), 'toalias': $('#alias' + toTable).val(), 'tofield': $('#' + toField).html(), 'join': relationships[relationshipKey], }; this.joins[fromField + '--' + toField] = relationshipObject; } } buildClauses() { const orderFunction = (b, a) => (b[1] + 10000 + Number(b[4])) - (a[1] + 10000 + Number(a[4])); const selectClauses = parent.nuFORM.subform('zzzzsys_select_clause_sf').rows; selectClauses.sort(orderFunction); let clauses = ''; let whereClauses = []; let orderByClauses = []; let groupByClauses = []; let havingClauses = []; for (let i = 0; i < selectClauses.length; i++) { let [_, type, field, condition, sortOrder, , display] = selectClauses[i]; let isClauseValid = field != '' && condition != ''; let isGroupOrOrderValid = field != '' && sortOrder != ''; if (display == 0) { if (type == 1 && isClauseValid) { whereClauses.push('(' + field + ' ' + condition + ')'); } if (type == 4 && isClauseValid) { havingClauses.push('(' + field + condition + ')'); } if (type == 2 && isGroupOrOrderValid) { groupByClauses.push(field + ' ' + sortOrder); } if (type == 3 && isGroupOrOrderValid) { orderByClauses.push(field + ' ' + sortOrder); } } } if (whereClauses.length > 0) { clauses += "\n\nWHERE\n (" + whereClauses.join(" AND \n ") + ")\n"; } if (groupByClauses.length > 0) { clauses += "\nGROUP BY\n " + groupByClauses.join(",\n ") + "\n"; } if (havingClauses.length > 0) { clauses += "\nHAVING\n " + havingClauses.join(" AND \n ") + "\n"; } if (orderByClauses.length > 0) { clauses += "\nORDER BY\n " + orderByClauses.join(",\n ") + "\n"; } return clauses; } boxWidth(tableSchema, t) { let wordWidth = nuGetWordWidth(t) + 130; for (let i = 0; i < tableSchema[t].names.length; i++) { wordWidth = Math.max(wordWidth, nuGetWordWidth(tableSchema[t].names[i])); } return wordWidth; } boxRow(i, v, t, w) { this.boxColumn('select', i, 0, 18, v, ''); this.boxColumn('field', i, 22, 300, v, t, w); } boxColumn(c, t, l, w, v, title) { var suf = '_' + t + '_' + this.boxID; const col = document.createElement(c == 'select' ? 'input' : 'span'); col.setAttribute('id', c + suf); $('#' + this.scrollID).append(col); $('#' + col.id) .css({ 'position': 'absolute', 'width': w, 'top': t * 18, 'left': l, }) .attr('title', title); if (c == 'select') { //-- checkbox $('#' + col.id) .attr('data-nu-field', 'field' + suf) .attr('onchange', 'window.nuSQL.buildSQL("field","' + this.boxID + '")') .attr('type', 'checkbox') .addClass(this.boxID) .addClass('checkfield'); } else { $('#' + col.id) .addClass('nuBoxTitle') .addClass('nuBoxField') .addClass(this.boxID) .css('width', Number(w)) .css('padding-top', 2) .hover( function (event) { if (event.buttons == 1 && window.nuCurrentID != '') { $(this).css('color', 'green'); $(this).css('cursor', 'e-resize'); } else { $(this).css('color', 'red'); $(this).css('cursor', 'e-resize'); } }, function () { $(this).css('color', ''); $(this).css('cursor', 'default'); }) .html(v); } } buildJSON() { const jsonResult = {}; const tables = []; const self = this; $('.nuBox').each(function () { const id = this.id; const tableObject = { id: id, position: $(this).position(), tablename: $(`#tablename${id}`).html(), alias: $(`#alias${id}`).val(), checkall: $(`#checkall${id}`).is(':checked'), checkboxes: self.getCheckboxes(id) }; tables.push(tableObject); }); jsonResult.tables = tables; const joins = {}; const relations = this.joins; for (const key in relations) { const { from, to, join } = relations[key]; joins[`${from}--${to}`] = join; } jsonResult.joins = joins; return JSON.stringify(jsonResult); } getCheckboxes(b) { let checkBoxes = []; $(':checkbox.' + b).each(function () { checkBoxes.push($(this).is(':checked')); }); return checkBoxes; } rebuildGraphic() { const jsonString = $('#sse_json', parent.document).val(); if (!jsonString) { return true; } const jsonData = JSON.parse(jsonString); for (const table of jsonData.tables) { if (!parent.nuFORM.tableSchema[table.tablename]) { nuMessage([`No table named ${table.tablename}.`]); return false; } } for (const table of jsonData.tables) { this.addBox(table.tablename, table.id); $(`#${table.id}`) .css('top', table.position.top) .css('left', table.position.left); $(`#tablename${table.id}`).html(table.tablename); $(`#alias${table.id}`).val(table.alias); $(`#checkall${table.id}`).prop('checked', table.checkall); table.checkboxes.forEach((checkbox, index) => { $(`#select_${index}_${table.id}`).prop('checked', checkbox); }); } const joins = jsonData.joins; for (const key in joins) { const [startIndex, endIndex] = key.split('--'); this.joins[`${startIndex}--${endIndex}`] = joins[key]; } nuAngle(); return true; } addJoin(key, v) { const jsonString = parent.$('#sse_json').val(); let Joins = { 'joins': [] }; if (jsonString !== '') { Joins = JSON.parse(jsonString); } Joins.joins[key] = v; const sseJson = JSON.stringify(Joins); parent.$('#sse_json').val(sseJson); } } //=========functions========================================================================== function nuUp(event) { const element = $(event.target); if (element.hasClass('nuTableName')) { const parentStyle = element.parent().css(['top', 'left']); window.nuY = parseInt(parentStyle.top, 10); window.nuX = parseInt(parentStyle.left, 10); } if (element.hasClass('nuBoxField')) { const currentId = String(window.nuCurrentID); if (currentId.split('_').length === 3) { const fieldId = currentId; const targetId = event.target.id; if (fieldId.split('_')[2] !== targetId.split('_')[2]) { nuSQL.addJoin(`${fieldId}--${targetId}`, ''); nuAngle(); } } } window.nuCurrentID = ''; nuSQL.buildSQL(); } function nuDown(event) { const element = $(event.target); if (element.hasClass('nuRelationships')) { nuChangeJoin(event); return; } window.nuCurrentID = event.target.id; if (element.hasClass('nuTableName')) { const parentStyle = element.parent().css(['top', 'left']); window.nuY = event.clientY - parseInt(parentStyle.top, 10); window.nuX = event.clientX - parseInt(parentStyle.left, 10); } } function nuMove(event) { if (window.nuCurrentID === '') { return; } const movingElement = $('#' + window.nuCurrentID); if (movingElement.hasClass('nuTableName')) { if (event.buttons === 1) { const newYPosition = event.clientY - window.nuY; const newXPosition = event.clientX - window.nuX; if (newYPosition > 0) { movingElement.parent().css('top', newYPosition); } if (newXPosition > 0) { movingElement.parent().css('left', newXPosition); } nuAngle(); } } } function nuAngle() { $('.nuRelationships').remove(); const jsonData = parent.$('#sse_json').val(); if (jsonData == '') { return; } const parsedData = JSON.parse(jsonData); const joins = parsedData.joins; const validJoins = []; //-- remove links to closed boxes for (const key in joins) { let fromId = key.split('--')[0]; let toId = key.split('--')[1]; if ($('#' + fromId).length === 1 && $('#' + toId).length === 1) { validJoins[key] = joins[key]; } } nuSQL.refreshJoins(validJoins); for (let key in nuSQL.joins) { let fromElement = $('#' + nuSQL.joins[key].from); let toElement = $('#' + nuSQL.joins[key].to); let fromOffset = fromElement.offset(); let toOffset = toElement.offset(); let angle = Math.atan2(toOffset.top - fromOffset.top, toOffset.left - fromOffset.left) * 180 / Math.PI; //-- angle in degrees let distance = Math.sqrt(Math.pow(fromOffset.top - toOffset.top, 2) + Math.pow(fromOffset.left - toOffset.left, 2)); let joinId = 'joins' + nuID(); let joinType = nuSQL.joins[key].join; let leftMargin = 7; //-- relationship box (line) let $lineElement = document.createElement('div'); $lineElement.setAttribute('id', joinId); $('body').append($lineElement); $('#' + $lineElement.id).css({ 'width': joinType == 'LEFT' ? distance - leftMargin : distance, 'height': 6, 'left': fromOffset.left, 'top': fromOffset.top, 'position': 'absolute', 'text-align': 'center', 'border': 'rgba(255, 153, 0, .5) 0px solid', 'border-left-width': joinType == 'LEFT' ? leftMargin : 0, 'border-left-color': 'purple', 'background-color': 'rgba(255, 153, 0, .5)', 'transform': 'rotate(' + angle + 'deg)', 'z-index': 10 }) .attr('data-nu-join', key) .attr('title', joinType + ' JOIN ON ' + nuSQL.joins[key].fromfield + ' = ' + nuSQL.joins[key].tofield + ' (Click to Change Join)') .addClass('nuRelationships') .hover(function () { $(this).css({ 'border-top-width': '2px', 'border-bottom-width': '2px' }); }, function () { $(this).css({ 'border-top-width': '0', 'border-bottom-width': '0' }); }); $lineElement = $('#' + $lineElement.id); let top = parseInt(fromOffset.top + fromOffset.top - $lineElement.top, 10); let left = parseInt(fromOffset.left + fromOffset.left - $lineElement.left, 10); $('#' + joinId).css({ 'top': `${top}px`, 'left': `${left}px` }); let lTop = parseInt($lineElement.css('top'), 10); let lLeft = parseInt($lineElement.css('left'), 10); if (fromElement.offset().top < toElement.offset().top) { $lineElement.css('top', 7 + lTop + fromElement.offset().top - $lineElement.offset().top); } else { $lineElement.css('top', 7 + lTop + $lineElement.offset().top - fromElement.offset().top); } if (fromElement.offset().left < toElement.offset().left) { $lineElement.css('left', -20 + lLeft - ($lineElement.offset().left - fromElement.offset().left)); } else { $lineElement.css('left', -20 + lLeft - ($lineElement.offset().left - toElement.offset().left)); } } } function nuChangeJoin(e) { const jsonInputElement = parent.$('#sse_json'); let jsonString = jsonInputElement.val(); const parsedJson = JSON.parse(jsonString); const joinIndex = $(e.target).attr('data-nu-join'); parsedJson.joins[joinIndex] = parsedJson.joins[joinIndex] === '' ? 'LEFT' : ''; jsonInputElement .val(JSON.stringify(parsedJson)) .trigger("change"); nuSQL.buildSQL(); }