diff --git a/frontend/Node.js b/frontend/Node.js index 4984a306ec..a7c5ff345e 100644 --- a/frontend/Node.js +++ b/frontend/Node.js @@ -15,6 +15,7 @@ var React = require('react'); var assign = require('object-assign'); var decorate = require('./decorate'); var Props = require('./Props'); +var shouldSkipToChildRendering = require('../plugins/Wrappers/shouldSkipToChildRendering'); import type {Map} from 'immutable'; @@ -71,6 +72,10 @@ class Node extends React.Component { } var children = node.get('children'); + if (this.props.skipToChildRendering) { + return ; + } + if (node.get('nodeType') === 'Wrapper') { return ( @@ -257,7 +262,7 @@ Node.contextTypes = { var WrappedNode = decorate({ listeners(props) { - return [props.id]; + return [props.id, 'hidewrapperschange']; }, props(store, props) { var node = store.get(props.id); @@ -266,9 +271,11 @@ var WrappedNode = decorate({ var child = store.get(node.get('children')[0]); wrappedChildren = child && child.get('children'); } + return { node, wrappedChildren, + skipToChildRendering: shouldSkipToChildRendering(node, store), selected: store.selected === props.id, isBottomTagSelected: store.isBottomTagSelected, hovered: store.hovered === props.id, diff --git a/frontend/SettingsPane.js b/frontend/SettingsPane.js index c4518b1377..b2d34bb8fa 100644 --- a/frontend/SettingsPane.js +++ b/frontend/SettingsPane.js @@ -12,6 +12,7 @@ var BananaSlugFrontendControl = require('../plugins/BananaSlug/BananaSlugFrontendControl'); var ColorizerFrontendControl = require('../plugins/Colorizer/ColorizerFrontendControl'); var RegexFrontendControl = require('../plugins/Regex/RegexFrontendControl'); +var WrappersFrontendControl = require('../plugins/Wrappers/WrappersFrontendControl'); var React = require('react'); class SettingsPane extends React.Component { @@ -21,6 +22,7 @@ class SettingsPane extends React.Component { + ); } diff --git a/frontend/Store.js b/frontend/Store.js index 6b66b9fb8f..f80a6dabca 100644 --- a/frontend/Store.js +++ b/frontend/Store.js @@ -90,6 +90,7 @@ class Store extends EventEmitter { bananaslugState: ?ControlState; colorizerState: ?ControlState; regexState: ?ControlState; + hideWrappersState: ?ControlState; contextMenu: ?ContextMenu; hovered: ?ElementID; isBottomTagSelected: boolean; @@ -127,6 +128,7 @@ class Store extends EventEmitter { this.bananaslugState = null; this.colorizerState = null; this.regexState = null; + this.hideWrappersState = null; this.placeholderText = DEFAULT_PLACEHOLDER; this.refreshSearch = false; @@ -496,6 +498,11 @@ class Store extends EventEmitter { this.changeSearch(this.searchText); } + toggleHideWrappers(state: ControlState) { + this.hideWrappersState = state; + this.emit('hidewrapperschange'); + } + // Private stuff _establishConnection() { var tries = 0; diff --git a/frontend/decorate.js b/frontend/decorate.js index c19491ac27..95429d824e 100644 --- a/frontend/decorate.js +++ b/frontend/decorate.js @@ -56,7 +56,8 @@ module.exports = function(options: Options, Component: any): any { var storeKey = options.store || 'store'; class Wrapper extends React.Component { _listeners: Array; - _update: () => void; + _update: () => boolean|void; + _mounted: boolean; state: State; constructor(props) { @@ -64,12 +65,16 @@ module.exports = function(options: Options, Component: any): any { this.state = {}; } + componentDidMount() { + this._mounted = true; + } + componentWillMount() { if (!this.context[storeKey]) { console.warn('no store on context...'); return; } - this._update = () => this.forceUpdate(); + this._update = () => this._mounted && this.forceUpdate(); if (!options.listeners) { return; } @@ -87,6 +92,7 @@ module.exports = function(options: Options, Component: any): any { this._listeners.forEach(evt => { this.context[storeKey].off(evt, this._update); }); + this._mounted = false; } shouldComponentUpdate(nextProps, nextState) { diff --git a/plugins/Wrappers/WrappersFrontendControl.js b/plugins/Wrappers/WrappersFrontendControl.js new file mode 100644 index 0000000000..c034383b1f --- /dev/null +++ b/plugins/Wrappers/WrappersFrontendControl.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +'use strict'; + +var decorate = require('../../frontend/decorate'); +var SettingsCheckbox = require('../../frontend/SettingsCheckbox'); + +var Wrapped = decorate({ + listeners() { + return ['hidewrapperschange']; + }, + props(store) { + return { + state: store.hideWrappersState, + text: 'Hide Wrappers', + onChange: state => store.toggleHideWrappers(state), + }; + }, +}, SettingsCheckbox); + +module.exports = Wrapped; diff --git a/plugins/Wrappers/shouldSkipToChildRendering.js b/plugins/Wrappers/shouldSkipToChildRendering.js new file mode 100644 index 0000000000..b3e9b09efe --- /dev/null +++ b/plugins/Wrappers/shouldSkipToChildRendering.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +/* + * Should hide higher-order wrapper components. An HOC is defined + * as a component that returns a single, non-DOM child. + */ +function shouldSkipToChildRendering(node, store) { + if (store.hideWrappersState && store.hideWrappersState.enabled) { + const children = node.get('children'); + if (children && children.length === 1) { + const childName = store.get(children[0]).get('name'); + return childName && !childName.match(/^[a-z]+$/); + } + } + return false; +} + +module.exports = shouldSkipToChildRendering;