Skip to content

Commit

Permalink
Merge pull request #246 from bregenspan/add-context-menu
Browse files Browse the repository at this point in the history
Add context menu to quickly filter bundles (resolves #241)
  • Loading branch information
th0r committed Apr 10, 2019
2 parents 95a6417 + 2b53d43 commit 4a232f0
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 2 deletions.
16 changes: 16 additions & 0 deletions README.md
Expand Up @@ -148,6 +148,22 @@ as Uglify, then this value will reflect the minified size of your code.

This is the size of running the parsed bundles/modules through gzip compression.

<h2 align="center">Selecting Which Chunks to Display</h2>

When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.

### Sidebar

The Sidebar Menu can be opened by clicking the `>` button at the top left of the report. You can select or deselect chunks to display under the "Show chunks" heading there.

### Chunk Context Menu

The Chunk Context Menu can be opened by right-clicking or `Ctrl`-clicking on a specific chunk in the report. It provides the following options:

* **Hide chunk:** Hides the selected chunk
* **Hide all other chunks:** Hides all chunks besides the selected one
* **Show all chunks:** Un-hides any hidden chunks, returning the report to its initial, unfiltered view

<h2 align="center">Troubleshooting</h2>

### I can't see all the dependencies in a chunk
Expand Down
18 changes: 18 additions & 0 deletions client/components/ContextMenu.css
@@ -0,0 +1,18 @@
.container {
font: var(--main-font);
position: absolute;
padding: 0;
border-radius: 4px;
background: #fff;
border: 1px solid #aaa;
list-style: none;
opacity: 1;
white-space: nowrap;
visibility: visible;
transition: opacity .2s ease, visibility .2s ease;
}

.hidden {
opacity: 0;
visibility: hidden;
}
121 changes: 121 additions & 0 deletions client/components/ContextMenu.jsx
@@ -0,0 +1,121 @@
/** @jsx h */
import {h} from 'preact';
import cls from 'classnames';
import ContextMenuItem from './ContextMenuItem';
import PureComponent from '../lib/PureComponent';
import {store} from '../store';
import {elementIsOutside} from '../utils';

import s from './ContextMenu.css';

export default class ContextMenu extends PureComponent {
componentDidMount() {
this.boundingRect = this.node.getBoundingClientRect();
}

componentDidUpdate(prevProps) {
if (this.props.visible && !prevProps.visible) {
document.addEventListener('mousedown', this.handleDocumentMousedown, true);
} else if (prevProps.visible && !this.props.visible) {
document.removeEventListener('mousedown', this.handleDocumentMousedown, true);
}
}

render() {
const {visible} = this.props;
const containerClassName = cls({
[s.container]: true,
[s.hidden]: !visible
});
const multipleChunksSelected = store.selectedChunks.length > 1;
return (
<ul ref={this.saveNode} className={containerClassName} style={this.getStyle()}>
<ContextMenuItem disabled={!multipleChunksSelected}
onClick={this.handleClickHideChunk}>
Hide chunk
</ContextMenuItem>
<ContextMenuItem disabled={!multipleChunksSelected}
onClick={this.handleClickFilterToChunk}>
Hide all other chunks
</ContextMenuItem>
<hr/>
<ContextMenuItem disabled={store.allChunksSelected}
onClick={this.handleClickShowAllChunks}>
Show all chunks
</ContextMenuItem>
</ul>
);
}

handleClickHideChunk = () => {
const {chunk: selectedChunk} = this.props;
if (selectedChunk && selectedChunk.label) {
const filteredChunks = store.selectedChunks.filter(chunk => chunk.label !== selectedChunk.label);
store.selectedChunks = filteredChunks;
}
this.hide();
}

handleClickFilterToChunk = () => {
const {chunk: selectedChunk} = this.props;
if (selectedChunk && selectedChunk.label) {
const filteredChunks = store.allChunks.filter(chunk => chunk.label === selectedChunk.label);
store.selectedChunks = filteredChunks;
}
this.hide();
}

handleClickShowAllChunks = () => {
store.selectedChunks = store.allChunks;
this.hide();
}

/**
* Handle document-wide `mousedown` events to detect clicks
* outside the context menu.
* @param {MouseEvent} e - DOM mouse event object
* @returns {void}
*/
handleDocumentMousedown = (e) => {
const isSecondaryClick = e.ctrlKey || e.button === 2;
if (!isSecondaryClick && elementIsOutside(e.target, this.node)) {
e.preventDefault();
e.stopPropagation();
this.hide();
}
}

hide() {
if (this.props.onHide) {
this.props.onHide();
}
}

saveNode = node => (this.node = node);

getStyle() {
const {boundingRect} = this;

// Upon the first render of this component, we don't yet know
// its dimensions, so can't position it yet
if (!boundingRect) return;

const {coords} = this.props;

const pos = {
left: coords.x,
top: coords.y
};

if (pos.left + boundingRect.width > window.innerWidth) {
// Shifting horizontally
pos.left = window.innerWidth - boundingRect.width;
}

if (pos.top + boundingRect.height > window.innerHeight) {
// Flipping vertically
pos.top = coords.y - boundingRect.height;
}
return pos;
}
}
19 changes: 19 additions & 0 deletions client/components/ContextMenuItem.css
@@ -0,0 +1,19 @@
.item {
cursor: pointer;
margin: 0;
padding: 8px 14px;
user-select: none;
}

.item:hover {
background: #ffefd7;
}

.disabled {
cursor: default;
color: gray;
}

.item.disabled:hover {
background: transparent;
}
17 changes: 17 additions & 0 deletions client/components/ContextMenuItem.jsx
@@ -0,0 +1,17 @@
/** @jsx h */
import {h} from 'preact';
import cls from 'classnames';
import s from './ContextMenuItem.css';

function noop() {
return false;
}

export default function ContextMenuItem({children, disabled, onClick}) {
const className = cls({
[s.item]: true,
[s.disabled]: disabled
});
const handler = disabled ? noop : onClick;
return (<li className={className} onClick={handler}>{children}</li>);
}
62 changes: 60 additions & 2 deletions client/components/ModulesTreemap.jsx
Expand Up @@ -11,6 +11,7 @@ import Switcher from './Switcher';
import Sidebar from './Sidebar';
import Checkbox from './Checkbox';
import CheckboxList from './CheckboxList';
import ContextMenu from './ContextMenu';

import s from './ModulesTreemap.css';
import Search from './Search';
Expand All @@ -26,13 +27,23 @@ const SIZE_SWITCH_ITEMS = [
@observer
export default class ModulesTreemap extends Component {
state = {
selectedChunk: null,
selectedMouseCoords: {x: 0, y: 0},
sidebarPinned: false,
showChunkContextMenu: false,
showTooltip: false,
tooltipContent: null
};

render() {
const {sidebarPinned, showTooltip, tooltipContent} = this.state;
const {
selectedChunk,
selectedMouseCoords,
sidebarPinned,
showChunkContextMenu,
showTooltip,
tooltipContent
} = this.state;

return (
<div className={s.container}>
Expand Down Expand Up @@ -97,10 +108,16 @@ export default class ModulesTreemap extends Component {
highlightGroups={this.highlightedModules}
weightProp={store.activeSize}
onMouseLeave={this.handleMouseLeaveTreemap}
onGroupHover={this.handleTreemapGroupHover}/>
onGroupHover={this.handleTreemapGroupHover}
onGroupSecondaryClick={this.handleTreemapGroupSecondaryClick}
onResize={this.handleResize}/>
<Tooltip visible={showTooltip}>
{tooltipContent}
</Tooltip>
<ContextMenu onHide={this.handleChunkContextMenuHide}
visible={showChunkContextMenu}
chunk={selectedChunk}
coords={selectedMouseCoords}/>
</div>
);
}
Expand Down Expand Up @@ -180,6 +197,22 @@ export default class ModulesTreemap extends Component {
store.showConcatenatedModulesContent = flag;
}

handleChunkContextMenuHide = () => {
this.setState({
showChunkContextMenu: false
});
}

handleResize = () => {
// Close any open context menu when the report is resized,
// so it doesn't show in an incorrect position
if (this.state.showChunkContextMenu) {
this.setState({
showChunkContextMenu: false
});
}
}

handleSidebarToggle = () => {
if (this.state.sidebarPinned) {
setTimeout(() => this.treemap.resize());
Expand Down Expand Up @@ -211,6 +244,25 @@ export default class ModulesTreemap extends Component {
this.setState({showTooltip: false});
};

handleTreemapGroupSecondaryClick = event => {
const {group, x, y} = event;
if (group && group.isAsset) {
this.setState({
selectedChunk: group,
selectedMouseCoords: {
x,
y
},
showChunkContextMenu: true
});
} else {
this.setState({
selectedChunk: null,
showChunkContextMenu: false
});
}
};

handleTreemapGroupHover = event => {
const {group} = event;

Expand Down Expand Up @@ -245,6 +297,12 @@ export default class ModulesTreemap extends Component {
{module.path &&
<div>Path: <strong>{module.path}</strong></div>
}
{module.isAsset &&
<div>
<br/>
<strong><em>Right-click to view options related to this chunk</em></strong>
</div>
}
</div>
);
}
Expand Down
15 changes: 15 additions & 0 deletions client/components/Treemap.jsx
Expand Up @@ -89,8 +89,18 @@ export default class Treemap extends Component {
};
}
},
/**
* Handle Foamtree's "group clicked" event
* @param {FoamtreeEvent} event - Foamtree event object
* (see https://get.carrotsearch.com/foamtree/demo/api/index.html#event-details)
* @returns {void}
*/
onGroupClick(event) {
preventDefault(event);
if ((event.ctrlKey || event.secondary) && props.onGroupSecondaryClick) {
props.onGroupSecondaryClick.call(component, event);
return;
}
component.zoomOutDisabled = false;
this.zoom(event.group);
},
Expand Down Expand Up @@ -145,7 +155,12 @@ export default class Treemap extends Component {
}

resize = () => {
const {props} = this;
this.treemap.resize();

if (props.onResize) {
props.onResize();
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions client/utils.js
Expand Up @@ -13,3 +13,7 @@ export function walkModules(modules, cb) {
}
}
}

export function elementIsOutside(elem, container) {
return !(elem === container || container.contains(elem));
}
1 change: 1 addition & 0 deletions src/analyzer.js
Expand Up @@ -94,6 +94,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
return _.transform(assets, (result, asset, filename) => {
result.push({
label: filename,
isAsset: true,
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
// be the size of minified bundle.
Expand Down

0 comments on commit 4a232f0

Please sign in to comment.