Skip to content

Commit

Permalink
feat: add support for grid inside Shadow DOM (#1166)
Browse files Browse the repository at this point in the history
* feat: add support for grid inside Shadow DOM
  • Loading branch information
ghiscoding authored Nov 1, 2023
1 parent 90876c9 commit f7b8c46
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 51 deletions.
4 changes: 4 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
{
packageNames: ['rxjs'],
allowedVersions: '7.8.1',
},
{
packageNames: ['slickgrid'],
allowedVersions: '4.x',
}
],
schedule: ['on friday and tuesday'],
Expand Down
2 changes: 2 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Example16 from './examples/example16';
import Example17 from './examples/example17';
import Example18 from './examples/example18';
import Example19 from './examples/example19';
import Example20 from './examples/example20';

export class AppRouting {
constructor(private config: RouterConfig) {
Expand All @@ -45,6 +46,7 @@ export class AppRouting {
{ route: 'example17', name: 'example17', view: './examples/example17.html', viewModel: Example17, title: 'Example17', },
{ route: 'example18', name: 'example18', view: './examples/example18.html', viewModel: Example18, title: 'Example18', },
{ route: 'example19', name: 'example19', view: './examples/example19.html', viewModel: Example19, title: 'Example19', },
{ route: 'example20', name: 'example20', view: './examples/example20.html', viewModel: Example20, title: 'Example20', },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' }
];
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ <h4 class="title is-4 has-text-white">Slickgrid-Universal</h4>
<a class="navbar-item" onclick.delegate="loadRoute('example19')">
Example19 - ExcelCopyBuffer with Cell Selection
</a>
<a class="navbar-item" onclick.delegate="loadRoute('example20')">
Example20 - Basic grid inside a Shadow DOM
</a>
</div>
</div>
</div>
Expand Down
12 changes: 1 addition & 11 deletions examples/vite-demo-vanilla-bundle/src/examples/example18.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,7 @@ const historicSparklineFormatter: Formatter = (_row: number, _cell: number, _val
return svgElem.outerHTML;
};

export default class Example34 {
title = 'Example 34: Real-Time Stock Trading';
subTitle = `Simulate a stock trading platform with lot of price changes
<ul>
<li>you can start/stop the simulation</li>
<li>optionally change random numbers, between 0 and 10 symbols, per cycle (higher numbers means more changes)</li>
<li>optionally change the simulation changes refresh rate in ms (lower number means more changes).</li>
<li>you can Group by 1 of these columns: Currency, Market or Type</li>
<li>to show SlickGrid HUGE PERF., do the following: (1) lower Changes Rate (2) increase both Changes per Cycle and (3) lower Highlight Duration
</ul>`;

export default class Example18 {
columnDefinitions: Column[] = [];
dataset: any[] = [];
gridOptions!: GridOption;
Expand Down
12 changes: 2 additions & 10 deletions examples/vite-demo-vanilla-bundle/src/examples/example19.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,8 @@ import './example19.scss';

const NB_ITEMS = 100;
declare const Slick: SlickNamespace;
export default class Example34 {
export default class Example19 {
protected _eventHandler: SlickEventHandler;
title = 'Example 19: ExcelCopyBuffer with Cell Selection';
subTitle = `Cell Selection using "Shift+{key}" where "key" can be any of:
<ul>
<li>Arrow Up/Down/Left/Right</li>
<li>Page Up/Down</li>
<li>Home</li>
<li>End</li>
</ul>`;

columnDefinitions: Column[] = [];
dataset: any[] = [];
Expand Down Expand Up @@ -71,7 +63,7 @@ export default class Example34 {
id: i,
name: i < 26
? String.fromCharCode('A'.charCodeAt(0) + (i % 26))
: String.fromCharCode('A'.charCodeAt(0) + ((i / 26) | 0) -1) + String.fromCharCode('A'.charCodeAt(0) + (i % 26)),
: String.fromCharCode('A'.charCodeAt(0) + (Math.floor(i / 26)) - 1) + String.fromCharCode('A'.charCodeAt(0) + (i % 26)),
field: i as any,
minWidth: 60,
width: 60,
Expand Down
14 changes: 14 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example20.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<h3 class="title is-3">
Example 20 - Basic grid inside a Shadow DOM
<span class="subtitle">(with Salesforce Theme)</span>
<div class="subtitle" style="float: right; margin-top: -20px">
<span class="is-size-6">see</span>
<a class="is-size-5" target="_blank"
href="https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/vite-demo-vanilla-bundle/src/examples/example19.ts">
<span class="mdi mdi-link-variant mdi-v-align-sub"></span> code
</a>
</div>
</h3>

<div id="host">
</div>
135 changes: 135 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Column, FieldType, Filters, Formatters, GridOption, SlickEventHandler, SlickNamespace, } from '@slickgrid-universal/common';
import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import { ExampleGridOptions } from './example-grid-options';

const NB_ITEMS = 100;
declare const Slick: SlickNamespace;

interface ShadowContainer {
shadow: ShadowRoot;
gridContainer: HTMLDivElement;
}

export default class Example20 {
protected _eventHandler: SlickEventHandler;

columnDefinitions: Column[] = [];
dataset: any[] = [];
gridOptions!: GridOption;
gridContainerElm: HTMLDivElement;
isWithPagination = true;
sgb: SlickVanillaGridBundle;

attached() {
this._eventHandler = new Slick.EventHandler();
const shadowObj = this.createShadowElement();

// define the grid options & columns and then create the grid itself
this.defineGrid(shadowObj);

// mock some data (different in each dataset)
this.dataset = this.getData(NB_ITEMS);
this.gridContainerElm = document.querySelector<HTMLDivElement>(`#host`) as HTMLDivElement;
document.body.classList.add('salesforce-theme');

setTimeout(() => {
this.sgb = new Slicker.GridBundle(shadowObj.gridContainer as HTMLDivElement, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset);
}, 25);
}

/**
* Build the Shadow DOM. In this example, it will
* have just a div for the grid, and a <link>
* for the Alpine style.
*
* Notice that the <link> tag must be placed inside
* the Shadow DOM tree, it cannot be placed on the <head>
* tag because the Shadow DOM is unaffected by external
* styles
*/
createShadowElement(): ShadowContainer {
const host = document.querySelector('#host') as HTMLDivElement;
const shadow = host.attachShadow({ mode: 'open' });
const gridContainer = document.createElement('div');
// gridContainer.style.width = '600px';
// gridContainer.style.height = '500px';
gridContainer.classList.add('grid20');
shadow.appendChild(gridContainer);

const linkElement = document.createElement('link');
linkElement.type = 'text/css';
linkElement.rel = 'stylesheet';
linkElement.href = './src/styles.scss';
shadow.appendChild(linkElement);
return { shadow, gridContainer };
}

dispose() {
this._eventHandler.unsubscribeAll();
this.sgb?.dispose();
this.gridContainerElm.remove();
document.body.classList.remove('salesforce-theme');
}

/* Define grid Options and Columns */
defineGrid(shadowObj) {
this.columnDefinitions = [
{ id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true },
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
{ id: '%', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
{ id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, filter: { model: Filters.compoundDate }, type: FieldType.date, exportWithFormatter: true, filterable: true },
{ id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, filter: { model: Filters.compoundDate }, type: FieldType.date, exportWithFormatter: true, filterable: true },
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', sortable: true, minWidth: 100, filterable: true }
];

this.gridOptions = {
gridHeight: 450,
gridWidth: 800,
enableCellNavigation: true,
enableFiltering: true,
headerRowHeight: 35,
rowHeight: 30,
shadowRoot: shadowObj.shadow
};
}

getData(itemCount: number) {
// mock a dataset
const datasetTmp: any[] = [];
for (let i = 0; i < itemCount; i++) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
const randomPercent = Math.round(Math.random() * 100);

datasetTmp.push({
id: i,
title: 'Task ' + i,
duration: Math.round(Math.random() * 100) + '',
percentComplete: randomPercent,
start: new Date(randomYear, randomMonth + 1, randomDay),
finish: new Date(randomYear + 1, randomMonth + 1, randomDay),
effortDriven: (i % 5 === 0)
});
}

return datasetTmp;
}

generatePhoneNumber(): string {
let phone = '';
for (let i = 0; i < 10; i++) {
phone += Math.round(Math.random() * 9) + '';
}
return phone;
}

// Toggle the Grid Pagination
// IMPORTANT, the Pagination MUST BE CREATED on initial page load before you can start toggling it
// Basically you cannot toggle a Pagination that doesn't exist (must created at the time as the grid)
togglePagination() {
this.isWithPagination = !this.isWithPagination;
this.sgb.paginationService!.togglePaginationVisibility(this.isWithPagination);
this.sgb.slickGrid!.setSelectedRows([]);
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"servor": "^4.0.2",
"slickgrid": "^4.0.3",
"slickgrid": "^4.1.1",
"sortablejs": "^1.15.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"flatpickr": "^4.6.13",
"moment-mini": "^2.29.4",
"multiple-select-vanilla": "^0.5.0",
"slickgrid": "^4.0.3",
"slickgrid": "^4.1.1",
"sortablejs": "^1.15.0",
"un-flatten-tree": "^2.0.12"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const gridStub = {
getCellNode: jest.fn(),
getCellFromEvent: jest.fn(),
getColumns: jest.fn(),
getContainerNode: jest.fn(),
getHeaderColumn: jest.fn(),
getOptions: jest.fn(),
getPreHeaderPanel: jest.fn(),
Expand Down Expand Up @@ -141,6 +142,7 @@ describe('Draggable Grouping Plugin', () => {
sharedService = new SharedService();
translateService = new TranslateServiceStub();
extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService);
jest.spyOn(gridStub, 'getContainerNode').mockReturnValue(document.body as HTMLDivElement);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub);
jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const gridStub = {
autosizeColumns: jest.fn(),
getColumnIndex: jest.fn(),
getColumns: jest.fn(),
getContainerNode: jest.fn(),
getGridPosition: () => ({ width: 10, left: 0 }),
getOptions: jest.fn(),
getSelectedRows: jest.fn(),
Expand Down Expand Up @@ -156,6 +157,7 @@ describe('GridMenuControl', () => {
translateService = new TranslateServiceStub();
extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService);

jest.spyOn(gridStub, 'getContainerNode').mockReturnValue(document.body as HTMLDivElement);
jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock);
jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock);
jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub);
Expand Down
10 changes: 7 additions & 3 deletions packages/common/src/extensions/slickDraggableGrouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export class SlickDraggableGrouping {
return this._gridUid || (this.grid?.getUID() ?? '');
}

get gridContainer() {
return this.grid.getContainerNode();
}

/** Initialize plugin. */
init(grid: SlickGrid, groupingOptions?: DraggableGrouping) {
this._addonOptions = { ...this._defaults, ...groupingOptions };
Expand Down Expand Up @@ -215,7 +219,7 @@ export class SlickDraggableGrouping {
this._eventHandler.unsubscribeAll();
this.pubSubService.unsubscribeAll(this._subscriptions);
this._bindingEventService.unbindAll();
emptyElement(document.querySelector(`.${this.gridUid} .slick-preheader-panel`));
emptyElement(this.gridContainer.querySelector(`.${this.gridUid} .slick-preheader-panel`));
}

clearDroppedGroups() {
Expand Down Expand Up @@ -354,8 +358,8 @@ export class SlickDraggableGrouping {
}
} as SortableOptions;

this._sortableLeftInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-left`) as HTMLDivElement, sortableOptions) as SortableInstance;
this._sortableRightInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-right`) as HTMLDivElement, sortableOptions) as SortableInstance;
this._sortableLeftInstance = Sortable.create(this.gridContainer.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-left`) as HTMLDivElement, sortableOptions) as SortableInstance;
this._sortableRightInstance = Sortable.create(this.gridContainer.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-right`) as HTMLDivElement, sortableOptions) as SortableInstance;

return {
sortableLeftInstance: this._sortableLeftInstance,
Expand Down
5 changes: 3 additions & 2 deletions packages/common/src/extensions/slickGridMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ export class SlickGridMenu extends MenuBaseClass<GridMenu> {
const gridUidSelector = this._gridUid ? `.${this._gridUid}` : '';
const gridMenuWidth = this._addonOptions?.menuWidth || this._defaults.menuWidth;
const headerSide = (this.gridOptions.hasOwnProperty('frozenColumn') && this.gridOptions.frozenColumn! >= 0) ? 'right' : 'left';
this._headerElm = document.querySelector<HTMLDivElement>(`${gridUidSelector} .slick-header-${headerSide}`);
const gridContainer = this.grid.getContainerNode();
this._headerElm = gridContainer.querySelector<HTMLDivElement>(`.slick-header-${headerSide}`);

if (this._headerElm && this._addonOptions) {
// resize the header row to include the hamburger menu icon
Expand All @@ -196,7 +197,7 @@ export class SlickGridMenu extends MenuBaseClass<GridMenu> {
// if header row is enabled, we also need to resize its width
const enableResizeHeaderRow = this._addonOptions.resizeOnShowHeaderRow ?? this._defaults.resizeOnShowHeaderRow;
if (enableResizeHeaderRow && this.gridOptions.showHeaderRow) {
const headerRowElm = document.querySelector<HTMLDivElement>(`${gridUidSelector} .slick-headerrow`);
const headerRowElm = gridContainer.querySelector<HTMLDivElement>(`${gridUidSelector} .slick-headerrow`);
if (headerRowElm) {
headerRowElm.style.width = `calc(100% - ${gridMenuWidth}px)`;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/interfaces/gridOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,9 @@ export interface GridOption {
/** CSS class name used when cell is selected */
selectedCellCssClass?: string;

/** Defaults to undefined. If we are inside a Shadow DOM tree, this must be the shadow root of the tree */
shadowRoot?: ShadowRoot;

/** Do we want to show cell selection? */
showCellSelection?: boolean;

Expand Down
Loading

0 comments on commit f7b8c46

Please sign in to comment.