@syncfusion/ej2-spreadsheet
Version:
Feature-rich JavaScript Spreadsheet (Excel) control with built-in support for selection, editing, formatting, importing and exporting to Excel
605 lines (604 loc) • 27.8 kB
JavaScript
import { initiateCustomSort, locale, dialog, getFilterRange, refreshFilterRange } from '../index';
import { applySort, completeAction, focus, getUpdateUsingRaf, isDiscontinuousRange } from '../index';
import { sortComplete, beforeSort, getFormattedCellObject, sortImport, workbookFormulaOperation, isImported } from '../../workbook/index';
import { getIndexesFromAddress, getSwapRange, getCell, inRange, getSheet, getSheetIndex } from '../../workbook/index';
import { getColumnHeaderText, getRangeAddress, initiateSort, beginAction } from '../../workbook/index';
import { applyCF, getRangeIndexes } from '../../workbook/common/index';
import { getUniqueID, getComponent, enableRipple, initializeCSPTemplate } from '@syncfusion/ej2-base';
import { DropDownList } from '@syncfusion/ej2-dropdowns';
import { RadioButton, CheckBox } from '@syncfusion/ej2-buttons';
import { ListView } from '@syncfusion/ej2-lists';
/**
* `Sort` module is used to handle the sort action in Spreadsheet.
*/
var Sort = /** @class */ (function () {
/**
* Constructor for sort module.
*
* @param {Spreadsheet} parent - Specifies the Spreadsheet instance.
*/
function Sort(parent) {
this.parent = parent;
this.addEventListener();
}
/**
* To destroy the sort module.
*
* @returns {void}
*/
Sort.prototype.destroy = function () {
this.removeEventListener();
this.parent = null;
};
Sort.prototype.addEventListener = function () {
this.parent.on(applySort, this.applySortHandler, this);
this.parent.on(sortComplete, this.sortCompleteHandler, this);
this.parent.on(initiateCustomSort, this.initiateCustomSortHandler, this);
this.parent.on(sortImport, this.sortImport, this);
};
Sort.prototype.removeEventListener = function () {
if (!this.parent.isDestroyed) {
this.parent.off(applySort, this.applySortHandler);
this.parent.off(sortComplete, this.sortCompleteHandler);
this.parent.off(initiateCustomSort, this.initiateCustomSortHandler);
this.parent.off(sortImport, this.sortImport);
}
};
/**
* Gets the module name.
*
* @returns {string} - Gets the module name.
*/
Sort.prototype.getModuleName = function () {
return 'sort';
};
/**
* Validates the range and returns false when invalid.
*
* @returns {boolean} - Validates the range and returns false when invalid.
*/
Sort.prototype.isValidSortRange = function () {
var sheet = this.parent.getActiveSheet();
var range = getSwapRange(getIndexesFromAddress(sheet.selectedRange));
if (range[0] > sheet.usedRange.rowIndex || range[1] > sheet.usedRange.colIndex) {
return false;
}
return true;
};
/**
* sort while importing.
*
* @param {any} args - Specifies the args
* @param {number} args.sheetIdx - Specifies the sheet index
* @returns {void}
*/
Sort.prototype.sortImport = function (args) {
var sort = this.parent.sortCollection;
var cell;
var rowId = this.parent.getActiveSheet().usedRange.rowIndex - 1;
var sheetIdx = args ? args.sheetIdx : this.parent.activeSheetIndex;
for (var j = 0; j < sort.length; j++) {
if (sort[j].sheetIndex === sheetIdx) {
for (var i = 0; i < rowId; i++) {
cell = this.parent.getCell(i, sort[j].columnIndex);
if (cell && cell.querySelector('.e-filter-icon')) {
if (sort[j].order === 'Ascending' || sort[j].order === 'OnTop') {
if (!cell.querySelector('.e-filter-icon').classList.contains('e-sortasc-filter')) {
cell.querySelector('.e-filter-icon').classList.add('e-sortasc-filter');
}
}
else {
if (!cell.querySelector('.e-filter-icon').classList.contains('e-sortdesc-filter')) {
cell.querySelector('.e-filter-icon').classList.add('e-sortdesc-filter');
}
}
return;
}
}
}
}
};
/**
* Shows the range error alert dialog.
*
* @param {object} args - specify the args
* @param {string} args.error - range error string.
* @returns {void}
*/
Sort.prototype.sortRangeAlertHandler = function (args) {
var _this = this;
var dialogInst = this.parent.serviceLocator.getService(dialog);
var l10n = this.parent.serviceLocator.getService(locale);
dialogInst.show({
height: 180, width: 400, isModal: true, showCloseIcon: true,
content: args.error, enableRtl: this.parent.enableRtl,
beforeOpen: function (openArgs) {
var dlgArgs = {
dialogName: args.error === l10n.getConstant('MultiRangeSortError') ? 'MultiRangeSortDialog' : 'SortRangeDialog',
content: args.error,
element: openArgs.element, target: openArgs.target, cancel: openArgs.cancel
};
_this.parent.trigger('dialogBeforeOpen', dlgArgs);
if (dlgArgs.cancel) {
openArgs.cancel = true;
}
else {
dialogInst.dialogInstance.content = dlgArgs.content;
focus(_this.parent.element);
}
}
});
this.parent.hideSpinner();
};
/**
* Initiates the custom sort dialog.
*
* @returns {void}
*/
Sort.prototype.initiateCustomSortHandler = function () {
var _this = this;
var l10n = this.parent.serviceLocator.getService(locale);
var sheet = this.parent.getActiveSheet();
if (!this.isValidSortRange() || sheet.rows.length === 0) {
this.sortRangeAlertHandler({ error: l10n.getConstant('SortOutOfRangeError') });
return;
}
if (isDiscontinuousRange(sheet.selectedRange)) {
this.sortRangeAlertHandler({ error: l10n.getConstant('MultiRangeSortError') });
return;
}
var dialogInst = this.parent.serviceLocator.getService(dialog);
var sortOptions;
dialogInst.show({
height: 400, width: 560, isModal: true, showCloseIcon: true, cssClass: 'e-customsort-dlg',
header: l10n.getConstant('CustomSort'), enableRtl: this.parent.enableRtl,
beforeOpen: function (args) {
var dlgArgs = {
dialogName: 'CustomSortDialog',
element: args.element, target: args.target, cancel: args.cancel
};
_this.parent.trigger('dialogBeforeOpen', dlgArgs);
if (dlgArgs.cancel) {
args.cancel = true;
}
else {
dialogInst.dialogInstance.content = _this.customSortContent();
dialogInst.dialogInstance.dataBind();
focus(_this.parent.element);
}
},
buttons: [{
buttonModel: { content: l10n.getConstant('Ok'), isPrimary: true },
click: function () {
var element = dialogInst.dialogInstance.content;
var listview = getComponent(element.getElementsByClassName('e-list-sort e-listview e-lib')[0], 'listview');
var data = listview.dataSource;
_this.clearError();
var errorElem = element.getElementsByClassName('e-sort-error')[0];
errorElem.style.display = 'block';
if (!_this.validateError(data, element, errorElem)) {
sortOptions = { sortDescriptors: data, containsHeader: element.getElementsByClassName('e-sort-checkheader')[0].checked,
caseSensitive: element.getElementsByClassName('e-sort-checkcase')[0].checked };
dialogInst.hide();
}
}
}],
close: function () {
if (sortOptions) {
getUpdateUsingRaf(function () { return _this.applySortHandler({ sortOptions: sortOptions }); });
}
}
});
};
/**
* Validates the errors of the sort criteria and displays the error.
*
* @param {Object} json - listview datasource.
* @param {HTMLElement} dialogElem - dialog content element.
* @param {HTMLElement} errorElem - element to display error.
* @returns {boolean} - Return boolean value.
*/
Sort.prototype.validateError = function (json, dialogElem, errorElem) {
var l10n = this.parent.serviceLocator.getService(locale);
var hasEmpty = json.some(function (element) { return element.field.toString() === ''; });
if (hasEmpty) {
Array.prototype.some.call(dialogElem.getElementsByClassName('e-sort-field'), function (dropDown) {
var hasError = !getComponent(dropDown, 'dropdownlist').value;
if (hasError) {
dropDown.parentElement.classList.add('e-error');
}
return hasError; //breaks the loop if only one error added.
});
errorElem.innerText = l10n.getConstant('SortEmptyFieldError');
return true;
}
var temp = new Set();
var duplicateField = '';
var hasDuplicate = json.some(function (element) {
duplicateField = element.field.toString();
return temp.size === temp.add(element.field).size;
});
var errorField = '';
if (hasDuplicate) {
var count_1 = 0;
Array.prototype.some.call(dialogElem.getElementsByClassName('e-sort-field'), function (dropDown) {
var dropDownList = getComponent(dropDown, 'dropdownlist');
if (dropDownList.value === duplicateField) {
dropDown.parentElement.classList.add('e-error');
errorField = dropDownList.text;
count_1++;
}
return count_1 === 2; //breaks the loop when 2 errors added.
});
errorElem.innerText = '';
var errorFieldEle = this.parent.createElement('strong');
errorFieldEle.innerText = errorField;
errorElem.appendChild(errorFieldEle);
errorElem.appendChild(document.createTextNode(l10n.getConstant('SortDuplicateFieldError')));
return true;
}
return false;
};
/**
* Creates all the elements and generates the dialog content element.
*
* @returns {HTMLElement} - Returns the dialog element.
*/
Sort.prototype.customSortContent = function () {
var dialogElem = this.parent.createElement('div', { className: 'e-sort-dialog' });
var fields = this.getFields();
var listId = getUniqueID('customSort');
var listviewObj = this.getCustomListview(listId);
this.setHeaderTab(dialogElem, listviewObj, fields);
var contentElem = this.parent.createElement('div', {
className: 'e-sort-listsection',
styles: ''
});
dialogElem.appendChild(contentElem);
var listview = this.parent.createElement('div', { className: 'e-list-sort', styles: '' });
contentElem.appendChild(listview);
listviewObj.createElement = this.parent.createElement;
listviewObj.appendTo(listview);
listview.removeAttribute('tabindex');
this.renderListItem(listId, listviewObj, true, fields);
var errorElem = this.parent.createElement('div', { className: 'e-sort-error' });
dialogElem.appendChild(errorElem);
return dialogElem;
};
/**
* Gets the fields data from the selected range.
*
* @returns {Object} - Gets the fields data from the selected range.
*/
Sort.prototype.getFields = function () {
var sheet = this.parent.getActiveSheet();
var range = getSwapRange(getIndexesFromAddress(sheet.selectedRange));
if (range[0] === range[2] && (range[2] - range[0]) === 0) { //for entire range
range[0] = 0;
range[1] = 0;
range[3] = sheet.usedRange.colIndex;
var args = { filterRange: [], hasFilter: false };
this.parent.notify(getFilterRange, args);
if (args.hasFilter && args.filterRange) {
range[0] = args.filterRange[0];
}
}
var fields = [];
var text;
var value;
for (range[1]; range[1] <= range[3]; range[1]++) {
var cell = getCell(range[0], range[1], sheet);
value = 'Column ' + getColumnHeaderText(range[1] + 1);
if (cell && cell.value) {
text = cell.value;
if (cell.format) {
var eventArgs = { formattedText: text, value: text, format: cell.format, rowIndex: range[0],
colIndex: range[1] };
this.parent.notify(getFormattedCellObject, eventArgs);
text = eventArgs.formattedText;
}
}
else {
text = value;
}
fields.push({ text: text, value: value });
}
return fields;
};
/**
* Creates the header tab for the custom sort dialog.
*
* @param {HTMLElement} dialogElem - dialog content element.
* @param {ListView} listviewObj - listview instance.
* @param {Object} fields - fields data.
* @returns {void} - set header tab.
*/
Sort.prototype.setHeaderTab = function (dialogElem, listviewObj, fields) {
var _this = this;
var l10n = this.parent.serviceLocator.getService(locale);
var headerTabElement = this.parent.createElement('div', { className: 'e-sort-header' });
dialogElem.appendChild(headerTabElement);
var addButton = this.parent.createElement('button', { className: 'e-btn e-sort-addbtn e-flat', attrs: { 'type': 'button' } });
addButton.innerText = l10n.getConstant('AddColumn');
var footer = this.parent.element.querySelector('.e-customsort-dlg .e-footer-content');
footer.insertBefore(addButton, footer.firstElementChild);
addButton.addEventListener('click', function () {
if (listviewObj) {
var listId = getUniqueID('customSort');
listviewObj.addItem([{ id: listId, text: l10n.getConstant('ThenBy'), field: '', order: 'ascending' }]);
_this.renderListItem(listId, listviewObj, checkHeaderObj.checked, fields, true);
}
});
var checkHeaderObj = new CheckBox({
label: l10n.getConstant('ContainsHeader'),
checked: true,
enableRtl: this.parent.enableRtl,
change: function (args) {
var fieldsMap = args.checked ? { text: 'text', value: 'value' } : { text: 'value' };
Array.prototype.forEach.call(dialogElem.getElementsByClassName('e-sort-field e-dropdownlist e-lib'), function (dropDown) {
var dropDownListObj = getComponent(dropDown, 'dropdownlist');
dropDownListObj.dataSource = null; //reset datasource.
dropDownListObj.dataSource = fields;
dropDownListObj.fields = fieldsMap;
dropDownListObj.dataBind();
});
},
cssClass: 'e-sort-headercheckbox'
});
var headerCheckbox = this.parent.createElement('input', {
className: 'e-sort-checkheader', attrs: { type: 'checkbox' }
});
headerTabElement.appendChild(headerCheckbox);
checkHeaderObj.createElement = this.parent.createElement;
checkHeaderObj.appendTo(headerCheckbox);
var checkCaseObj = new CheckBox({
label: l10n.getConstant('CaseSensitive'),
checked: false,
cssClass: 'e-sort-casecheckbox',
enableRtl: this.parent.enableRtl
});
var caseCheckbox = this.parent.createElement('input', {
className: 'e-sort-checkcase', attrs: { type: 'checkbox' }
});
headerTabElement.appendChild(caseCheckbox);
checkCaseObj.createElement = this.parent.createElement;
checkCaseObj.appendTo(caseCheckbox);
};
/**
* Creates a listview instance.
*
* @param {string} listId - unique id of the list item.
* @returns {void}
*/
Sort.prototype.getCustomListview = function (listId) {
var l10n = this.parent.serviceLocator.getService(locale);
var data = [{ id: listId, text: l10n.getConstant('SortBy'), field: '', order: 'ascending' }];
enableRipple(false);
var listviewObj = new ListView({
dataSource: data,
fields: { id: 'id' },
height: '100%',
enableRtl: this.parent.enableRtl,
/* eslint-disable */
template: initializeCSPTemplate(function (data) {
return ('<div class="e-sort-listwrapper">' +
("<span class=\"text\">" + data.text + "</span>") +
'<div class="e-sort-row"><div class="e-sort-field"></div>' +
'<div class="e-sort-order">' +
("<span class=\"e-sort-ordertxt\" style=\"display:none;\">" + data.order + "</span></div>") +
'<span class="e-icons e-sort-delete" tabindex="0"></span></div>');
}),
/* eslint-enable */
cssClass: 'e-sort-template'
});
return listviewObj;
};
/**
* Triggers the click event for delete icon.
*
* @param {Element} element - current list item element.
* @param {ListView} listviewObj - listview instance.
* @returns {void}
*/
Sort.prototype.deleteHandler = function (element, listviewObj) {
var iconEle = element.getElementsByClassName('e-sort-delete')[0];
//Event handler to bind the click event for delete icon
iconEle.addEventListener('click', function () {
if (element) {
listviewObj.removeItem(element);
}
});
};
/**
* Renders the dropdown and radio button components inside list item.
*
* @param {string} id - unique id of the list item.
* @param {ListView} lvObj - listview instance.
* @param {boolean} containsHeader - data contains header.
* @param {string} fields - fields data.
* @param {boolean} btn - boolean value.
* @returns {void}
*/
Sort.prototype.renderListItem = function (id, lvObj, containsHeader, fields, btn) {
var _this = this;
var l10n = this.parent.serviceLocator.getService(locale);
var element = lvObj.element.querySelector('li[data-uid=' + id + ']');
var fieldsMap = containsHeader ? { text: 'text', value: 'value' } : { text: 'value' };
var dropDown = element.getElementsByClassName('e-sort-field')[0];
var dropDownListObj = new DropDownList({
dataSource: fields,
enableRtl: this.parent.enableRtl,
width: 'auto',
cssClass: 'e-sort-field-ddl',
fields: fieldsMap,
placeholder: l10n.getConstant('SelectAColumn'),
change: function (args) {
if (!args.value) {
return;
}
Array.prototype.some.call(lvObj.dataSource, function (item) {
if (item.id === id) {
item.field = args.value.toString().replace('Column ', '');
}
return item.id === id; //breaks the loop when proper id found
});
_this.clearError();
}
});
dropDownListObj.createElement = this.parent.createElement;
dropDownListObj.appendTo(dropDown);
if (!btn) {
dropDownListObj.index = 0;
}
/* sort ascending radio button */
var orderRadio = element.getElementsByClassName('e-sort-order')[0];
var ordertxtElem = orderRadio.getElementsByClassName('e-sort-ordertxt')[0];
var isAscending = ordertxtElem.innerText.toLocaleLowerCase() === 'ascending';
var radiobutton = new RadioButton({
label: l10n.getConstant('SortAscending'), enableRtl: this.parent.enableRtl,
name: 'sortAZ_' + id, value: 'ascending', checked: isAscending, cssClass: 'e-sort-radiobutton',
change: function (args) { _this.setRadioBtnValue(lvObj, id, args.value); }
});
var radio = this.parent.createElement('input', {
id: 'orderAsc_' + id, className: 'e-sort-radioasc', styles: '', attrs: { type: 'radio' }
});
orderRadio.appendChild(radio);
radiobutton.createElement = this.parent.createElement;
radiobutton.appendTo(radio);
/* sort descending radio button */
var radiobutton2 = new RadioButton({
label: l10n.getConstant('SortDescending'), enableRtl: this.parent.enableRtl,
name: 'sortAZ_' + id, value: 'descending', checked: !isAscending, cssClass: 'e-sort-radiobutton',
change: function (args) { _this.setRadioBtnValue(lvObj, id, args.value); }
});
var radio2 = this.parent.createElement('input', {
id: 'orderDesc_' + id, className: 'e-sort-radiodesc', styles: '', attrs: { type: 'radio' }
});
orderRadio.appendChild(radio2);
radiobutton2.createElement = this.parent.createElement;
radiobutton2.appendTo(radio2);
this.deleteHandler(element, lvObj);
};
/**
* Sets the new value of the radio button.
*
* @param {ListView} listviewObj - listview instance.
* @param {string} id - unique id of the list item.
* @param {string} value - new value.
* @returns {void}
*/
Sort.prototype.setRadioBtnValue = function (listviewObj, id, value) {
if (!value) {
return;
}
Array.prototype.some.call(listviewObj.dataSource, function (item) {
if (item.id === id) {
item.order = value;
}
return item.id === id; //breaks the loop when proper id found
});
};
/**
*
* Clears the error from the dialog.
*
* @returns {void}
*/
Sort.prototype.clearError = function () {
var dialogElem = document.getElementsByClassName('e-sort-dialog')[0];
var errorElem = dialogElem.getElementsByClassName('e-sort-error')[0];
if (errorElem.style.display !== 'none' && errorElem.innerHTML !== '') {
errorElem.style.display = 'none';
Array.prototype.forEach.call(dialogElem.getElementsByClassName('e-error'), function (element) {
element.classList.remove('e-error');
});
}
};
/**
* Triggers sort events and applies sorting.
*
* @param {Object} args - Specifies the args.
* @param {SortOptions} args.sortOptions - Specifies the sort options.
* @param {string} args.range - Specifies the range.
* @param {SortCollectionModel} args.previousSort - Specifies the previous sort collection.
* @returns {void}
*/
Sort.prototype.applySortHandler = function (args) {
var _this = this;
var sheet = this.parent.getActiveSheet();
var address = args && args.range || sheet.selectedRange;
var range = getSwapRange(getIndexesFromAddress(address));
var sortOptions = args && args.sortOptions || { sortDescriptors: {} };
var isSingle = range[0] === range[2];
var eventArgs = { filterRange: [], hasFilter: false, sheetIdx: this.parent.activeSheetIndex };
this.parent.notify(getFilterRange, eventArgs);
if (eventArgs.hasFilter && (isSingle ? inRange(eventArgs.filterRange, range[0], range[1]) : (eventArgs.filterRange[0] ===
range[0] && (this.parent.element.querySelector('.e-selectall.e-highlight') || (range[1] >= eventArgs.filterRange[1] && range[1]
<= eventArgs.filterRange[3]) || (range[3] >= eventArgs.filterRange[1] && range[3] <= eventArgs.filterRange[3]))))) {
range[0] = eventArgs.filterRange[0];
range[1] = eventArgs.filterRange[1];
range[2] = sheet.usedRange.rowIndex;
range[3] = sheet.usedRange.colIndex;
sortOptions.containsHeader = !eventArgs.allowHeaderFilter;
isSingle = false;
}
address = getRangeAddress(range);
var beforeArgs = { range: address, sortOptions: sortOptions, cancel: false };
this.parent.trigger(beforeSort, beforeArgs);
if (beforeArgs.cancel) {
return;
}
this.parent.notify(beginAction, { eventArgs: beforeArgs, action: 'beforeSort' });
this.parent.showSpinner();
var promise = new Promise(function (resolve) { resolve((function () { })()); });
var sortArgs = { args: { range: beforeArgs.range, sortOptions: beforeArgs.sortOptions, checkForHeader: isSingle && address !== beforeArgs.range }, promise: promise, previousSort: args && args.previousSort };
this.parent.notify(initiateSort, sortArgs);
sortArgs.promise.then(function (sortArgs) {
_this.sortCompleteHandler(sortArgs);
_this.parent.trigger(sortComplete, sortArgs);
_this.parent.notify(completeAction, { eventArgs: sortArgs, action: 'sorting' });
if (document.activeElement.tagName === 'BODY') {
focus(_this.parent.element);
}
return Promise.resolve(sortArgs);
}).catch(function (error) {
_this.sortRangeAlertHandler({ error: error });
return Promise.reject(error);
});
};
/**
*
* Invoked when the sort action is completed.
*
* @param {SortEventArgs} args - Specifies the range and sort options.
* @returns {void}
*/
Sort.prototype.sortCompleteHandler = function (args) {
var sheet;
var range;
var sheetIdx = this.parent.activeSheetIndex;
if (args.range.includes('!')) {
var lastIndex = args.range.lastIndexOf('!');
sheetIdx = getSheetIndex(this.parent, args.range.substring(0, lastIndex));
sheet = getSheet(this.parent, sheetIdx);
range = getRangeIndexes(args.range.substring(lastIndex + 1));
}
else {
sheet = this.parent.getActiveSheet();
range = getRangeIndexes(args.range);
}
for (var i = range[0]; i <= range[2]; i++) {
for (var j = range[1]; j <= range[3]; j++) {
this.parent.notify(workbookFormulaOperation, { action: 'refreshCalculate', rowIndex: i, colIndex: j });
}
}
this.parent.serviceLocator.getService('cell').refreshRange(range, true, true, false, false, isImported(this.parent), undefined, undefined, undefined, true);
if (sheetIdx === this.parent.activeSheetIndex && sheet.conditionalFormats && sheet.conditionalFormats.length) {
this.parent.notify(applyCF, { indexes: range });
}
this.parent.hideSpinner();
this.parent.notify(refreshFilterRange, null);
};
return Sort;
}());
export { Sort };