diff --git a/server/camlistored/ui/blob_item_container.css b/server/camlistored/ui/blob_item_container.css index 7f59dca08..752b1db46 100644 --- a/server/camlistored/ui/blob_item_container.css +++ b/server/camlistored/ui/blob_item_container.css @@ -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; } diff --git a/server/camlistored/ui/blob_item_container_react.js b/server/camlistored/ui/blob_item_container_react.js index b3a574126..eed7b34f1 100644 --- a/server/camlistored/ui/blob_item_container_react.js +++ b/server/camlistored/ui/blob_item_container_react.js @@ -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; } diff --git a/server/camlistored/ui/header.css b/server/camlistored/ui/header.css index 79898fb0e..26e2c8344 100644 --- a/server/camlistored/ui/header.css +++ b/server/camlistored/ui/header.css @@ -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; -} diff --git a/server/camlistored/ui/header.js b/server/camlistored/ui/header.js index 4d04d59c0..bf1ae4c43 100644 --- a/server/camlistored/ui/header.js +++ b/server/camlistored/ui/header.js @@ -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; diff --git a/server/camlistored/ui/index.css b/server/camlistored/ui/index.css index bda9235e3..b5ec048f2 100644 --- a/server/camlistored/ui/index.css +++ b/server/camlistored/ui/index.css @@ -46,4 +46,4 @@ body { .cam-index-upload-dialog>* { vertical-align: middle; -} \ No newline at end of file +} diff --git a/server/camlistored/ui/index.html b/server/camlistored/ui/index.html index 6fcdd4673..17e47b00e 100644 --- a/server/camlistored/ui/index.html +++ b/server/camlistored/ui/index.html @@ -40,7 +40,6 @@ limitations under the License. - @@ -55,7 +54,8 @@ limitations under the License. - + + @@ -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; }, }, diff --git a/server/camlistored/ui/index.js b/server/camlistored/ui/index.js index d8e9a962a..7a5e3e024 100644 --- a/server/camlistored/ui/index.js +++ b/server/camlistored/ui/index.js @@ -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_(), }; }, diff --git a/server/camlistored/ui/sidebar.css b/server/camlistored/ui/sidebar.css new file mode 100644 index 000000000..e187aaf6d --- /dev/null +++ b/server/camlistored/ui/sidebar.css @@ -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; +} diff --git a/server/camlistored/ui/sidebar.js b/server/camlistored/ui/sidebar.js new file mode 100644 index 000000000..9b2a3b965 --- /dev/null +++ b/server/camlistored/ui/sidebar.js @@ -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); + } +}); diff --git a/server/camlistored/ui/tags_control.css b/server/camlistored/ui/tags_control.css index 6eed4f660..9f28aca5e 100644 --- a/server/camlistored/ui/tags_control.css +++ b/server/camlistored/ui/tags_control.css @@ -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; } } diff --git a/server/camlistored/ui/tags_control.js b/server/camlistored/ui/tags_control.js index 95b411517..e3321c1a9 100644 --- a/server/camlistored/ui/tags_control.js +++ b/server/camlistored/ui/tags_control.js @@ -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( {