mirror of https://github.com/perkeep/perkeep.git
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:
parent
9b6a9c587a
commit
ac67a8d479
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -46,4 +46,4 @@ body {
|
|||
|
||||
.cam-index-upload-dialog>* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; },
|
||||
},
|
||||
|
|
|
@ -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_(),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue