diff --git a/examples/webpack-demo-vanilla-bundle/package.json b/examples/webpack-demo-vanilla-bundle/package.json
index 3122ca549..e057bb679 100644
--- a/examples/webpack-demo-vanilla-bundle/package.json
+++ b/examples/webpack-demo-vanilla-bundle/package.json
@@ -44,6 +44,9 @@
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
+ "@fnando/sparkline": "^0.3.10",
+ "@types/faker": "^5.5.9",
+ "@types/fnando__sparkline": "^0.3.4",
"@types/jquery": "^3.5.11",
"@types/moment": "^2.13.0",
"@types/node": "^17.0.5",
@@ -51,6 +54,7 @@
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "^10.2.0",
"css-loader": "^6.5.1",
+ "faker": "^5.5.3",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.5.0",
"html-loader": "^3.0.1",
diff --git a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
index acbcbd8cf..632b1faa5 100644
--- a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
@@ -21,6 +21,7 @@ export class AppRouting {
{ route: 'example15', name: 'example15', title: 'Example15', moduleId: './examples/example15' },
{ route: 'example16', name: 'example16', title: 'Example16', moduleId: './examples/example16' },
{ route: 'example17', name: 'example17', title: 'Example17', moduleId: './examples/example17' },
+ { route: 'example18', name: 'example18', title: 'Example18', moduleId: './examples/example18' },
{ route: 'icons', name: 'icons', title: 'icons', moduleId: './examples/icons' },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' }
diff --git a/examples/webpack-demo-vanilla-bundle/src/app.html b/examples/webpack-demo-vanilla-bundle/src/app.html
index 15aa14378..994850756 100644
--- a/examples/webpack-demo-vanilla-bundle/src/app.html
+++ b/examples/webpack-demo-vanilla-bundle/src/app.html
@@ -33,7 +33,7 @@
Slickgrid-Universal
+
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.html b/examples/webpack-demo-vanilla-bundle/src/examples/example18.html
new file mode 100644
index 000000000..e889b38b1
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.html
@@ -0,0 +1,59 @@
+
+ Example 18 - Real-Time Trading Platform
+ (with Material Theme)
+
+
+
+ Simulate a stock trading platform with lot of price changes, to show SlickGrid HUGE PERF., do the following: (1) lower
+ Changes Rate (2) increase both Changes per Cycle and (3) lower Highlight Duration
+
+
+
\ No newline at end of file
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss
new file mode 100644
index 000000000..791d364ec
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss
@@ -0,0 +1,51 @@
+// @import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.lite.scss';
+
+$sparkline-color: #00b78d;
+// $sparkline-color: #573585;
+
+.trading-platform.full-screen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 10px 12px 0 10px;
+ background-color: white;
+ z-index: 8000;
+ position: fixed;
+}
+.changed-gain {
+ background-color: #eafae8 !important;
+}
+.changed-loss {
+ background-color: #ffeae8 !important;
+}
+.simulation-form {
+ margin-bottom: 15px;
+
+ input[type=number] {
+ height: 30px;
+ width: 50px;
+ border: 1px solid #c0c0c0;
+ border-radius: 3px;
+ }
+ div.range {
+ display: contents;
+ width: 200px;
+ label.form-label {
+ margin: 0;
+ }
+ input.form-range {
+ width: 120px;
+ }
+ }
+ .refresh-rate input {
+ height: 30px;
+ width: 46px;
+ }
+}
+.sparkline {
+ stroke: $sparkline-color;
+ // fill: none;
+ fill: rgba($sparkline-color, 0.03);
+}
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts
new file mode 100644
index 000000000..cee136c88
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts
@@ -0,0 +1,314 @@
+import * as Faker from 'faker';
+import sparkline from '@fnando/sparkline';
+import {
+ Aggregators,
+ Column,
+ deepCopy,
+ FieldType,
+ Filters,
+ Formatter,
+ Formatters,
+ GridOption,
+ GroupTotalFormatters,
+} from '@slickgrid-universal/common';
+import './example18.scss';
+import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
+import { ExampleGridOptions } from './example-grid-options';
+
+const NB_ITEMS = 200;
+
+const currencyFormatter: Formatter = (cell: number, row: number, value: string) =>
+ ` ${value}`;
+
+const priceFormatter: Formatter = (cell: number, row: number, value: number, col: Column, dataContext: any) => {
+ const direction = dataContext.priceChange >= 0 ? 'up' : 'down';
+ return ` ${value}`;
+};
+
+const transactionTypeFormatter: Formatter = (row: number, cell: number, value: string) =>
+ ` ${value}`;
+
+const historicSparklineFormatter: Formatter = (row: number, cell: number, value: string, col: Column, dataContext: any) => {
+ const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svgElem.setAttributeNS(null, 'width', '135');
+ svgElem.setAttributeNS(null, 'height', '30');
+ svgElem.setAttributeNS(null, 'stroke-width', '2');
+ svgElem.classList.add('sparkline');
+ sparkline(svgElem, dataContext.historic, { interactive: true });
+ return svgElem.outerHTML;
+};
+
+export class Example34 {
+ title = 'Example 34: Real-Time Stock Trading';
+ subTitle = `Simulate a stock trading platform with lot of price changes
+
+ - you can start/stop the simulation
+ - optionally change random numbers, between 0 and 10 symbols, per cycle (higher numbers means more changes)
+ - optionally change the simulation changes refresh rate in ms (lower number means more changes).
+ - you can Group by 1 of these columns: Currency, Market or Type
+ - to show SlickGrid HUGE PERF., do the following: (1) lower Changes Rate (2) increase both Changes per Cycle and (3) lower Highlight Duration
+
`;
+
+ columnDefinitions: Column[] = [];
+ dataset: any[] = [];
+ gridOptions!: GridOption;
+ isFullScreen = false;
+ highlightDuration = 150;
+ itemCount = 200;
+ minChangePerCycle = 0;
+ maxChangePerCycle = 10;
+ refreshRate = 75;
+ timer: any;
+ toggleClassName = this.isFullScreen ? 'icon mdi mdi-arrow-collapse' : 'icon mdi mdi-arrow-expand';
+ sgb: SlickVanillaGridBundle;
+
+ attached() {
+ // define the grid options & columns and then create the grid itself
+ this.defineGrid();
+
+ // mock some data (different in each dataset)
+ this.dataset = this.getData(NB_ITEMS);
+ this.sgb = new Slicker.GridBundle(document.querySelector(`.grid18`), this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset);
+
+ setTimeout(() => {
+ this.startSimulation();
+ }, this.refreshRate);
+ }
+
+ dispose() {
+ this.stopSimulation();
+ this.sgb?.dispose();
+ }
+
+ /* Define grid Options and Columns */
+ defineGrid() {
+ // the columns field property is type-safe, try to add a different string not representing one of DataItems properties
+ this.columnDefinitions = [
+ {
+ id: 'currency', name: 'Currency', field: 'currency', filterable: true, sortable: true, minWidth: 65, width: 65,
+ formatter: currencyFormatter,
+ filter: {
+ model: Filters.singleSelect,
+ collection: [{ label: '', value: '' }, { label: 'CAD', value: 'CAD' }, { label: 'USD', value: 'USD' }]
+ },
+ grouping: {
+ getter: 'currency',
+ formatter: (g) => `Currency: ${g.value} (${g.count} items)`,
+ aggregators: [
+ new Aggregators.Sum('amount')
+ ],
+ aggregateCollapsed: true,
+ collapsed: false
+ }
+ },
+ { id: 'symbol', name: 'Symbol', field: 'symbol', filterable: true, sortable: true, minWidth: 65, width: 65 },
+ {
+ id: 'market', name: 'Market', field: 'market', filterable: true, sortable: true, minWidth: 75, width: 75,
+ grouping: {
+ getter: 'market',
+ formatter: (g) => `Market: ${g.value} (${g.count} items)`,
+ aggregators: [
+ new Aggregators.Sum('amount')
+ ],
+ aggregateCollapsed: true,
+ collapsed: false
+ }
+ },
+ { id: 'company', name: 'Company', field: 'company', filterable: true, sortable: true, minWidth: 80, width: 130 },
+ {
+ id: 'trsnType', name: 'Type', field: 'trsnType', filterable: true, sortable: true, minWidth: 60, width: 60,
+ formatter: transactionTypeFormatter,
+ filter: {
+ model: Filters.singleSelect,
+ collection: [{ label: '', value: '' }, { label: 'Buy', value: 'Buy' }, { label: 'Sell', value: 'Sell' }]
+ },
+ grouping: {
+ getter: 'trsnType',
+ formatter: (g) => `Type: ${g.value} (${g.count} items)`,
+ aggregators: [
+ new Aggregators.Sum('amount')
+ ],
+ aggregateCollapsed: true,
+ collapsed: false
+ }
+ },
+ {
+ id: 'priceChange', name: 'Change', field: 'priceChange', filterable: true, sortable: true, minWidth: 80, width: 80,
+ filter: { model: Filters.compoundInputNumber }, type: FieldType.number,
+ formatter: Formatters.multiple,
+ params: {
+ formatters: [Formatters.dollarColored, priceFormatter],
+ maxDecimal: 2,
+ }
+
+ },
+ {
+ id: 'price', name: 'Price', field: 'price', filterable: true, sortable: true, minWidth: 70, width: 70,
+ filter: { model: Filters.compoundInputNumber }, type: FieldType.number,
+ formatter: Formatters.dollar, params: { maxDecimal: 2 }
+ },
+ {
+ id: 'quantity', name: 'Quantity', field: 'quantity', filterable: true, sortable: true, minWidth: 70, width: 70,
+ filter: { model: Filters.compoundInputNumber }, type: FieldType.number,
+ },
+ {
+ id: 'amount', name: 'Amount', field: 'amount', filterable: true, sortable: true, minWidth: 70, width: 60,
+ filter: { model: Filters.compoundInputNumber }, type: FieldType.number,
+ formatter: Formatters.dollar, params: { maxDecimal: 2 },
+ groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarBold,
+ },
+ { id: 'historic', name: 'Price History', field: 'historic', minWidth: 100, width: 150, maxWidth: 150, formatter: historicSparklineFormatter },
+ {
+ id: 'execution', name: 'Execution Timestamp', field: 'execution', filterable: true, sortable: true, minWidth: 125,
+ formatter: Formatters.dateTimeIsoAmPm, exportWithFormatter: true,
+ type: FieldType.dateTimeIsoAM_PM, filter: { model: Filters.compoundDate }
+ },
+ ];
+
+ this.gridOptions = {
+ autoResize: {
+ container: '.trading-platform',
+ rightPadding: 0,
+ bottomPadding: 20,
+ },
+ formatterOptions: {
+ displayNegativeNumberWithParentheses: true,
+ thousandSeparator: ','
+ },
+ draggableGrouping: {
+ dropPlaceHolderText: 'Drop a column header here to group by any of these available columns: Currency, Market or Type',
+ deleteIconCssClass: 'mdi mdi-close color-danger',
+ },
+ enableDraggableGrouping: true,
+ createPreHeaderPanel: true,
+ showPreHeaderPanel: true,
+ preHeaderPanelHeight: 40,
+ enableCellNavigation: true,
+ enableFiltering: true,
+ cellHighlightCssClass: 'changed',
+ rowHeight: 40
+ };
+ }
+
+ getData(itemCount: number) {
+ // mock a dataset
+ const datasetTmp = [];
+ for (let i = 0; i < itemCount; i++) {
+ const randomPercent = Math.round(Math.random() * 100);
+ const randomLowQty = this.randomNumber(1, 50);
+ const randomHighQty = this.randomNumber(125, 255);
+ const priceChange = this.randomNumber(-25, 35, false);
+ const price = this.randomNumber(priceChange, 300);
+ const quantity = price < 5 ? randomHighQty : randomLowQty;
+ const amount = price * quantity;
+ const now = new Date();
+ now.setHours(9, 30, 0);
+ const currency = (Math.floor(Math.random() * 10)) % 2 ? 'CAD' : 'USD';
+ const company = Faker.company.companyName();
+
+ datasetTmp[i] = {
+ id: i,
+ currency,
+ trsnType: (Math.round(Math.random() * 100)) % 2 ? 'Buy' : 'Sell',
+ company,
+ symbol: currency === 'CAD' ? company.substr(0, 3).toUpperCase() + '.TO' : company.substr(0, 4).toUpperCase(),
+ market: currency === 'CAD' ? 'TSX' : price > 200 ? 'Nasdaq' : 'S&P 500',
+ duration: (i % 33 === 0) ? null : Math.random() * 100 + '',
+ percentCompleteNumber: randomPercent,
+ priceChange,
+ price,
+ quantity,
+ amount,
+ execution: now,
+ historic: [price]
+ };
+ }
+ return datasetTmp;
+ }
+
+ startSimulation() {
+ const changes: any = {};
+ const numberOfUpdates = this.randomNumber(this.minChangePerCycle, this.maxChangePerCycle);
+
+ for (let i = 0; i < numberOfUpdates; i++) {
+ const randomLowQty = this.randomNumber(1, 50);
+ const randomHighQty = this.randomNumber(125, 255);
+ const rowNumber = Math.round(Math.random() * (this.dataset.length - 1));
+ const priceChange = this.randomNumber(-25, 25, false);
+ const prevItem = deepCopy(this.dataset[rowNumber]);
+ const itemTmp = { ...this.dataset[rowNumber] };
+ itemTmp.priceChange = priceChange;
+ itemTmp.price = ((itemTmp.price + priceChange) < 0) ? 0 : itemTmp.price + priceChange;
+ itemTmp.quantity = itemTmp.price < 5 ? randomHighQty : randomLowQty;
+ itemTmp.amount = itemTmp.price * itemTmp.quantity;
+ itemTmp.trsnType = (Math.round(Math.random() * 100)) % 2 ? 'Buy' : 'Sell';
+ itemTmp.execution = new Date();
+ itemTmp.historic.push(itemTmp.price);
+ itemTmp.historic = itemTmp.historic.slice(-20); // keep a max of X historic values
+
+ if (!changes[rowNumber]) {
+ changes[rowNumber] = {};
+ }
+
+ // highlight whichever cell is being changed
+ changes[rowNumber]['id'] = 'changed';
+ this.renderCellHighlighting(itemTmp, this.findColumnById('priceChange'), priceChange);
+ if ((prevItem.priceChange < 0 && itemTmp.priceChange > 0) || (prevItem.priceChange > 0 && itemTmp.priceChange < 0)) {
+ this.renderCellHighlighting(itemTmp, this.findColumnById('price'), priceChange);
+ }
+ // if (prevItem.trsnType !== itemTmp.trsnType) {
+ // this.renderCellHighlighting(itemTmp, this.findColumnById('trsnType'), priceChange);
+ // }
+
+ // update the data
+ this.sgb.dataView.updateItem(itemTmp.id, itemTmp);
+ // NOTE: we should also invalidate/render the row after updating cell data to see the new data rendered in the UI
+ // but the cell highlight actually does that for us so we can skip it
+ }
+
+ this.timer = setTimeout(this.startSimulation.bind(this), this.refreshRate || 0);
+ }
+
+ stopSimulation() {
+ clearTimeout(this.timer);
+ }
+
+ findColumnById(columnName: string): Column {
+ return this.columnDefinitions.find(col => col.field === columnName) as Column;
+ }
+
+ renderCellHighlighting(item: any, column: Column, priceChange: number) {
+ if (item && column) {
+ const row = this.sgb.dataView.getRowByItem(item) as number;
+ if (row >= 0) {
+ const hash = { [row]: { [column.id]: priceChange >= 0 ? 'changed-gain' : 'changed-loss' } };
+ this.sgb.slickGrid.setCellCssStyles(`highlight_${[column.id]}${row}`, hash);
+
+ // remove highlight after x amount of time
+ setTimeout(() => this.removeUnsavedStylingFromCell(item, column, row), this.highlightDuration);
+ }
+ }
+ }
+
+ /** remove change highlight css class from that cell */
+ removeUnsavedStylingFromCell(_item: any, column: Column, row: number) {
+ this.sgb.slickGrid.removeCellCssStyles(`highlight_${[column.id]}${row}`);
+ }
+
+ toggleFullScreen() {
+ const container = document.querySelector('.trading-platform');
+ if (container?.classList.contains('full-screen')) {
+ container.classList.remove('full-screen');
+ this.isFullScreen = false;
+ } else if (container) {
+ container.classList.add('full-screen');
+ this.isFullScreen = true;
+ }
+ this.sgb.resizerService.resizeGrid();
+ }
+
+ private randomNumber(min: number, max: number, floor = true) {
+ const number = Math.random() * (max - min + 1) + min;
+ return floor ? Math.floor(number) : number;
+ }
+}
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts b/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts
index 4b0b36143..f8c60ed91 100644
--- a/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts
@@ -176,6 +176,8 @@ export class Icons {
'.mdi.mdi-message-text-outline',
'.mdi.mdi-microsoft-excel',
'.mdi.mdi-minus',
+ '.mdi.mdi-minus-circle',
+ '.mdi.mdi-minus-circle-outline',
'.mdi.mdi-order-bool-ascending-variant',
'.mdi.mdi-page-first',
'.mdi.mdi-page-last',
@@ -188,9 +190,12 @@ export class Icons {
'.mdi.mdi-percent-outline',
'.mdi.mdi-pin-off-outline',
'.mdi.mdi-pin-outline',
+ '.mdi.mdi-play-circle-outline',
'.mdi.mdi-playlist-plus',
'.mdi.mdi-playlist-remove',
'.mdi.mdi-plus',
+ '.mdi.mdi-plus-circle',
+ '.mdi.mdi-plus-circle-outline',
'.mdi.mdi-progress-download',
'.mdi.mdi-redo',
'.mdi.mdi-refresh',
@@ -200,6 +205,7 @@ export class Icons {
'.mdi.mdi-sort-descending',
'.mdi.mdi-sort-variant-remove',
'.mdi.mdi-square-edit-outline',
+ '.mdi.mdi-stop-circle-outline',
'.mdi.mdi-subdirectory-arrow-right',
'.mdi.mdi-swap-horizontal',
'.mdi.mdi-swap-vertical',
diff --git a/examples/webpack-demo-vanilla-bundle/src/renderer.ts b/examples/webpack-demo-vanilla-bundle/src/renderer.ts
index d87763972..3bd3214c2 100644
--- a/examples/webpack-demo-vanilla-bundle/src/renderer.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/renderer.ts
@@ -118,6 +118,8 @@ export class Renderer {
observer.bind(elements, attribute, 'change').bind(elements, attribute, 'keyup');
break;
case 'checked':
+ case 'min':
+ case 'max':
default:
observer.bind(elements, attribute, 'change');
break;
diff --git a/packages/common/src/styles/material-svg-icons.scss b/packages/common/src/styles/material-svg-icons.scss
index f7d7628bb..29ba1cb18 100644
--- a/packages/common/src/styles/material-svg-icons.scss
+++ b/packages/common/src/styles/material-svg-icons.scss
@@ -756,6 +756,16 @@ $slick-icon-height: $slick-icon-width;
"M19,13H5V11H19V13Z",
encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+@include loadsvg(
+ ".mdi.mdi-minus-circle",
+ "M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
+@include loadsvg(
+ ".mdi.mdi-minus-circle-outline",
+ "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
@include loadsvg(
".mdi.mdi-order-bool-ascending-variant",
"M4 13C2.89 13 2 13.89 2 15V19C2 20.11 2.89 21 4 21H8C9.11 21 10 20.11 10 19V15C10 13.89 9.11 13 8 13M8.2 14.5L9.26 15.55L5.27 19.5L2.74 16.95L3.81 15.9L5.28 17.39M4 3C2.89 3 2 3.89 2 5V9C2 10.11 2.89 11 4 11H8C9.11 11 10 10.11 10 9V5C10 3.89 9.11 3 8 3M4 5H8V9H4M12 5H22V7H12M12 19V17H22V19M12 11H22V13H12Z",
@@ -821,11 +831,26 @@ $slick-icon-height: $slick-icon-width;
"M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z",
encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+@include loadsvg(
+ ".mdi.mdi-play-circle-outline",
+ "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
@include loadsvg(
".mdi.mdi-playlist-remove",
"M2,6V8H14V6H2M2,10V12H11V10H2M14.17,10.76L12.76,12.17L15.59,15L12.76,17.83L14.17,19.24L17,16.41L19.83,19.24L21.24,17.83L18.41,15L21.24,12.17L19.83,10.76L17,13.59L14.17,10.76M2,14V16H11V14H2Z",
encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+@include loadsvg(
+ ".mdi.mdi-plus-circle",
+ "M17,13H13V17H11V13H7V11H11V7H13V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
+@include loadsvg(
+ ".mdi.mdi-plus-circle-outline",
+ "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
@include loadsvg(
".mdi.mdi-plus",
"M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z",
@@ -876,6 +901,11 @@ $slick-icon-height: $slick-icon-width;
"M5,3C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19H5V5H12V3H5M17.78,4C17.61,4 17.43,4.07 17.3,4.2L16.08,5.41L18.58,7.91L19.8,6.7C20.06,6.44 20.06,6 19.8,5.75L18.25,4.2C18.12,4.07 17.95,4 17.78,4M15.37,6.12L8,13.5V16H10.5L17.87,8.62L15.37,6.12Z",
encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+@include loadsvg(
+ ".mdi.mdi-stop-circle-outline",
+ "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9",
+ encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block);
+
@include loadsvg(
".mdi.mdi-subdirectory-arrow-right",
"M19,15L13,21L11.58,19.58L15.17,16H4V4H6V14H15.17L11.58,10.42L13,9L19,15Z",
diff --git a/test/cypress/integration/example18.spec.js b/test/cypress/integration/example18.spec.js
new file mode 100644
index 000000000..6c4f95e45
--- /dev/null
+++ b/test/cypress/integration/example18.spec.js
@@ -0,0 +1,69 @@
+///
+
+describe('Example 18 - Real-Time Trading Platform', { retries: 1 }, () => {
+ const titles = ['Currency', 'Symbol', 'Market', 'Company', 'Type', 'Change', 'Price', 'Quantity', 'Amount', 'Price History', 'Execution Timestamp'];
+ const GRID_ROW_HEIGHT = 40;
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseExampleUrl')}/example18`);
+ cy.get('h3').should('contain', 'Example 18 - Real-Time Trading Platform');
+ });
+
+ it('should have exact column titles on 1st grid', () => {
+ cy.get('.grid18')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+
+ it('should check first 5 rows and expect certain data', () => {
+ for (let i = 0; i < 5; i++) {
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(0)`).contains(/CAD|USD$/);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(4)`).contains(/Buy|Sell$/);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(5)`).contains(/\$\(?[0-9\.]*\)?/);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(6)`).contains(/\$[0-9\.]*/);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(7)`).contains(/\d$/);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\.]*/);
+ }
+ });
+
+ it('should find multiple green & pink backgrounds to show gains & losses when in real-time mode', () => {
+ cy.get('#refreshRateRange').invoke('val', 5).trigger('change');
+
+ cy.get('.changed-gain').should('have.length.gt', 2);
+ cy.get('.changed-loss').should('have.length.gt', 2);
+ });
+
+ it('should NOT find any green neither pink backgrounds when in real-time is stopped', () => {
+ cy.get('[data-test="highlight-input"]').type('{backspace}{backspace}');
+ cy.get('[data-test="stop-btn"]').click();
+
+ cy.wait(5);
+ cy.get('.changed-gain').should('have.length', 0);
+ cy.get('.changed-loss').should('have.length', 0);
+ cy.wait(1);
+ cy.get('.changed-gain').should('have.length', 0);
+ cy.get('.changed-loss').should('have.length', 0);
+ });
+
+ it('should Group by 1st column "Currency" and expect 2 groups with Totals when collapsed', () => {
+ cy.get('.slick-column-name')
+ .first()
+ .trigger('mousedown', { button: 1, force: true })
+
+ cy.get('.slick-draggable-dropbox-toggle-placeholder')
+ .trigger('mousemove', 'center')
+ .trigger('mouseup', 'center', { force: true });
+
+ cy.get('.slick-group-toggle-all')
+ .click();
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: CAD');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\,\.]*/);
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1);
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: USD');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\,\.]*/);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index fb3ff067e..581c3f8ff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -576,6 +576,11 @@
unique-filename "^1.1.1"
which "^1.3.1"
+"@fnando/sparkline@^0.3.10":
+ version "0.3.10"
+ resolved "https://registry.yarnpkg.com/@fnando/sparkline/-/sparkline-0.3.10.tgz#0cb6549a232af0f19f75b33d38fddd4f5ed9f086"
+ integrity sha512-Rwz2swatdSU5F4sCOvYG8EOWdjtLgq5d8nmnqlZ3PXdWJI9Zq9BRUvJ/9ygjajJG8qOyNpMFX3GEVFjZIuB1Jg==
+
"@humanwhocodes/config-array@^0.9.2":
version "0.9.2"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
@@ -1834,6 +1839,16 @@
"@types/qs" "*"
"@types/serve-static" "*"
+"@types/faker@^5.5.9":
+ version "5.5.9"
+ resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c"
+ integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA==
+
+"@types/fnando__sparkline@^0.3.4":
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/@types/fnando__sparkline/-/fnando__sparkline-0.3.4.tgz#6a4a1a57983c1ecabf782abfb29ce864a9bbbf16"
+ integrity sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==
+
"@types/glob@5.0.30":
version "5.0.30"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.30.tgz#1026409c5625a8689074602808d082b2867b8a51"
@@ -5300,6 +5315,11 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+faker@^5.5.3:
+ version "5.5.3"
+ resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
+ integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==
+
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"