Moved contextual nav items back to a sidebar.

Global nav items remain in piggy menu.

This is an adaptation of https://camlistore-review.googlesource.com/#/c/3898/ by Mario Russo <mail.mr@gmail.com>.

Change-Id: I85f7f386aa0573026253e13c5bd12b46ad08f83a
This commit is contained in:
Aaron Boodman 2014-11-01 11:18:28 -07:00
parent 9b6a9c587a
commit ac67a8d479
11 changed files with 408 additions and 213 deletions

View File

@ -22,7 +22,13 @@ limitations under the License.
border: 1px solid rgba(0,0,0,0);
position: relative;
white-space: nowrap;
.transition-transform(75ms ease-out);
}
.cam-blobitemcontainer-transform {
position: absolute;
left: 0;
top: 0;
.transition-transform(100ms ease-out);
}
.cam-blobitemcontainer.cam-dropactive {
@ -33,6 +39,6 @@ limitations under the License.
display: none;
}
.cam-blobitemcontainer>.cam-blobitem {
.cam-blobitemcontainer>.cam-blobitemcontainer-transform>.cam-blobitem {
position: absolute;
}

View File

@ -41,10 +41,14 @@ cam.BlobItemContainerReact = React.createClass({
INFINITE_SCROLL_THRESHOLD_PX_: 100,
propTypes: {
availHeight: React.PropTypes.number.isRequired,
availWidth: React.PropTypes.number.isRequired,
detailURL: React.PropTypes.func.isRequired, // string->string (blobref->complete detail URL)
handlers: React.PropTypes.array.isRequired,
history: React.PropTypes.shape({replaceState:React.PropTypes.func.isRequired}).isRequired,
onSelectionChange: React.PropTypes.func,
scale: React.PropTypes.number.isRequired,
scaleEnabled: React.PropTypes.bool.isRequired,
scrolling: React.PropTypes.shape({
target:React.PropTypes.shape({addEventListener:React.PropTypes.func.isRequired, removeEventListener:React.PropTypes.func.isRequired}),
get: React.PropTypes.func.isRequired,
@ -54,7 +58,6 @@ cam.BlobItemContainerReact = React.createClass({
selection: React.PropTypes.object.isRequired,
style: React.PropTypes.object,
thumbnailSize: React.PropTypes.number.isRequired,
translateY: React.PropTypes.number,
},
getDefaultProps: function() {
@ -129,29 +132,30 @@ cam.BlobItemContainerReact = React.createClass({
);
}, this);
childControls.push(React.DOM.div({
key: 'marker',
style: {
position: 'absolute',
top: this.layoutHeight_ - 1,
left: 0,
height: 1,
width: 1,
},
}));
// If we haven't filled the window with results, add some more.
this.fillVisibleAreaWithResults_();
var transformStyle = {};
var scale = this.props.scaleEnabled ? this.props.scale : 1;
transformStyle[cam.reactUtil.getVendorProp('transform')] = goog.string.subs('scale3d(%s, %s, 1)', scale, scale);
transformStyle[cam.reactUtil.getVendorProp('transformOrigin')] = goog.string.subs('left %spx 0', this.state.scroll);
return React.DOM.div(
{
className: 'cam-blobitemcontainer',
style: cam.object.extend(this.props.style, cam.reactUtil.getVendorProps({
transform: 'translateY(' + (this.props.translateY || 0) + 'px)',
})),
style: cam.object.extend(this.props.style, {
height: this.layoutHeight_,
width: this.props.availWidth,
}),
onMouseDown: this.handleMouseDown_,
},
childControls
React.DOM.div(
{
className: 'cam-blobitemcontainer-transform',
style: transformStyle,
},
childControls
)
);
},
@ -184,7 +188,7 @@ cam.BlobItemContainerReact = React.createClass({
for (var i = rowStart; i <= lastItem; i++) {
var item = items[i];
var availWidth = this.props.style.width;
var availWidth = this.props.availWidth;
var nextWidth = currentWidth + this.props.thumbnailSize * item.handler.getAspectRatio() + this.BLOB_ITEM_MARGIN_;
if (i != lastItem && nextWidth < availWidth) {
currentWidth = nextWidth;
@ -228,7 +232,11 @@ cam.BlobItemContainerReact = React.createClass({
var rowHeight = Number.POSITIVE_INFINITY;
var numItems = endIndex - startIndex + 1;
var availThumbWidth = availWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1));
// Doesn't seem like this should be necessary. Subpixel bug? Aaron can't math?
var fudge = 1;
var availThumbWidth = availWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1)) - fudge;
var usedThumbWidth = usedWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1));
for (var i = startIndex; i <= endIndex; i++) {
@ -257,8 +265,30 @@ cam.BlobItemContainerReact = React.createClass({
return rowHeight;
},
getScrollFraction_: function() {
var max = this.layoutHeight_;
if (max == 0)
return 0;
return this.state.scroll / max;
},
getTranslation_: function() {
var maxOffset = (1 - this.props.scale) * this.layoutHeight_;
var currentOffset = maxOffset * this.getScrollFraction_();
return currentOffset;
},
transformY_: function(y) {
return y * this.props.scale + this.getTranslation_();
},
getScrollBottom_: function() {
return this.state.scroll + this.props.availHeight;
},
isVisible_: function(y) {
return y >= this.state.scroll && y < (this.state.scroll + this.props.style.height);
y = this.transformY_(y);
return y >= this.state.scroll && y < this.getScrollBottom_();
},
handleSearchSessionChanged_: function() {
@ -299,9 +329,10 @@ cam.BlobItemContainerReact = React.createClass({
},
handleScroll_: function() {
this.updateHistoryThrottle_.fire();
this.setState({scroll:this.props.scrolling.get()});
this.fillVisibleAreaWithResults_();
this.setState({scroll:this.props.scrolling.get()}, function() {
this.updateHistoryThrottle_.fire();
this.fillVisibleAreaWithResults_();
}.bind(this));
},
handleChildWheel_: function(child) {
@ -310,7 +341,7 @@ cam.BlobItemContainerReact = React.createClass({
// NOTE: This method causes the URL bar to throb for a split second (at least on Chrome), so it should not be called constantly.
updateHistory_: function() {
this.props.history.replaceState({scroll:this.props.scrolling.get()});
this.props.history.replaceState({scroll:this.state.scroll});
},
fillVisibleAreaWithResults_: function() {
@ -318,7 +349,8 @@ cam.BlobItemContainerReact = React.createClass({
return;
}
if ((this.layoutHeight_ - this.state.scroll - this.props.style.height) > this.INFINITE_SCROLL_THRESHOLD_PX_) {
var layoutEnd = this.transformY_(this.layoutHeight_);
if ((layoutEnd - this.getScrollBottom_()) > this.INFINITE_SCROLL_THRESHOLD_PX_) {
return;
}

View File

@ -48,19 +48,6 @@ limitations under the License.
box-shadow: none;
}
.cam-header-sub {
background: #eee;
height: 36px;
left: 0;
position: absolute;
text-align: center;
top: 38px;
width: 100%;
.translate3d(0, -100%, 0);
.transform(75ms ease-out);
z-index: 1;
}
.cam-header.cam-header-sub-active .cam-header-sub {
box-shadow: 0.1em 0 0.5em 0.1em rgba(0, 0, 0, 0.4);
.transform(translate3d(0, 0, 0));
@ -178,24 +165,3 @@ limitations under the License.
height: 38px;
border-bottom: 3px solid rgb(232,139,131);
}
.cam-header-sub button {
background: transparent;
border: none;
color: #444;
font-family: 'Open Sans', sans-serif;
font-weight: 600;
font-size: 12px;
height: 36px;
padding-left: 2em;
padding-right: 2em;
white-space: nowrap;
}
.cam-header-sub button:hover {
background: #ddd;
}
.cam-header-sub button:active {
outline: none;
}

View File

@ -55,7 +55,6 @@ cam.Header = React.createClass({
onSearch: React.PropTypes.func,
searchRootsURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
statusURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
subControls: React.PropTypes.arrayOf(React.PropTypes.renderable),
timer: React.PropTypes.shape({setTimeout:React.PropTypes.func.isRequired, clearTimeout:React.PropTypes.func.isRequired}).isRequired,
width: React.PropTypes.number.isRequired,
},
@ -81,10 +80,7 @@ cam.Header = React.createClass({
render: function() {
return React.DOM.div(
{
className: React.addons.classSet({
'cam-header': true,
'cam-header-sub-active': this.props.subControls.length,
}),
className: 'cam-header',
style: {
width: this.props.width,
},
@ -100,7 +96,6 @@ cam.Header = React.createClass({
this.getMainControls_()
)
),
this.getSubheader_(),
this.getMenuDropdown_()
)
},
@ -254,15 +249,6 @@ cam.Header = React.createClass({
);
},
getSubheader_: function() {
return React.DOM.div(
{
className: 'cam-header-sub',
},
this.props.subControls
);
},
getMenuTranslate_: function() {
if (this.state.menuVisible) {
return 0;

View File

@ -46,4 +46,4 @@ body {
.cam-index-upload-dialog>* {
vertical-align: middle;
}
}

View File

@ -40,7 +40,6 @@ limitations under the License.
<link rel="stylesheet" href="fontawesome/css/font-awesome.min.css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800">
<link rel="stylesheet" href="closure/goog/css/common.css" type="text/css">
<link rel="stylesheet/less" href="tags_control.css" type="text/css">
<link rel="stylesheet/less" href="index.css" type="text/css">
<link rel="stylesheet/less" href="header.css" type="text/css">
@ -55,7 +54,8 @@ limitations under the License.
<link rel="stylesheet/less" href="permanode_detail.css" type="text/css">
<link rel="stylesheet/less" href="property_sheet.css" type="text/css">
<link rel="stylesheet/less" href="pyramid_throbber.css" type="text/css">
<link rel="stylesheet/less" href="sidebar.css" type="text/css">
<link rel="stylesheet/less" href="tags_control.css" type="text/css">
<script src="less/less.js"></script>
</head>
<body class="cam-index-page">
@ -71,6 +71,7 @@ limitations under the License.
lastWidth = currentWidth;
lastHeight = currentHeight;
var index = cam.IndexPage({
availWidth: currentWidth,
availHeight: currentHeight,
@ -80,6 +81,7 @@ limitations under the License.
location: window.location,
scrolling: {
target: window,
// Note that calling get() can cause layout, so should be used only once per scroll event.
get: function() { return document.body.scrollTop; },
set: function(val) { document.body.scrollTop = val; },
},

View File

@ -20,6 +20,7 @@ goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventHandler');
goog.require('goog.functions');
goog.require('goog.labs.Promise');
goog.require('goog.object');
goog.require('goog.string');
@ -44,11 +45,14 @@ goog.require('cam.permanodeUtils');
goog.require('cam.reactUtil');
goog.require('cam.SearchSession');
goog.require('cam.ServerConnection');
goog.require('cam.Sidebar');
goog.require('cam.TagsControl');
cam.IndexPage = React.createClass({
displayName: 'IndexPage',
SIDEBAR_OPEN_WIDTH_: 250,
HEADER_HEIGHT_: 38,
SEARCH_PREFIX_: {
RAW: 'raw'
@ -83,7 +87,6 @@ cam.IndexPage = React.createClass({
componentWillMount: function() {
this.baseURL_ = null;
this.currentSet_ = null;
this.dragEndTimer_ = 0;
this.navigator_ = null;
this.searchSessionCache_ = [];
@ -117,12 +120,15 @@ cam.IndexPage = React.createClass({
getInitialState: function() {
return {
currentURL: null,
currentSet: '',
dropActive: false,
selection: {},
serverStatus: null,
tagsControlVisible: false,
// TODO: This should be calculated by whether selection is empty, and not need separate state.
sidebarVisible: false,
uploadDialogVisible: false,
dropActive: false,
numUploadsTotal: 0,
numUploadsComplete: 0,
};
@ -142,7 +148,6 @@ cam.IndexPage = React.createClass({
var contentSize = new goog.math.Size(this.props.availWidth, this.props.availHeight - this.HEADER_HEIGHT_);
return React.DOM.div({onDragEnter:this.handleDragStart_, onDragOver:this.handleDragStart_, onDrop:this.handleDrop_}, [
this.getHeader_(aspects, selectedAspect),
this.getTagsControl_(),
React.DOM.div(
{
className: 'cam-content-wrap',
@ -152,17 +157,14 @@ cam.IndexPage = React.createClass({
},
aspects[selectedAspect] && aspects[selectedAspect].createContent(contentSize, backwardPiggy)
),
this.getUploadDialog_(),
this.getSidebar_(aspects[selectedAspect]),
this.getUploadDialog_()
]);
},
setSelection_: function(selection) {
this.setState({selection: selection});
// leave contextual controls open if items are still selected
if (goog.object.isEmpty(selection)) {
this.setState({tagsControlVisible: false});
}
this.setState({sidebarVisible: !goog.object.isEmpty(selection)});
},
getTargetBlobref_: function(opt_url) {
@ -428,14 +430,6 @@ cam.IndexPage = React.createClass({
searchRootsURL: this.getSearchRootsURL_(),
statusURL: this.baseURL_.resolve(new goog.Uri(this.props.config.statusRoot)),
ref: 'header',
subControls: [
this.getClearSelectionItem_(),
this.getCreateSetWithSelectionItem_(),
this.getSelectAsCurrentSetItem_(),
this.getAddToCurrentSetItem_(),
this.getDeleteSelectionItem_(),
this.getTagsControlItem_()
].filter(function(c) { return c }),
timer: this.props.timer,
width: this.props.availWidth,
}
@ -461,12 +455,17 @@ cam.IndexPage = React.createClass({
},
handleSelectAsCurrentSet_: function() {
this.currentSet_ = goog.object.getAnyKey(this.state.selection);
this.setState({
currentSet: goog.object.getAnyKey(this.state.selection),
});
this.setSelection_({});
alert('Now, select the items to add to this set and click "Add to picked set" in the sidebar.\n\n' +
'Sorry this is lame, we\'re working on it.');
},
handleAddToSet_: function() {
this.addMembersToSet_(this.currentSet_, goog.object.getKeys(this.state.selection));
this.addMembersToSet_(this.state.currentSet, goog.object.getKeys(this.state.selection));
alert('Done!');
},
handleUpload_: function() {
@ -581,78 +580,93 @@ cam.IndexPage = React.createClass({
return null;
}
return React.DOM.button({key:'selectascurrent', onClick:this.handleSelectAsCurrentSet_}, 'Select as current set');
return React.DOM.button(
{
key:'selectascurrent',
onClick:this.handleSelectAsCurrentSet_
},
'Add items to set'
);
},
getAddToCurrentSetItem_: function() {
if (!this.currentSet_ || !goog.object.getAnyKey(this.state.selection)) {
if (!this.state.currentSet) {
return null;
}
return React.DOM.button({key:'addtoset', onClick:this.handleAddToSet_}, 'Add to current set');
return React.DOM.button(
{
key:'addtoset',
onClick:this.handleAddToSet_
},
'Add to picked set'
);
},
getCreateSetWithSelectionItem_: function() {
var numItems = goog.object.getCount(this.state.selection);
if (numItems == 0) {
return null;
}
var label = 'Create set';
if (numItems == 1) {
label += ' with item';
} else if (numItems > 1) {
label += goog.string.subs(' with %s items', numItems);
}
return React.DOM.button({key:'createsetwithselection', onClick:this.handleCreateSetWithSelection_}, label);
return React.DOM.button(
{
key:'createsetwithselection',
onClick:this.handleCreateSetWithSelection_
},
'Create set with items'
);
},
getClearSelectionItem_: function() {
if (!goog.object.getAnyKey(this.state.selection)) {
return null;
}
return React.DOM.button({key:'clearselection', onClick:this.handleClearSelection_}, 'Clear selection');
return React.DOM.button(
{
key:'clearselection',
onClick:this.handleClearSelection_
},
'Clear selection'
);
},
getDeleteSelectionItem_: function() {
if (!goog.object.getAnyKey(this.state.selection)) {
return React.DOM.button(
{
key:'deleteselection',
onClick:this.handleDeleteSelection_
},
'Delete items'
);
},
getSidebar_: function(selectedAspect) {
// We don't support the sidebar in other aspects (maybe we should though).
if (!selectedAspect || selectedAspect.fragment != 'search')
return null;
}
var numItems = goog.object.getCount(this.state.selection);
var label = 'Delete';
if (numItems == 1) {
label += ' selected item';
} else if (numItems > 1) {
label += goog.string.subs(' (%s) selected items', numItems);
}
// TODO(mpl): better icon in another CL, with Font Awesome.
return React.DOM.button({key:'deleteselection', onClick:this.handleDeleteSelection_}, label);
return cam.Sidebar({
isExpanded: this.state.sidebarVisible,
mainControls: [
{
"displayTitle": "Update Tags",
"control": this.getTagsControl_()
}
].filter(goog.functions.identity),
selectionControls: [
this.getClearSelectionItem_(),
this.getCreateSetWithSelectionItem_(),
this.getSelectAsCurrentSetItem_(),
this.getAddToCurrentSetItem_(),
this.getDeleteSelectionItem_(),
].filter(goog.functions.identity),
selectedItems: this.state.selection
});
},
getTagsControl_: function() {
if (!this.state.tagsControlVisible) {
return null;
}
return cam.TagsControl(
{
selectedItems: this.state.selection,
searchSession: this.childSearchSession_,
serverConnection: this.props.serverConnection,
onCloseControl: this.handleTagsControlClose_
serverConnection: this.props.serverConnection
}
);
},
getTagsControlItem_: function() {
var numItems = goog.object.getCount(this.state.selection);
if (numItems == 0) {
return null;
}
var label = goog.string.subs('Tag (%s) items', numItems);
return React.DOM.button({key:'tagselection', onClick:this.handleTagSelection_}, label);
},
isUploading_: function() {
return this.state.numUploadsTotal > 0;
},
@ -739,32 +753,31 @@ cam.IndexPage = React.createClass({
});
},
handleTagSelection_: function() {
this.setState({tagsControlVisible: !this.state.tagsControlVisible});
},
handleTagsControlClose_: function() {
this.setState({tagsControlVisible: false});
},
handleSelectionChange_: function(newSelection) {
this.setSelection_(newSelection);
},
getBlobItemContainer_: function() {
var sidebarClosedWidth = this.props.availWidth;
var sidebarOpenWidth = sidebarClosedWidth - this.SIDEBAR_OPEN_WIDTH_;
var scale = sidebarOpenWidth / sidebarClosedWidth;
return cam.BlobItemContainerReact({
key: 'blobitemcontainer',
ref: 'blobItemContainer',
availHeight: this.props.availHeight,
availWidth: this.props.availWidth,
detailURL: this.handleDetailURL_,
handlers: this.BLOB_ITEM_HANDLERS_,
history: this.props.history,
onSelectionChange: this.handleSelectionChange_,
scale: scale,
scaleEnabled: this.state.sidebarVisible,
scrolling: this.props.scrolling,
searchSession: this.childSearchSession_,
selection: this.state.selection,
style: this.getBlobItemContainerStyle_(),
thumbnailSize: this.THUMBNAIL_SIZE_,
translateY: goog.object.getAnyKey(this.state.selection) ? 36 : 0,
});
},
@ -774,8 +787,6 @@ cam.IndexPage = React.createClass({
overflowY: this.state.dropActive ? 'hidden' : '',
position: 'absolute',
top: 0,
height: this.props.availHeight - this.HEADER_HEIGHT_,
width: this.getContentWidth_(),
};
},

View File

@ -0,0 +1,85 @@
/*
Copyright 2014 The Camlistore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
@import (less) "prefix-free.css";
/* TODO: can the positioning (top: 38px) be pulled from the header.css? */
.cam-sidebar {
width: 250px;
height: 100%;
position: fixed;
top: 38px;
right: 0;
background-color: #e6e6e6;
color: #444;
.transform(translate3d(0, 0, 0));
.transition-transform(100ms ease-out);
&.cam-sidebar-hidden {
.transform(translate3d(100%, 0, 0));
}
}
.cam-sidebar {
padding: 5px 0px;
}
.cam-sidebar, .cam-sidebar-collapsible-section-header {
> button {
width: 100%;
height: 38px;
cursor: pointer;
background: transparent;
border: none;
font-family: 'Open Sans', sans-serif;
font-weight: 100;
font-size: 14px;
padding: 0 28px;
position: relative;
text-align: left;
white-space: nowrap;
> i {
color: #666;
cursor: pointer;
display: block;
left: 2px;
line-height: 38px;
position: absolute;
text-align: center;
top: 0;
width: 26px;
}
&:focus {
outline: none;
}
&:hover {
background: #d6d6d6;
}
&:active {
outline: none;
}
}
}
.cam-sidebar-section {
padding: 0 28px;
}

View File

@ -0,0 +1,144 @@
/*
Copyright 2014 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
goog.provide('cam.Sidebar');
goog.require('goog.array');
goog.require('goog.object');
goog.require('goog.string');
goog.require('cam.ServerConnection');
cam.Sidebar = React.createClass({
displayName: 'Sidebar',
propTypes: {
isExpanded: React.PropTypes.bool.isRequired,
mainControls: React.PropTypes.arrayOf(
React.PropTypes.shape(
{
displayTitle: React.PropTypes.string.isRequired,
control: React.PropTypes.renderable.isRequired,
}
)
),
selectionControls: React.PropTypes.arrayOf(React.PropTypes.renderable).isRequired,
selectedItems: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
openControls: [], // all controls that are currently 'open'
};
},
render: function() {
return React.DOM.div(
{
className: React.addons.classSet({
'cam-sidebar': true,
'cam-sidebar-hidden': !this.props.isExpanded,
})
},
this.props.selectionControls,
this.getMainControls_()
);
},
getMainControls_: function() {
return this.props.mainControls.map(
function(c) {
return cam.CollapsibleControl(
{
control: c.control,
isOpen: this.isControlOpen_(c.displayTitle),
onToggleOpen: this.handleToggleControlOpen_,
title: c.displayTitle
});
}.bind(this)
);
},
handleToggleControlOpen_: function(displayTitle) {
var currentlyOpen = this.state.openControls;
if(!this.isControlOpen_(displayTitle)) {
currentlyOpen.push(displayTitle);
} else {
goog.array.remove(currentlyOpen, displayTitle);
}
this.setState({openControls : currentlyOpen});
},
isControlOpen_: function(displayTitle) {
return goog.array.contains(this.state.openControls, displayTitle);
}
});
cam.CollapsibleControl = React.createClass({
displayName: 'CollapsibleControl',
propTypes: {
control: React.PropTypes.renderable.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onToggleOpen: React.PropTypes.func,
title: React.PropTypes.string.isRequired
},
getControl_: function() {
if(!this.props.control || !this.props.isOpen) {
return null;
}
return React.DOM.div(
{
className: 'cam-sidebar-section'
},
this.props.control
);
},
render: function() {
return React.DOM.div(
{
className: 'cam-sidebar-collapsible-section-header'
},
React.DOM.button(
{
onClick: this.handleToggleOpenClick_,
},
React.DOM.i(
{
className: React.addons.classSet({
'fa': true,
'fa-angle-down': this.props.isOpen,
'fa-angle-right': !this.props.isOpen
}),
key: 'toggle-sidebar-section'
}
),
this.props.title
),
this.getControl_()
);
},
handleToggleOpenClick_: function(e) {
e.preventDefault();
this.props.onToggleOpen(this.props.title);
}
});

View File

@ -16,48 +16,22 @@ limitations under the License.
@import (less) "prefix-free.css";
@color-background: #3a3a3a;
@color-green: #44c767;
@color-darker-green: #18ab29;
@color-brighter-green: #5cbf2a;
@color-button-border: #39463C;
@color-button-partial: #eee;
@color-button-full: #81A18A;
@color-button-hover: #576D5D;
@control-width: 300px;
.cam-tagscontrol-main {
width: @control-width;
margin-left: -1 * (@control-width / 2);
padding: 7px;
position: fixed;
top: 170px;
left: 50%;
z-index: 3; /* this should render above anything in the blob container */
background: @color-background;
box-shadow: 1px 2px 4px 2px black;
}
.cam-tagscontrol-header {
color: #e4e4e4;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
margin-bottom: 5px;
text-align: center;
i {
float: right;
cursor: pointer;
}
}
.cam-addtagsinput-form {
margin-bottom: 5px;
margin: 5px 0;
input {
> input {
width: 100%;
padding: 3px;
padding: 0;
}
div {
> div {
margin: 3px;
color: red;
font-size: 12px;
@ -74,20 +48,20 @@ limitations under the License.
margin: 3px;
display: inline-block;
button {
color: #ffffff;
> button {
font-size: 13px;
border: 1px solid @color-darker-green;
border: 1px solid @color-button-border;
}
button:active:enabled {
> button:active:enabled {
position: relative;
top: 1px;
}
}
.cam-edittagscontrol-button-all-tagged {
background-color: @color-green;
background-color: @color-button-full;
color: #fff;
padding: 2px 7px 2px 10px;
margin-right: 0px;
margin-left: 0px;
@ -101,7 +75,7 @@ limitations under the License.
}
.cam-edittagscontrol-button-some-tagged {
background-color: @color-background;
background-color: @color-button-partial;
padding: 2px 10px;
margin-right: 0px;
@ -115,12 +89,14 @@ limitations under the License.
border-bottom-right-radius: 0px;
&:hover:enabled {
background-color: @color-green;
background-color: @color-button-full;
color: #ffffff;
}
}
.cam-edittagscontrol-button-remove-tag {
background-color: @color-green;
background-color: @color-button-full;
color: #fff;
margin-left: -1px;
margin-right: 0px;
padding: 2px 10px 2px 7px;
@ -133,6 +109,6 @@ limitations under the License.
border-bottom-right-radius: 10px;
&:hover:enabled {
background-color: @color-brighter-green;
background-color: @color-button-hover;
}
}

View File

@ -37,11 +37,6 @@ cam.TagsControl = React.createClass({
selectedItems: React.PropTypes.object.isRequired,
searchSession: React.PropTypes.shape({getMeta:React.PropTypes.func.isRequired}),
serverConnection: React.PropTypes.instanceOf(cam.ServerConnection).isRequired,
onCloseControl: React.PropTypes.func.isRequired
},
handleCloseControl_: function(e) {
this.props.onCloseControl();
},
doesBlobHaveTag: function(blobref, tag) {
@ -92,15 +87,7 @@ cam.TagsControl = React.createClass({
React.DOM.div(
{
className: 'cam-tagscontrol-header'
},
React.DOM.span({}, 'Update tags for ' + blobrefs.length + ' item(s)'),
React.DOM.i(
{
className: 'fa fa-times-circle fa-lg',
key: 'close-tag-control',
onClick: this.handleCloseControl_,
}
)
}
),
cam.AddTagsInput(
{