declare let $: any; const DATA_FIELD_ARRAY_NAMES = [ '.mmui-data-fields', '.mmui-data-field-rows', '.mmui-data-field-columns', '.mmui-data-field-filters', '.mmui-data-field-values', ]; /** * This functions returns an empty state list item element given a css class name * The class name corresponds to the dimensions, dimension levels and pivot columns * @param name - class name * @returns a list item element */ const getEmptyState = function (name) { let msg; if (name == '.mmui-data-fields') { msg = 'n available data field'; } else if (name == '.mmui-data-field-rows') { msg = ' horizontal row'; } else if (name == '.mmui-data-field-columns') { msg = ' vertical column'; } else if (name == '.mmui-data-field-filters') { msg = ' data filter'; } else { msg = ' value'; } return `
  • Drag a field here to add a${msg}.
  • `; }; export class MmuiQueryFormComponent { parentId = null; outside = false; constructor(parentId) { this.parentId = parentId; const elemts = $( `#${parentId} .mmui-data-fields, #${parentId} .mmui-data-field-rows, #${parentId} .mmui-data-field-columns, #${parentId} .mmui-data-field-filters, #${parentId} .mmui-data-field-values` ); elemts.sortable({ connectWith: '.mmui-text-list', items: 'li[id!=empty]', }); elemts.on('sortstop', () => this.receive()); elemts.on('sortupdate', (evt, ui) => this.onUpdate(evt, ui)); elemts.on('sortstart', (evt, ui) => { this.setBorder(evt, ui); this.setSelectedItemStyles(evt, ui); }); elemts.on('sortstop', () => { this.resetBorder(); this.resetSelectedItemStyles(); }); elemts.on('sortbeforestop', (evt, ui) => this.removeDraggedOutItem(evt, ui) ); elemts.on('sortout', () => this.sortOut()); elemts.on('sortover', () => this.sortOver()); this.sortDataFields(); const removeLinks = document.querySelectorAll( `#${parentId} > * ul > li > a.mmui-data-field-reset` ); removeLinks.forEach((item) => { item.addEventListener('click', (evt) => { evt.preventDefault(); const dataValue = item.parentElement.getAttribute('data-value'); const dataLabel = item.parentElement.getAttribute('data-label'); const dataType = item.parentElement.getAttribute('data-type'); this.onRemove(evt, dataValue, dataLabel, dataType); this.disableDataFields(); }); }); this.checkEmptyStates(); $(`#${this.parentId} .search-data-field`).keyup(() => { this.search(); }); const badges = document.querySelectorAll( `#${this.parentId} #data-field-filter-all, #${this.parentId} #data-field-filter-metric, #${this.parentId} #data-field-filter-attribute` ); badges.forEach((badge) => { badge.addEventListener('click', (evt) => { this.changeSearchMode(evt); this.search(); }); }); this.checkForTwoOrMoreRows(); this.formatDataFieldList(); this.checkEmptyStates(); this.disableDataFields(); } validateQuery() { const errorObj = { rows: [], columns: [], values: [], filters: [], }; const numberOfRows = $( `#${this.parentId} > * ul[class~="mmui-data-field-rows"] li[class~="mmui-data-field"]` ).length; const numberOfValues = $( `#${this.parentId} > * ul[class~="mmui-data-field-values"] li[class~="mmui-data-field"]` ).length; if (numberOfRows == 0) { errorObj.rows.push('The query must include at least one row.'); } if (numberOfValues == 0) { errorObj.values.push('The query must include at least one value.'); } return errorObj; } reset() { const parentId = this.parentId; for (let i = 1; i < DATA_FIELD_ARRAY_NAMES.length; i++) { const className = DATA_FIELD_ARRAY_NAMES[i]; const selector = `#${parentId} > * ${className}`; const emptyState = getEmptyState(className); $(selector).html(emptyState); $(selector).parent().addClass('mmui-data-field-drop-empty'); } this.checkForTwoOrMoreRows(); this.sortDataFields(); this.formatDataFieldList(); this.disableDataFields(); } search() { let displayedFieldCount = 0; const dataFieldsFilterId = $(`#${this.parentId} .badge-primary`).attr('id'); const searchText = $(`#${this.parentId} .mmui-data-field-search`) .val() .toLowerCase(); if (dataFieldsFilterId == 'data-field-filter-all') { displayedFieldCount = 0; $(`#${this.parentId} .mmui-data-fields > li`).each((index, dataField) => { dataField = $(dataField); const dataFieldText = dataField .find(`span.mmui-data-field-label`) .text() .toLowerCase(); if (!dataFieldText.includes(searchText)) { dataField.css('display', 'none'); } if (dataFieldText.includes(searchText)) { dataField.css('display', 'list-item'); displayedFieldCount++; } }); } else { displayedFieldCount = 0; $(`#${this.parentId} .mmui-data-fields > li`).each((index, dataField) => { dataField = $(dataField); const dataFieldText = dataField .find('span.mmui-data-field-label') .text() .toLowerCase(); if ( dataFieldsFilterId.includes(dataField.attr('data-type')) && dataFieldText.includes(searchText) ) { dataField.css('display', 'list-item'); displayedFieldCount++; } else { dataField.css('display', 'none'); } }); } if (displayedFieldCount == 0) { $(`#${this.parentId} #empty-data-field-msg`).remove(); let filterTypeDisplay; switch (dataFieldsFilterId) { case 'data-field-filter-all': filterTypeDisplay = 'data fields'; break; case 'data-field-filter-metric': filterTypeDisplay = 'values'; break; case 'data-field-filter-attribute': filterTypeDisplay = 'attributes'; break; } $(`#${this.parentId} .mmui-data-fields`).append( `

    No ${filterTypeDisplay} matching "${searchText}"

    ` ); } else { $(`#${this.parentId} #empty-data-field-msg`).remove(); } } sortOut() { this.outside = true; } sortOver() { this.outside = false; } removeDraggedOutItem(evt, ui) { const emptyDataFieldItem = $( `#${this.parentId} > * .mmui-data-fields li[id="empty"]` ); if (emptyDataFieldItem) { emptyDataFieldItem.remove(); } const item = $(ui.item); if (this.outside && !evt.target.className.includes('mmui-data-fields')) { const dataValue = item.attr('data-value'); const dataLabel = item.attr('data-label'); const dataType = item.attr('data-type'); const dataFields = $(`#${this.parentId} ul[class~="mmui-data-fields"]`); const existInDataFields = dataFields.find(`li[data-value="${dataValue}"]`).length > 0; if (!existInDataFields) { const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } item.remove(); } } changeSearchMode(evt) { const searchBadges = [ `#${this.parentId} #data-field-filter-all`, `#${this.parentId} #data-field-filter-metric`, `#${this.parentId} #data-field-filter-attribute`, ]; const target = $(evt.target); const id = target.attr('id'); for (let i = 0; i < searchBadges.length; i++) { const badge = $(searchBadges[i]); if (badge.attr('id') != id) { badge.removeClass('badge-primary'); badge.addClass('mmui-badge-muted'); } else { badge.removeClass('mmui-badge-muted'); badge.addClass('badge-primary'); } } const emptyDataFieldItem = $( `#${this.parentId} > * .mmui-data-fields li[id="empty"]` ); const dataFields = $( `#${this.parentId} > * .mmui-data-fields li[class=mmui-data-field]` ); if (dataFields.length < 1) { emptyDataFieldItem.css('display', 'block'); } else { emptyDataFieldItem.css('display', 'none'); for (let i = 0; i < dataFields.length; i++) { $(dataFields[i]).css('display', 'list-item'); } } this.formatDataFieldList(); } formatDataFieldList() { const dataFields = $(`#${this.parentId} > * .badge-primary`).attr('id'); $(`#${this.parentId} .mmui-data-fields > li`).each((index, dataField) => { dataField = $(dataField); const dataFieldText = dataField .find('span.mmui-data-field-label') .text() .toLowerCase(); const searchText = $(`#${this.parentId} .mmui-data-field-search`) .val() .toLowerCase(); if ( (dataFields.includes(dataField.attr('data-type')) || dataFields == 'data-field-filter-all') && dataFieldText.includes(searchText) ) { dataField.css('display', 'list-item'); } else { dataField.css('display', 'none'); } }); } setBorder(evt, ui) { const dataType = $(ui.item).attr('data-type'); const dataValue = $(ui.item).attr('data-value'); if (dataType === 'attribute') { const rows = this.selectElmt('rows'); const columns = this.selectElmt('columns'); const filters = this.selectElmt('filters'); if (rows.find(`li[data-value="${dataValue}"]`).length < 1) { rows.parent().addClass('mmui-highlighted-dimension-drop'); } if (columns.find(`li[data-value="${dataValue}"]`).length < 1) { columns.parent().addClass('mmui-highlighted-dimension-drop'); } if (filters.find(`li[data-value="${dataValue}"]`).length < 1) { filters.parent().addClass('mmui-highlighted-dimension-drop'); } } if (dataType === 'metric') { const values = this.selectElmt('values'); if (values.find(`li[data-value="${dataValue}"]`).length < 1) { values.parent().addClass('mmui-highlighted-dimension-drop'); } } } setSelectedItemStyles(evt, ui) { const dataValue = $(ui.item).attr('data-value'); const fields = $(`#${this.parentId} > * li[data-value="${dataValue}"]`); for (let i = 0; i < fields.length; i++) { const field = fields[i]; field.classList.remove('mmui-deselected-item'); field.classList.add('mmui-selected-item'); } } resetBorder() { const rows = this.selectElmt('rows'); const columns = this.selectElmt('columns'); const filters = this.selectElmt('filters'); const values = this.selectElmt('values'); rows .parent() .removeClass('mmui-highlighted-dimension-drop') .addClass('mmui-dimension-drop'); columns .parent() .removeClass('mmui-highlighted-dimension-drop') .addClass('mmui-dimension-drop'); filters .parent() .removeClass('mmui-highlighted-dimension-drop') .addClass('mmui-dimension-drop'); values .parent() .removeClass('mmui-highlighted-dimension-drop') .addClass('mmui-dimension-drop'); } resetSelectedItemStyles() { const fields = $(`#${this.parentId} > * li[class~="mmui-selected-item"]`); for (let i = 0; i < fields.length; i++) { const field = fields[i]; field.classList.remove('mmui-selected-item'); field.classList.add('mmui-deselected-item'); } } receive() { this.removeDuplicatesInDataFields(); this.checkForTwoOrMoreRows(); this.sortDataFields(); this.formatDataFieldList(); this.checkEmptyStates(); this.disableDataFields(); } /** * Generic sorting function to alphabetize a list of objects in the format [{label: 'Label One', value: 'Value One'}] * @returns am alphabetized list of objects */ sort(a, b) { return b.label.toUpperCase() < a.label.toUpperCase() ? 1 : -1; } /** * This function sorts the available data field list */ sortDataFields() { const dimensionArray = $(`#${this.parentId} > * .mmui-data-fields > li`) .toArray() .map((item) => { const dict: any = {}; dict.label = item.dataset.label; dict.value = item.dataset.value; dict.type = item.dataset.type; return dict; }) .sort(this.sort); let dimStr = ''; for (const item of dimensionArray) { dimStr += this.createDataFieldItem(item.label, item.value, item.type); } if (dimensionArray.length == 0 || dimensionArray[0].value != undefined) { $(`#${this.parentId} .mmui-data-fields`).html(dimStr); } this.formatDataFieldList(); } /** * This function updates the DOM by adding a list item to the appropriate list * It removes existing event handlers and adds a new one and checks for empty states * @param evt - click event * @param ui - ui item */ onUpdate(evt, ui) { if (!evt.target || !ui.sender) { this.sortDataFields(); return; } const className = $(evt.target).attr('class'); const target = $(evt.target); const dataValue = $(ui.item).attr('data-value'); const dataLabel = $(ui.item).attr('data-label'); const dataType = $(ui.item).attr('data-type'); const currentNode = target.find('li[data-value=' + dataValue + ']'); target.find('li[id=empty]').remove(); let elmt; currentNode.find('a').off('click'); const isTargetFields = className.includes('mmui-data-fields'), isTargetRows = className.includes('mmui-data-field-rows'), isTargetColumns = className.includes('mmui-data-field-columns'), isTargetFilters = className.includes('mmui-data-field-filters'), isTargetValues = className.includes('mmui-data-field-values'); if (isTargetFields) { elmt = this.createDataFieldItem(dataLabel, dataValue, dataType); this.sortDataFields(); } else if (isTargetRows) { elmt = this.createDataFieldInputItem( dataLabel, dataValue, dataType, 'rows' ); } else if (isTargetColumns) { elmt = this.createDataFieldInputItem( dataLabel, dataValue, dataType, 'columns' ); } else if (isTargetFilters) { elmt = this.createDataFieldInputItem( dataLabel, dataValue, dataType, 'filters' ); } else if (isTargetValues) { elmt = this.createDataFieldInputItem( dataLabel, dataValue, dataType, 'values' ); } currentNode.replaceWith(elmt); $(`#${this.parentId} > * ul > li > a[data-value=${dataValue}]`).click( (evt) => { evt.preventDefault(); this.onRemove(evt, dataValue, dataLabel, dataType); } ); this.checkIfTwoExistAlready(dataLabel, dataValue, dataType); this.checkIfItemExistsInList(evt, dataLabel, dataValue, dataType); this.checkIfValidMove(evt, dataLabel, dataValue, dataType); this.removeDuplicatesInDataFields(); if (isTargetFields || isTargetRows) { this.checkForTwoOrMoreRows(); } this.formatDataFieldList(); this.checkEmptyStates(); this.disableDataFields(); } disableDataFields() { const dataFields = $( `#${this.parentId} > * ul[class^="mmui-data-fields"] > li[class~="mmui-data-field"]` ); for (let i = 0; i < dataFields.length; i++) { const field = $(dataFields[i]); const dataValue = field.attr('data-value'); const dataType = field.attr('data-type'); const numberOfDataFields = $( `#${this.parentId} li[data-value~=${dataValue}]` ).length; if ( numberOfDataFields > 2 || (numberOfDataFields > 1 && dataType == 'metric') ) { field.addClass('mmui-data-field-disabled'); } } } checkForTwoOrMoreRows() { if ($(`#${this.parentId} .mmui-data-field-rows li`).length > 1) { $(`#${this.parentId} .mmui-data-field-subtotals`).css('display', 'block'); } else { $(`#${this.parentId} .mmui-data-field-subtotals`).css('display', 'none'); } } checkIfTwoExistAlready(dataLabel, dataValue, dataType) { const item = $(`#${this.parentId} li[data-value~=${dataValue}]`); if (item.length > 2 || dataType == 'metric') { this.checkEmptyStates(); } else { const dataField = this.createDataFieldItem( dataLabel, dataValue, dataType ); $(`#${this.parentId} ul[class~="mmui-data-fields"]`).append(dataField); } } checkIfItemExistsInList(evt, dataLabel, dataValue, dataType) { const newElmt = this.createDataFieldItem(dataLabel, dataValue, dataType); const currentElmts = $(evt.target).find(`li[data-value=${dataValue}]`); if (currentElmts.length > 1) { currentElmts.first().remove(); $(`#${this.parentId} ul[class~="mmui-data-fields"]`).append(newElmt); } } checkIfValidMove(evt, dataLabel, dataValue, dataType) { let dataFields = this.selectElmt('data-fields'), rows = this.selectElmt('rows'), columns = this.selectElmt('columns'), filters = this.selectElmt('filters'), values = this.selectElmt('values'), existsInDataFields = false, existsInRows = false, existsInColumns = false, existsInFilters = false, existsInValues = false, rowsListItem = rows.find(`li[data-value=${dataValue}]`), columnsListItem = columns.find(`li[data-value=${dataValue}]`), filtersListItem = filters.find(`li[data-value=${dataValue}]`), valuesListItem = values.find(`li[data-value=${dataValue}]`), dataFieldsListItem = dataFields.find(`li[data-value=${dataValue}]`); if (rowsListItem.length > 0) { existsInRows = true; } if (columnsListItem.length > 0) { existsInColumns = true; } if (filtersListItem.length > 0) { existsInFilters = true; } if (valuesListItem.length > 0) { existsInValues = true; } if (dataFieldsListItem.length > 0) { existsInDataFields = true; } if (dataType == 'attribute') { if (existsInRows && existsInColumns) { const currentElmts = $(evt.target).find(`li[data-value=${dataValue}]`); currentElmts.first().remove(); if (!existsInDataFields) { const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } } if (existsInValues) { const currentElmts = valuesListItem; currentElmts.first().remove(); if (!existsInDataFields) { const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } } } else if (dataType == 'metric') { if (existsInValues) { const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } if (existsInRows) { rowsListItem.first().remove(); const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } if (existsInColumns) { columnsListItem.first().remove(); const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } if (existsInFilters) { filtersListItem.first().remove(); const newElmt = this.createDataFieldItem( dataLabel, dataValue, dataType ); dataFields.append(newElmt); } } } removeDuplicatesInDataFields() { const seen = {}; $(`#${this.parentId} > * ul[class~="mmui-data-fields"] li`).each( function () { const txt = $(this).text(); if (seen[txt]) $(this).remove(); else seen[txt] = true; } ); } /** * This function creates a dimension list item for the available dimension list * @param label - the data-label attribute * @param value - the data-value attribute * @returns a list item element */ createDataFieldItem(label, value, dataType) { let metricIcon = ``; if (dataType == 'metric') { metricIcon = ``; } return `
  • ${label} ${metricIcon}
  • `; } /** * This function creates a list item for the dimensions, dimension levels, and pivot columns containing an input * @param label - the data-label attribute * @param value - the data-value attribute * @param name - the name for the input item * @returns a list item element */ createDataFieldInputItem(label, value, dataType, name = 'rows') { const id = `id_${name}_${value}`; return `
  • ${label}
  • `; } /** * This function handles the removing of a list item from dimensions, dimension levels, and pivot columns * @param evt - the data-label attribute * @param dataValue - the data-value attribute * @param dataLabel - the data-label attribute */ onRemove(evt, dataValue, dataLabel, dataType) { const emptyDataFieldItem = $( `#${this.parentId} > * .mmui-data-fields li[id="empty"]` ); if (emptyDataFieldItem) { emptyDataFieldItem.remove(); } $(evt.currentTarget).parent().remove(); const elmt = this.createDataFieldItem(dataLabel, dataValue, dataType); $(`#${this.parentId} > * .mmui-data-fields`).append(elmt); this.removeDuplicatesInDataFields(); this.checkForTwoOrMoreRows(); this.sortDataFields(); this.formatDataFieldList(); this.checkEmptyStates(); this.disableDataFields(); } /** * This function checks for empty states in the lists and will add the empty state list item if it finds one */ checkEmptyStates() { const parentId = this.parentId; for (let i = 0; i < DATA_FIELD_ARRAY_NAMES.length; i++) { const className = DATA_FIELD_ARRAY_NAMES[i]; const selector = `#${parentId} > * ${className}`; const array = $(selector + ' > li > span') .toArray() .map((item) => $(item).html()); if ( array.length < 1 || (array.length > 0 && array[0].includes('Drag items here')) ) { const emptyState = getEmptyState(className); $(selector).append(emptyState); $(selector).parent().addClass('mmui-data-field-drop-empty'); } else { $(selector).parent().removeClass('mmui-data-field-drop-empty'); } } } selectElmt(key) { let elmt; switch (key) { case 'data-fields': elmt = $(`#${this.parentId} ul[class~="mmui-data-fields"]`); break; case 'rows': elmt = $(`#${this.parentId} ul[class~="mmui-data-field-rows"]`); break; case 'columns': elmt = $(`#${this.parentId} ul[class~="mmui-data-field-columns"]`); break; case 'filters': elmt = $(`#${this.parentId} ul[class~="mmui-data-field-filters"]`); break; case 'values': elmt = $(`#${this.parentId} ul[class~="mmui-data-field-values"]`); break; } return elmt; } } export function showOrHideFacts(parentId) { const valueListItems = $(`#${parentId} > * .mmui-data-field-values > li`) .toArray() .filter((item) => item.dataset.value !== undefined) .map((item) => { return 'facts-' + item.dataset.value; }); let factItem, factItems = [], factItemMap = {}, queryFactsDiv = document.querySelector(`#${parentId} #query-facts`); for (let i = 0; i < queryFactsDiv.childNodes.length; i++) { factItem = queryFactsDiv.childNodes[i]; if (factItem instanceof HTMLElement) { factItem.style.display = 'none'; factItemMap[factItem.id] = factItem; factItems.push(factItem); } } queryFactsDiv.innerHTML = ''; let valueId; for (let i = 0; i < valueListItems.length; i++) { valueId = valueListItems[i]; factItem = factItemMap[valueId]; factItem.style.display = 'inline-block'; queryFactsDiv.appendChild(factItem); } for (let i = 0; i < factItems.length; i++) { factItem = factItems[i]; if (valueListItems.indexOf(factItem.id) == -1) { factItem.style.display = 'none'; queryFactsDiv.appendChild(factItem); } } }