diff --git a/client/modules/accounts/components/auth/index.js b/client/modules/accounts/components/auth/index.js new file mode 100644 index 00000000000..526fc99f7cf --- /dev/null +++ b/client/modules/accounts/components/auth/index.js @@ -0,0 +1,3 @@ +export LoginButtons from "./loginButtons"; +export SignIn from "./signIn"; +export SignUp from "./signUp"; diff --git a/client/modules/accounts/components/auth/loginButtons.js b/client/modules/accounts/components/auth/loginButtons.js new file mode 100644 index 00000000000..1b7b958d682 --- /dev/null +++ b/client/modules/accounts/components/auth/loginButtons.js @@ -0,0 +1,76 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Button, Divider, Translation } from "/imports/plugins/core/ui/client/components"; + +class LoginButtons extends Component { + static propTypes = { + capitalizeName: PropTypes.func, + currentView: PropTypes.string, + loginServices: PropTypes.func, + onSeparator: PropTypes.func, + onSocialClick: PropTypes.func + } + + renderLoginButtons() { + const enabledServices = this.props.loginServices().filter((service) =>{ + return service.enabled; + }); + + return ( +
+ {this.props.loginServices && + enabledServices.map((service) => ( + + )) + } +
+ ); + } + + renderSeparator() { + if (this.props.onSeparator()) { + return ( +
+ +
+ ); + } + } + + render() { + return ( +
+ {this.renderLoginButtons()} + {this.renderSeparator()} +
+ ); + } +} + +export default LoginButtons; diff --git a/client/modules/accounts/components/auth/signIn.js b/client/modules/accounts/components/auth/signIn.js new file mode 100644 index 00000000000..212fae21aaf --- /dev/null +++ b/client/modules/accounts/components/auth/signIn.js @@ -0,0 +1,182 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classnames from "classnames"; +import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components"; + +class SignIn extends Component { + static propTypes = { + credentials: PropTypes.object, + isLoading: PropTypes.bool, + loginFormMessages: PropTypes.func, + messages: PropTypes.object, + onError: PropTypes.func, + onForgotPasswordClick: PropTypes.func, + onFormSubmit: PropTypes.func, + onSignUpClick: PropTypes.func, + uniqueId: PropTypes.string + } + + constructor(props) { + super(props); + + this.state = { + email: props.credentials.email, + password: props.credentials.password + }; + + this.handleFieldChange = this.handleFieldChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleFieldChange = (event, value, field) => { + this.setState({ + [field]: value + }); + } + + handleSubmit = (event) => { + if (this.props.onFormSubmit) { + this.props.onFormSubmit(event, this.state.email, this.state.password); + } + } + + renderEmailErrors() { + if (this.props.onError(this.props.messages.errors && this.props.messages.errors.email)) { + return ( + + + + ); + } + } + + renderPasswordErrors() { + return ( + + {this.props.onError(this.props.messages.errors && this.props.messages.errors.password) && + this.props.messages.errors.password.map((error, i) => ( + + )) + } + + ); + } + + renderFormMessages() { + if (this.props.loginFormMessages) { + return ( +
+ {this.props.loginFormMessages()} +
+ ); + } + } + + renderSpinnerOnWait() { + if (this.props.isLoading === true) { + return ( +
+ +
+ ); + } + return ( + + ); + } + + renderAdminIcons() { + return ( + Reaction.Apps(this.props.adminShortcuts).map((shortcut) => ( + + )) + ); + } + + renderUserIcons() { + return ( + Reaction.Apps(this.props.userShortcuts).map((option) => ( + + )) + ); + } + + renderSignOutButton() { + return ( + + ); + } + + renderSignInComponent() { + return ( +
+
+ +
+
+ +
+
+ ); + } + + render() { + return ( +
+ {this.props.currentUser ? +
+ + + {this.renderUserIcons()} + {this.renderAdminIcons()} + {this.renderSignOutButton()} + + +
+ : +
+ {this.renderSignInComponent()} +
+ } +
+ ); + } +} + +export default MainDropdown; diff --git a/client/modules/accounts/components/helpers/index.js b/client/modules/accounts/components/helpers/index.js new file mode 100644 index 00000000000..be7e8d25998 --- /dev/null +++ b/client/modules/accounts/components/helpers/index.js @@ -0,0 +1 @@ +export LoginFormMessages from "./loginFormMessages"; diff --git a/client/modules/accounts/components/helpers/loginFormMessages.js b/client/modules/accounts/components/helpers/loginFormMessages.js new file mode 100644 index 00000000000..a07b8ccc0d3 --- /dev/null +++ b/client/modules/accounts/components/helpers/loginFormMessages.js @@ -0,0 +1,41 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +class LoginFormMessages extends Component { + static propTypes = { + formMessages: PropTypes.object, + loginFormMessages: PropTypes.func + } + + renderFormMessages() { + if (this.props.loginFormMessages) { + if (this.props.formMessages.info) { + return ( +
+

+ {this.props.loginFormMessages()} +

+
+ ); + } else if (this.props.formMessages.alerts) { + return ( +
+

+ {this.props.loginFormMessages()} +

+
+ ); + } + } + } + + render() { + return ( +
+ {this.renderFormMessages()} +
+ ); + } +} + +export default LoginFormMessages; diff --git a/client/modules/accounts/components/index.js b/client/modules/accounts/components/index.js new file mode 100644 index 00000000000..2d0c5401ca8 --- /dev/null +++ b/client/modules/accounts/components/index.js @@ -0,0 +1,3 @@ +export { SignIn, SignUp, LoginButtons } from "./auth"; +export { Forgot, UpdatePasswordOverlay } from "./passwordReset"; +export { LoginFormMessages } from "./helpers"; diff --git a/client/modules/accounts/components/passwordReset/forgot.js b/client/modules/accounts/components/passwordReset/forgot.js new file mode 100644 index 00000000000..187aa7321a2 --- /dev/null +++ b/client/modules/accounts/components/passwordReset/forgot.js @@ -0,0 +1,142 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import classnames from "classnames"; +import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components"; + +class Forgot extends Component { + static propTypes = { + credentials: PropTypes.object, + isDisabled: PropTypes.bool, + isLoading: PropTypes.bool, + loginFormMessages: PropTypes.func, + messages: PropTypes.object, + onError: PropTypes.func, + onFormSubmit: PropTypes.func, + onSignInClick: PropTypes.func, + uniqueId: PropTypes.string + } + + constructor(props) { + super(props); + + this.state = { + email: props.credentials.email + }; + + this.handleFieldChange = this.handleFieldChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleFieldChange = (event, value, field) => { + this.setState({ + [field]: value + }); + } + + handleSubmit = (event) => { + if (this.props.onFormSubmit) { + this.props.onFormSubmit(event, this.state.email); + } + } + + renderFormMessages() { + if (this.props.loginFormMessages) { + return ( +
+ {this.props.loginFormMessages()} +
+ ); + } + } + + renderEmailErrors() { + if (this.props.onError(this.props.messages.errors && this.props.messages.errors.email)) { + return ( + + + + ); + } + } + + renderSpinnerOnWait() { + if (this.props.isLoading === true) { + return ( +
+ +
+ ); + } + return ( + + ); + } + + render() { + return ( +
+ {this.props.currencies.length > 1 && + + + + + + {this.props.currencies.map((currency) => ( + + ))} + + } +
+ ); + } +} + +export default Currency; diff --git a/client/modules/i18n/templates/currency/containers/currencyContainer.js b/client/modules/i18n/templates/currency/containers/currencyContainer.js new file mode 100644 index 00000000000..05d822be261 --- /dev/null +++ b/client/modules/i18n/templates/currency/containers/currencyContainer.js @@ -0,0 +1,96 @@ +import React, { Component } from "react"; +import { Meteor } from "meteor/meteor"; +import { Match } from "meteor/check"; +import { Reaction } from "/client/api"; +import { composeWithTracker } from "/lib/api/compose"; +import { Cart, Shops } from "/lib/collections"; +import Currency from "../components/currency"; + +class CurrencyContainer extends Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange = (value) => { + const currency = value.split(" "); + const currencyName = currency[0]; + // + // this is a sanctioned use of Meteor.user.update + // and only possible because we allow it in the + // UserProfile and ShopMembers publications. + // + Meteor.users.update(Meteor.userId(), { $set: { "profile.currency": currencyName } }); + localStorage.setItem("currency", currencyName); + + const cart = Cart.findOne({ userId: Meteor.userId() }); + + // Attach changed currency to this users cart + Meteor.call("cart/setUserCurrency", cart._id, currencyName); + } + + render() { + return ( +
+ +
+ ); + } +} + +const composer = (props, onData) => { + let currentCurrency = "USD $"; + const currencies = []; + + if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) { + const shop = Shops.findOne(Reaction.getShopId(), { + fields: { + currencies: 1, + currency: 1 + } + }); + if (Match.test(shop, Object) && shop.currency) { + const localStorageCurrency = localStorage.getItem("currency"); + const locale = Reaction.Locale.get(); + + if (localStorageCurrency) { + currentCurrency = localStorageCurrency + " " + shop.currencies[localStorageCurrency].symbol; + } else if (locale && locale.currency && locale.currency.enabled) { + currentCurrency = locale.locale.currency + " " + locale.currency.symbol; + } else { + currentCurrency = shop.currency + " " + shop.currencies[shop.currency].symbol; + } + } + + if (Match.test(shop, Object) && shop.currencies) { + for (const currencyName in shop.currencies) { + if (shop.currencies[currencyName].enabled === true) { + const currency = { currency: currencyName }; + const localStorageCurrency = localStorage.getItem("currency"); + // only one currency will be "active". Either the one + // matching the localStorageCurrency if exists or else + // the one matching shop currency + if (localStorageCurrency) { + if (localStorageCurrency === currency.currency) { + currency.class = "active"; + } + } else if (shop.currency === currency.currency) { + currency.class = "active"; + } + currency.symbol = shop.currencies[currencyName].symbol; + currencies.push(currency); + } + } + } + } + + onData(null, { + currentCurrency: currentCurrency, + currencies: currencies + }); +}; + +export default composeWithTracker(composer)(CurrencyContainer); diff --git a/client/modules/i18n/templates/currency/currency.html b/client/modules/i18n/templates/currency/currency.html deleted file mode 100644 index 770ca3d4e48..00000000000 --- a/client/modules/i18n/templates/currency/currency.html +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/client/modules/i18n/templates/currency/currency.js b/client/modules/i18n/templates/currency/currency.js deleted file mode 100644 index 7aa0b39b26f..00000000000 --- a/client/modules/i18n/templates/currency/currency.js +++ /dev/null @@ -1,85 +0,0 @@ -import { Meteor } from "meteor/meteor"; -import { Reaction } from "/client/api"; -import { Cart, Shops } from "/lib/collections"; - -Template.currencySelect.helpers({ - currencies() { - const currencies = []; - if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) { - const shop = Shops.findOne(Reaction.getShopId(), { - fields: { - currencies: 1, - currency: 1 - } - }); - Reaction.Locale.get(); - if (Match.test(shop, Object) && shop.currencies) { - for (const currencyName in shop.currencies) { - if (shop.currencies[currencyName].enabled === true) { - const currency = { currency: currencyName }; - const localStorageCurrency = localStorage.getItem("currency"); - // only one currency will be "active". Either the one - // matching the localStorageCurrency if exists or else - // the one matching shop currency - if (localStorageCurrency) { - if (localStorageCurrency === currency.currency) { - currency.class = "active"; - } - } else if (shop.currency === currency.currency) { - currency.class = "active"; - } - currency.symbol = shop.currencies[currencyName].symbol; - currencies.push(currency); - } - } - if (currencies.length > 1) { - return currencies; - } - } - } - if (currencies.length > 1) { - return currencies; - } - }, - - currentCurrency() { - if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) { - const shop = Shops.findOne(Reaction.getShopId(), { - fields: { - currencies: 1, - currency: 1 - } - }); - if (Match.test(shop, Object) && shop.currency) { - const localStorageCurrency = localStorage.getItem("currency"); - if (localStorageCurrency) { - return localStorageCurrency + " " + shop.currencies[localStorageCurrency].symbol; - } - const locale = Reaction.Locale.get(); - if (locale && locale.currency && locale.currency.enabled) { - return locale.locale.currency + " " + locale.currency.symbol; - } - return shop.currency + " " + shop.currencies[shop.currency].symbol; - } - } - return "USD $"; - } -}); - -Template.currencySelect.events({ - "click .currency"(event) { - event.preventDefault(); - // - // this is a sanctioned use of Meteor.user.update - // and only possible because we allow it in the - // UserProfile and ShopMembers publications. - // - Meteor.users.update(Meteor.userId(), { $set: { "profile.currency": this.currency } }); - localStorage.setItem("currency", this.currency); - - const cart = Cart.findOne({ userId: Meteor.userId() }); - - // Attach changed currency to this users cart - Meteor.call("cart/setUserCurrency", cart._id, this.currency); - } -}); diff --git a/client/modules/i18n/templates/header/components/i18n.js b/client/modules/i18n/templates/header/components/i18n.js new file mode 100644 index 00000000000..fa38ce62a1d --- /dev/null +++ b/client/modules/i18n/templates/header/components/i18n.js @@ -0,0 +1,65 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Button, Divider, DropDownMenu, MenuItem } from "/imports/plugins/core/ui/client/components"; + +class LanguageDropDown extends Component { + static propTypes = { + currentLanguage: PropTypes.string, + handleChange: PropTypes.func, + languages: PropTypes.array + } + + state = { + value: "" + } + + buttonElement() { + return ( + + ); + } + onChange = (event, value) => { + this.setState({ + value: value + }); + + this.props.handleChange(value); + } + + render() { + return ( +
+ {this.props.languages.length > 1 && +
+ + + + {this.props.languages.map((language) => ( + + ))} + +
+ } +
+ ); + } +} + +export default LanguageDropDown; diff --git a/client/modules/i18n/templates/header/containers/i18nContainer.js b/client/modules/i18n/templates/header/containers/i18nContainer.js new file mode 100644 index 00000000000..1ad62ab85fc --- /dev/null +++ b/client/modules/i18n/templates/header/containers/i18nContainer.js @@ -0,0 +1,55 @@ +import React, { Component } from "react"; +import { Reaction } from "/client/api"; +import { Shops } from "/lib/collections"; +import { composeWithTracker } from "/lib/api/compose"; +import Language from "../components/i18n"; + +class LanguageDropdownContainer extends Component { + handleChange = (value) => { + Meteor.users.update(Meteor.userId(), { $set: { "profile.lang": value } }); + } + render() { + return ( +
+ +
+ ); + } +} + +const composer = (props, onData) => { + const languages = []; + let currentLanguage = ""; + if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) { + const shop = Shops.findOne(); + if (typeof shop === "object" && shop.languages) { + for (const language of shop.languages) { + if (language.enabled === true) { + language.translation = "languages." + language.label.toLowerCase(); + // appending a helper to let us know this + // language is currently selected + const profile = Meteor.user().profile; + if (profile && profile.lang) { + if (profile.lang === language.i18n) { + currentLanguage = profile.lang; + } + } else if (shop.language === language.i18n) { + // we don't have a profile language + // use the shop default + currentLanguage = shop.language; + } + languages.push(language); + } + } + } + } + onData(null, { + languages: languages, + currentLanguage: currentLanguage + }); +}; + +export default composeWithTracker(composer)(LanguageDropdownContainer); diff --git a/client/modules/i18n/templates/header/i18n.html b/client/modules/i18n/templates/header/i18n.html deleted file mode 100644 index da9f628ace1..00000000000 --- a/client/modules/i18n/templates/header/i18n.html +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/client/modules/i18n/templates/header/i18n.js b/client/modules/i18n/templates/header/i18n.js deleted file mode 100644 index 285eb4e68fb..00000000000 --- a/client/modules/i18n/templates/header/i18n.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Reaction } from "/client/api"; -import { Shops } from "/lib/collections"; -/** - * i18nChooser helpers - */ - -Template.i18nChooser.helpers({ - languages() { - const languages = []; - if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) { - const shop = Shops.findOne(); - if (typeof shop === "object" && shop.languages) { - for (const language of shop.languages) { - if (language.enabled === true) { - language.translation = "languages." + language.label.toLowerCase(); - // appending a helper to let us know this - // language is currently selected - const profile = Meteor.user().profile; - if (profile && profile.lang) { - if (profile.lang === language.i18n) { - language.class = "active"; - } - } else if (shop.language === language.i18n) { - // we don't have a profile language - // use the shop default - language.class = "active"; - } - languages.push(language); - } - } - if (languages.length > 1) { - return languages; - } - } - } - if (languages.length > 1) { - return languages; - } - } -}); - -/** - * i18nChooser events - */ - -Template.i18nChooser.events({ - "click .i18n-language"(event) { - event.preventDefault(); - // - // this is a sanctioned use of Meteor.user.update - // and only possible because we allow it in the - // UserProfile and ShopMembers publications. - // - Meteor.users.update(Meteor.userId(), { $set: { "profile.lang": this.i18n } }); - } -}); diff --git a/imports/plugins/core/checkout/client/components/cartIcon.js b/imports/plugins/core/checkout/client/components/cartIcon.js new file mode 100644 index 00000000000..a9cd220922c --- /dev/null +++ b/imports/plugins/core/checkout/client/components/cartIcon.js @@ -0,0 +1,31 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import Velocity from "velocity-animate"; +import { Reaction } from "/client/api"; + +class CartIcon extends Component { + static propTypes = { + cart: PropTypes.object + } + + handleClick = (event) => { + event.preventDefault(); + const cartDrawer = document.querySelector("#cart-drawer-container"); + Velocity(cartDrawer, { opacity: 1 }, 300, () => { + Reaction.toggleSession("displayCart"); + }); + } + + render() { + return ( +
+ + + +
{this.props.cart ? this.props.cart.cartCount() : 0}
+
+ ); + } +} + +export default CartIcon; diff --git a/imports/plugins/core/checkout/client/container/cartIconContainer.js b/imports/plugins/core/checkout/client/container/cartIconContainer.js new file mode 100644 index 00000000000..39bed4dc6c5 --- /dev/null +++ b/imports/plugins/core/checkout/client/container/cartIconContainer.js @@ -0,0 +1,29 @@ +import React, { Component } from "react"; +import { Cart } from "/lib/collections"; +import { composeWithTracker } from "/lib/api/compose"; +import { Reaction } from "/client/api"; +import CartIcon from "../components/cartIcon"; + +class CartIconContainer extends Component { + render() { + return ( +
+ +
+ ); + } +} + +const composer = (props, onData) => { + const subscription = Reaction.Subscriptions.Cart; + + if (subscription.ready()) { + const cart = Cart.findOne(); + + onData(null, { + cart: cart + }); + } +}; + +export default composeWithTracker(composer)(CartIconContainer); diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/components/brand.js b/imports/plugins/core/ui-navbar/client/components/navbar/components/brand.js new file mode 100644 index 00000000000..e47ac636700 --- /dev/null +++ b/imports/plugins/core/ui-navbar/client/components/navbar/components/brand.js @@ -0,0 +1,42 @@ +import React, { Component } from "react"; +import { Reaction } from "/client/api"; +import { Media, Shops } from "/lib/collections"; + +class Brand extends Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick = (event) => { + event.preventDefault(); + Reaction.Router.go("/"); + } + + getShop() { + return Shops.findOne(Reaction.getShopId()); + } + + getLogo() { + if (_.isArray(this.getShop().brandAssets)) { + const brandAsset = _.find(this.getShop().brandAssets, (asset) => asset.type === "navbarBrandImage"); + return Media.findOne(brandAsset.mediaId); + } + return false; + } + + render() { + return ( + + {this.getLogo() && +
+ +
+ } + {this.getShop().name} +
+ ); + } +} + +export default Brand; diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/components/navbar.js b/imports/plugins/core/ui-navbar/client/components/navbar/components/navbar.js new file mode 100644 index 00000000000..44f3b3aff45 --- /dev/null +++ b/imports/plugins/core/ui-navbar/client/components/navbar/components/navbar.js @@ -0,0 +1,130 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { FlatButton, Button } from "/imports/plugins/core/ui/client/components"; +import { NotificationContainer } from "/imports/plugins/included/notifications/client/containers"; +import CartIconContainer from "/imports/plugins/core/checkout/client/container/cartIconContainer"; +import CartPanel from "/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer"; +import MainDropdown from "/client/modules/accounts/containers/dropdown/mainDropdownContainer"; +import LanguageContainer from "/client/modules/i18n/templates/header/containers/i18nContainer"; +import CurrencyContainer from "/client/modules/i18n/templates/currency/containers/currencyContainer"; +import TagNavContainer from "/imports/plugins/core/ui-tagnav/client/containers/tagNavContainer"; +import Brand from "./brand"; + +class NavBar extends Component { + static propTypes = { + hasProperPermission: PropTypes.bool, + searchEnabled: PropTypes.bool + } + + state = { + navBarVisible: false + } + + toggleNavbarVisibility = () => { + const isVisible = this.state.navBarVisible; + this.setState({ navBarVisible: !isVisible }); + } + + handleCloseNavbar = () => { + this.setState({ navBarVisible: false }); + }; + + renderLanguage() { + return ( +
+ +
+ ); + } + + renderCurrency() { + return ( +
+ +
+ ); + } + + renderBrand() { + return ( + + ); + } + + renderSearchButton() { + if (this.props.searchEnabled) { + return ( +
+ +
+ ); + } + } + + renderNotificationIcon() { + if (this.props.hasProperPermission) { + return ( + + ); + } + } + + renderCartContainerAndPanel() { + return ( +
+
+ +
+
+ +
+
+ ); + } + + renderMainDropdown() { + return ( + + ); + } + + renderHamburgerButton() { + return ( +
+ ); + } + + renderTagNav() { + return ( +
+ + + +
+ ); + } + + render() { + return ( +
+ {this.renderHamburgerButton()} + {this.renderBrand()} + {this.renderTagNav()} + {this.renderSearchButton()} + {this.renderNotificationIcon()} + {this.renderLanguage()} + {this.renderCurrency()} + {this.renderMainDropdown()} + {this.renderCartContainerAndPanel()} +
+ ); + } +} + +export default NavBar; diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/containers/navbarContainer.js b/imports/plugins/core/ui-navbar/client/components/navbar/containers/navbarContainer.js new file mode 100644 index 00000000000..7f3accbd796 --- /dev/null +++ b/imports/plugins/core/ui-navbar/client/components/navbar/containers/navbarContainer.js @@ -0,0 +1,39 @@ +import React, { Component } from "react"; +import { composeWithTracker } from "/lib/api/compose"; +import { Reaction } from "/client/api"; +import NavBar from "../components/navbar"; + +class NavBarContainer extends Component { + render() { + return ( +
+ +
+ ); + } +} + +function composer(props, onData) { + const searchPackage = Reaction.Apps({ provides: "ui-search" }); + let searchEnabled; + let searchTemplate; + + if (searchPackage.length) { + searchEnabled = true; + searchTemplate = searchPackage[0].template; + } else { + searchEnabled = false; + } + + const hasProperPermission = Reaction.hasPermission("account/profile"); + + onData(null, { + searchEnabled, + searchTemplate, + hasProperPermission + }); +} + +export default composeWithTracker(composer)(NavBarContainer); diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html index 2717da455d6..df8e40c1756 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html @@ -1,22 +1,5 @@ - diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js index 5e3d17d59eb..9864fe71c13 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js @@ -1,9 +1,9 @@ import { FlatButton } from "/imports/plugins/core/ui/client/components"; import { NotificationContainer } from "/imports/plugins/included/notifications/client/containers"; import { Reaction } from "/client/api"; -import { Tags } from "/lib/collections"; import CartPanel from "../../../../checkout/client/templates/cartPanel/container/cartPanelContainer"; - +import MainDropdown from "/client/modules/accounts/containers/dropdown/mainDropdownContainer.js"; +import NavBarContainer from "./containers/navbarContainer"; Template.CoreNavigationBar.onCreated(function () { this.state = new ReactiveDict(); @@ -36,17 +36,19 @@ Template.CoreNavigationBar.events({ }, $("html").get(0)); $("body").css("overflow", "hidden"); $("#search-input").focus(); - }, - "click .notification-icon": function () { - $("body").css("overflow", "hidden"); - $("#notify-dropdown").focus(); } }); Template.CoreNavigationBar.helpers({ - isSearchEnabled() { - const instance = Template.instance(); - return instance.state.get("searchEnabled"); + dropdown() { + return { + component: MainDropdown + }; + }, + navbar() { + return { + component: NavBarContainer + }; }, searchTemplate() { @@ -56,18 +58,6 @@ Template.CoreNavigationBar.helpers({ } }, - IconButtonComponent() { - return { - component: FlatButton, - icon: "fa fa-search", - kind: "flat" - }; - }, - notificationButtonComponent() { - return { - component: NotificationContainer - }; - }, onMenuButtonClick() { const instance = Template.instance(); return () => { @@ -77,27 +67,24 @@ Template.CoreNavigationBar.helpers({ }; }, - tagNavProps() { + isSearchEnabled() { const instance = Template.instance(); - const tags = Tags.find({ - isTopLevel: true - }, { - sort: { - position: 1 - } - }).fetch(); + return instance.state.get("searchEnabled"); + }, + IconButtonComponent() { return { - name: "coreHeaderNavigation", - editable: Reaction.hasAdminAccess(), - isEditing: true, - tags: tags, - onToggleMenu(callback) { - // Register the callback - instance.toggleMenuCallback = callback; - } + component: FlatButton, + icon: "fa fa-search", + kind: "flat" }; }, + notificationButtonComponent() { + return { + component: NotificationContainer + }; + }, + cartPanel() { return CartPanel; } diff --git a/imports/plugins/core/ui-tagnav/client/components/tagGroup.js b/imports/plugins/core/ui-tagnav/client/components/tagGroup.js new file mode 100644 index 00000000000..ac9d93c5503 --- /dev/null +++ b/imports/plugins/core/ui-tagnav/client/components/tagGroup.js @@ -0,0 +1,163 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import update from "react/lib/update"; +import TagGroupBody from "./tagGroupBody"; +import TagGroupHeader from "./tagGroupHeader"; +import { TagItem } from "/imports/plugins/core/ui/client/components/tags/"; +import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; +import { getTagIds } from "/lib/selectors/tags"; + +class TagGroup extends Component { + constructor(props) { + super(props); + + const { parentTag, tagsByKey, tagIds } = props.tagGroupProps; + this.state = { + suggestions: [], + newTag: { + name: "" + }, + tagIds, + parentTag, + tagsByKey + }; + } + + componentWillReceiveProps(nextProps) { + const { parentTag, tagsByKey, tagIds } = nextProps.tagGroupProps; + this.setState({ tagIds, parentTag, tagsByKey }); + } + + get tags() { + if (this.props.editable) { + return this.state.tagIds.map((tagId) => this.state.tagsByKey[tagId]); + } + + return this.props.tagGroupProps.subTagGroups; + } + + get className() { + if (this.props.blank) { + return "create"; + } + return ""; + } + + handleGetSuggestions = (suggestionUpdateRequest) => { + const suggestions = TagHelpers.updateSuggestions( + suggestionUpdateRequest.value, + { excludeTags: this.state.tagIds } + ); + + this.setState({ suggestions }); + } + + handleClearSuggestions = () => { + this.setState({ suggestions: [] }); + } + + handleNewTagSave = (event, tag) => { + if (this.props.onNewTagSave) { + this.props.onNewTagSave(tag, this.props.tagGroupProps.parentTag); + this.setState({ + newTag: { name: "" } + }); + } + } + + handleTagUpdate = (event, tag) => { + const newState = update(this.state, { + tagsByKey: { + [tag._id]: { + $set: tag + } + } + }); + + this.setState(newState); + } + + handleNewTagUpdate = (event, tag) => { // updates blank tag state being edited + this.setState({ newTag: tag }); + } + + tagGroupBodyProps = (tag) => { + const subTagGroups = _.compact(TagHelpers.subTags(tag)); + const tagsByKey = {}; + + if (Array.isArray(subTagGroups)) { + for (const tagItem of subTagGroups) { + tagsByKey[tagItem._id] = tagItem; + } + } + + return { + parentTag: tag, + tagsByKey: tagsByKey || {}, + tagIds: getTagIds({ tags: subTagGroups }) || [], + subTagGroups + }; + } + + renderTree(tags) { + if (Array.isArray(tags)) { + return tags.map((tag) => ( +
+ + +
+ )); + } + } + + render() { + return ( +
+
+ {this.state.parentTag.name} + View All +
+
+ {this.renderTree(this.tags)} + {this.props.editable && +
+
+ +
+
+ } +
+
+ ); + } +} + +TagGroup.propTypes = { + blank: PropTypes.bool, + editable: PropTypes.bool, + onNewTagSave: PropTypes.func, + onTagRemove: PropTypes.func, + tagGroupProps: PropTypes.object +}; + +export default TagGroup; diff --git a/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.html b/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.html deleted file mode 100644 index 1b501217817..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.html +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.js b/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.js deleted file mode 100644 index 82592b1c3b6..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagGroup/tagGroup.js +++ /dev/null @@ -1,50 +0,0 @@ -import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; - -Template.tagGroup.onRendered(() => { - -}); - -Template.tagGroup.helpers({ - className() { - const instance = Template.instance(); - if (instance.data.blank) { - return "create"; - } - }, - - tagGroupProps(groupTag) { - const instance = Template.instance(); - - return { - tag: groupTag, - isEditing: instance.data.isEditing, - onTagRemove(tag) { - instance.data.onTagRemove(tag, instance.data.parentTag); - }, - onTagUpdate: instance.data.onTagUpdate - }; - }, - - tagListProps(groupTag) { - const instance = Template.instance(); - - return { - parentTag: groupTag, - tags: TagHelpers.subTags(groupTag), - isEditing: instance.data.isEditing, - onTagCreate(tagName) { - if (instance.data.onTagCreate) { - instance.data.onTagCreate(tagName, instance.data.groupTag); - } - }, - onTagRemove(tag) { - instance.data.onTagRemove(tag, instance.data.groupTag); - }, - onTagSort(newTagsOrder) { - instance.data.onTagSort(newTagsOrder, instance.data.groupTag); - }, - onTagDragAdd: instance.data.onTagDragAdd, - onTagUpdate: instance.data.onTagUpdate - }; - } -}); diff --git a/imports/plugins/core/ui-tagnav/client/components/tagGroupBody.js b/imports/plugins/core/ui-tagnav/client/components/tagGroupBody.js new file mode 100644 index 00000000000..83cc330ffc3 --- /dev/null +++ b/imports/plugins/core/ui-tagnav/client/components/tagGroupBody.js @@ -0,0 +1,174 @@ +import debounce from "lodash/debounce"; +import React, { Component } from "react"; +import update from "react/lib/update"; +import PropTypes from "prop-types"; +import { TagItem } from "/imports/plugins/core/ui/client/components/tags/"; + +class TagGroupBody extends Component { + constructor(props) { + super(props); + + const { parentTag, tagsByKey, tagIds } = props.tagGroupBodyProps; + this.state = { + suggestions: [], + newTag: { + name: "" + }, + tagIds, + parentTag, + tagsByKey + }; + } + + componentWillReceiveProps(nextProps) { + const { parentTag, tagsByKey, tagIds } = nextProps.tagGroupBodyProps; + this.setState({ tagIds, parentTag, tagsByKey }); + } + + handleNewTagSave = (event, tag) => { + if (this.props.onNewTagSave) { + this.props.onNewTagSave(tag, this.state.parentTag); + this.setState({ + newTag: { name: "" } + }); + } + } + + handleTagUpdate = (event, tag) => { + const newState = update(this.state, { + tagsByKey: { + [tag._id]: { + $set: tag + } + } + }); + + this.setState(newState); + } + + handleNewTagUpdate = (event, tag) => { // updates blank tag state being edited + this.setState({ newTag: tag }); + } + + handleGetSuggestions = (suggestionUpdateRequest) => { + const suggestions = this.props.updateSuggestions( + suggestionUpdateRequest.value, + { excludeTags: this.state.tagIds } + ); + + this.setState({ suggestions }); + } + + handleClearSuggestions = () => { + this.setState({ suggestions: [] }); + } + + handleMoveTag = (dragIndex, hoverIndex) => { + const tag = this.state.tagIds[dragIndex]; + if (!tag) { + return false; + } + // Apply new sort order to variant list + const newState = update(this.state, { + tagIds: { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, tag] + ] + } + }); + + // Set local state so the component does't have to wait for a round-trip + // to the server to get the updated list of variants + this.setState(newState, () => { + debounce(() => this.props.onTagSort(this.state.tagIds, this.state.parentTag), 500)(); + }); + } + + handleTagSave = (event, tag) => { + if (this.props.onUpdateTag) { + this.props.onUpdateTag(tag._id, tag.name, this.state.parentTag._id); + } + } + + get tags() { + if (this.props.editable) { + return this.state.tagIds.map((tagId) => this.state.tagsByKey[tagId]); + } + + return this.props.tagGroupBodyProps.subTagGroups; + } + + genTagsList(tags, parentTag) { + if (Array.isArray(tags)) { + return tags.map((tag, index) => { + return ( + + ); + }); + } + } + + render() { + return ( +
+
+ {this.genTagsList(_.compact(this.tags), this.state.parentTag)} + {this.props.editable && +
+ +
+ } +
+
+ ); + } +} + +TagGroupBody.propTypes = { + editable: PropTypes.bool, + onNewTagSave: PropTypes.func, + onTagClick: PropTypes.func, + onTagRemove: PropTypes.func, + onTagSort: PropTypes.func, + onUpdateTag: PropTypes.func, + tagGroupBodyProps: PropTypes.object, + updateSuggestions: PropTypes.func +}; + +export default TagGroupBody; diff --git a/imports/plugins/core/ui-tagnav/client/components/tagGroupHeader.js b/imports/plugins/core/ui-tagnav/client/components/tagGroupHeader.js new file mode 100644 index 00000000000..4ff5e587531 --- /dev/null +++ b/imports/plugins/core/ui-tagnav/client/components/tagGroupHeader.js @@ -0,0 +1,72 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { TagItem } from "/imports/plugins/core/ui/client/components/tags/"; + +class TagGroupHeader extends Component { + constructor(props) { + super(props); + this.state = { + suggestions: [], + tag: this.props.tag + }; + } + + handleGetSuggestions = (suggestionUpdateRequest) => { + const suggestions = this.props.updateSuggestions( + suggestionUpdateRequest.value, + { excludeTags: this.state.tagIds } + ); + + this.setState({ suggestions }); + } + + handleTagUpdate = (event, tag) => { + this.setState({ tag: tag }); + } + + handleTagSave = (event, tag) => { + if (this.props.onUpdateTag) { + this.props.onUpdateTag(tag._id, tag.name, this.props.parentTag._id); + } + } + + handleTagTreeMove = () => { + // needed to prevent move errors, pending fix for TagGroup draging + } + + render() { + return ( +
+ +
+ ); + } +} + +TagGroupHeader.propTypes = { + editable: PropTypes.bool, + onTagClick: PropTypes.func, + onTagRemove: PropTypes.func, + onUpdateTag: PropTypes.func, + parentTag: PropTypes.object, + tag: PropTypes.object, + updateSuggestions: PropTypes.func +}; + +export default TagGroupHeader; diff --git a/imports/plugins/core/ui-tagnav/client/components/tagNav.js b/imports/plugins/core/ui-tagnav/client/components/tagNav.js new file mode 100644 index 00000000000..db0ba76e433 --- /dev/null +++ b/imports/plugins/core/ui-tagnav/client/components/tagNav.js @@ -0,0 +1,115 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { getTagIds } from "/lib/selectors/tags"; +import { DragDropProvider } from "/imports/plugins/core/ui/client/providers"; +import { Button, EditButton } from "/imports/plugins/core/ui/client/components"; +import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; +import { TagList } from "/imports/plugins/core/ui/client/components/tags/"; +import TagGroup from "./tagGroup"; + +class TagNav extends Component { + constructor(props) { + super(props); + this.state = { + selectedTag: this.props.selectedTag || {} + }; + } + + componentWillReceiveProps(nextProps) { + this.setState({ selectedTag: nextProps.selectedTag }); + } + + renderEditButton() { + const { editContainerItem } = this.props.navButtonStyles; + return ( + + + + ); + } + + tagGroupProps = (tag) => { + const subTagGroups = _.compact(TagHelpers.subTags(tag)); + const tagsByKey = {}; + + if (Array.isArray(subTagGroups)) { + for (const tagItem of subTagGroups) { + tagsByKey[tagItem._id] = tagItem; + } + } + + return { + parentTag: tag, + tagsByKey: tagsByKey || {}, + tagIds: getTagIds({ tags: subTagGroups }) || [], + subTagGroups + }; + } + + render() { + const { navbarOrientation, navbarPosition, navbarAnchor, navbarVisibility } = this.props; + return ( +
+
+
+
+ + +
+ +
+
+
+ {this.props.canEdit && this.renderEditButton()} +
+
+ ); + } +} + +TagNav.propTypes = { + canEdit: PropTypes.bool, + children: PropTypes.node, + closeNavbar: PropTypes.func, + editable: PropTypes.bool, + navButtonStyles: PropTypes.object, + navbarAnchor: PropTypes.string, + navbarOrientation: PropTypes.string, + navbarPosition: PropTypes.string, + navbarVisibility: PropTypes.string, + onEditButtonClick: PropTypes.func, + onMoveTag: PropTypes.func, + selectedTag: PropTypes.object +}; + +export default TagNav; diff --git a/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.html b/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.html deleted file mode 100644 index fa739906d8a..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.html +++ /dev/null @@ -1,36 +0,0 @@ - - diff --git a/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.js b/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.js deleted file mode 100644 index c1c39757bea..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagNav/tagNav.js +++ /dev/null @@ -1,381 +0,0 @@ -import Sortable from "sortablejs"; -import { Template } from "meteor/templating"; -import { ReactiveDict } from "meteor/reactive-dict"; -import { Reaction } from "/client/api"; -import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; -import { IconButton, Overlay } from "/imports/plugins/core/ui/client/components"; - -const NavbarStates = { - Orientation: "stateNavbarOrientation", - Position: "stateNavbarPosition", - Anchor: "stateNavbarAnchor", - Visible: "stateNavbarVisible" -}; - -const NavbarOrientation = { - Vertical: "vertical", - Horizontal: "horizontal" -}; - -const NavbarVisibility = { - Shown: "shown", - Hidden: "hidden" -}; - -const NavbarPosition = { - Static: "static", - Fixed: "fixed" -}; - -const NavbarAnchor = { - Top: "top", - Right: "right", - Bottom: "bottom", - Left: "left", - None: "inline" -}; - -const TagNavHelpers = { - onTagCreate(tagName, parentTag) { - TagHelpers.createTag(tagName, undefined, parentTag); - }, - onTagRemove(tag, parentTag) { - TagHelpers.removeTag(tag, parentTag); - }, - onTagSort(tagIds, parentTag) { - TagHelpers.sortTags(tagIds, parentTag); - }, - onTagDragAdd(movedTagId, toListId, toIndex, ofList) { - TagHelpers.moveTagToNewParent(movedTagId, toListId, toIndex, ofList); - }, - onTagUpdate(tagId, tagName) { - TagHelpers.updateTag(tagId, tagName); - }, - isMobile() { - return window.matchMedia("(max-width: 991px)").matches; - }, - tagById(tagId, tags) { - return _.find(tags, (tag) => tag._id === tagId); - }, - hasSubTags(tagId, tags) { - const foundTag = this.tagById(tagId, tags); - - if (foundTag) { - if (_.isArray(foundTag.relatedTagIds) && foundTag.relatedTagIds.length) { - return true; - } - } - return false; - } -}; - -Template.tagNav.onCreated(function () { - this.state = new ReactiveDict(); - this.state.setDefault({ - attachedBodyListener: false, - isEditing: false, - selectedTag: null, - [NavbarStates.Visible]: false - }); - - this.moveItem = (array, fromIndex, toIndex) => { - array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); - return array; - }; - - this.toggleNavbarVisibility = () => { - const isVisible = this.state.get(NavbarStates.Visible); - this.state.set(NavbarStates.Visible, !isVisible); - }; - - this.closeNavbar = () => { - this.state.set(NavbarStates.Visible, false); - }; - - this.data.onToggleMenu(this.toggleNavbarVisibility); - - if (this.data.name) { - document.body.addEventListener(`${this.data.name}_toggleMenuVisibility`, this.toggleNavbarVisibility); - } - - this.attachBodyListener = () => { - document.body.addEventListener("mouseover", this.closeDropdown); - this.state.set("attachedBodyListener", true); - }; - - this.detachhBodyListener = () => { - document.body.removeEventListener("mouseover", this.closeDropdown); - this.state.set("attachedBodyListener", false); - }; - - this.closeDropdown = (event) => { - if ($(event.target).closest(".navbar-item").length === 0) { - this.closeDropdownTimeout = setTimeout(() => { - this.state.set("selectedTag", null); - this.detachhBodyListener(); - }, 500); - } else { - if (this.closeDropdownTimeout) { - clearTimeout(this.closeDropdownTimeout); - } - } - }; - - this.state.set(NavbarStates.Visibility, NavbarVisibility.Hidden); - - - this.onWindowResize = () => { - if (window.matchMedia("(max-width: 991px)").matches) { - this.state.set(NavbarStates.Orientation, NavbarOrientation.Vertical); - this.state.set(NavbarStates.Position, NavbarPosition.Fixed); - this.state.set(NavbarStates.Anchor, NavbarAnchor.Left); - } else { - this.state.set(NavbarStates.Orientation, NavbarOrientation.Horizontal); - this.state.set(NavbarStates.Position, NavbarPosition.Static); - this.state.set(NavbarStates.Anchor, NavbarAnchor.None); - this.state.set(NavbarStates.Visible, false); - } - }; -}); - -Template.tagNav.onRendered(() => { - const instance = Template.instance(); - const list = instance.$(".navbar-items")[0]; - - $(window).on("resize", instance.onWindowResize).trigger("resize"); - - instance._sortable = Sortable.create(list, { - group: "tags", - handle: ".js-tagNav-item", - onSort(event) { - const tagIds = instance.data.tags.map(item => { - if (item) { - return item._id; - } - return null; - }); - - const newTagsOrder = instance.moveItem(tagIds, event.oldIndex, event.newIndex); - - if (newTagsOrder) { - TagNavHelpers.onTagSort(newTagsOrder, instance.data.parentTag); - } - }, - - // On add from another list - onAdd(event) { - const toListId = event.to.dataset.id; - const movedTagId = event.item.dataset.id; - const tagIds = instance.data.tags.map(item => { - if (item) { - return item._id; - } - return null; - }); - - TagNavHelpers.onTagDragAdd(movedTagId, toListId, event.newIndex, tagIds); - }, - - // Tag removed from list becuase it was dragged to a different list - onRemove(event) { - const movedTagId = event.item.dataset.id; - const foundTag = _.find(instance.data.tags, (tag) => { - return tag._id === movedTagId; - }); - - TagNavHelpers.onTagRemove(foundTag, instance.data.parentTag); - } - }); -}); - -Template.tagNav.onDestroyed(function () { - // $(window).off("resize", this.onWindowResize); -}); - - -Template.tagNav.helpers({ - EditButton() { - const instance = Template.instance(); - const state = instance.state; - const isEditing = state.equals("isEditing", true); - - return { - component: IconButton, - bezelStyle: "solid", - primary: true, - icon: "fa fa-pencil", - onIcon: "fa fa-check", - toggle: true, - toggleOn: isEditing, - onClick() { - state.set("isEditing", !isEditing); - } - }; - }, - - OverlayComponent() { - const instance = Template.instance(); - const isVisible = instance.state.get(NavbarStates.Visible); - - return { - component: Overlay, - isVisible, - onClick() { - instance.closeNavbar(); - } - }; - }, - - navbarOrientation() { - return Template.instance().state.get(NavbarStates.Orientation); - }, - - navbarPosition() { - return Template.instance().state.get(NavbarStates.Position); - }, - - navbarAnchor() { - return Template.instance().state.get(NavbarStates.Anchor); - }, - - navbarVisibility() { - const isVisible = Template.instance().state.equals(NavbarStates.Visible, true); - - if (isVisible) { - return "open"; - } - return "closed"; - }, - - navbarSelectedClassName(tag) { - const selectedTag = Template.instance().state.get("selectedTag"); - - if (selectedTag) { - if (selectedTag._id === tag._id) { - return "selected"; - } - } - return ""; - }, - - hasDropdownClassName(tag) { - if (_.isArray(tag.relatedTagIds)) { - return "has-dropdown"; - } - return null; - }, - - isEditing() { - return Template.instance().state.equals("isEditing", true); - }, - - canEdit() { - return Template.instance().data.editable && Reaction.isPreview() === false; - }, - - handleMenuClose() { - const instance = Template.instance(); - - return () => { - instance.toggleNavbarVisibility(); - }; - }, - - tagTreeProps(parentTag) { - const instance = Template.instance(); - - return { - parentTag, - subTagGroups: TagHelpers.subTags(parentTag), - isEditing: instance.state.equals("isEditing", true), - ...TagNavHelpers - }; - }, - tagProps(tag) { - const instance = Template.instance(); - let isSelected = false; - if (instance.data.selectedTag && tag) { - isSelected = instance.data.selectedTag._id === tag._id; - } - - return { - tag, - isEditing: instance.state.equals("isEditing", true), - selectable: true, - isSelected, - className: "js-tagNav-item", - onTagSelect(selectedTag) { - if (JSON.stringify(selectedTag) === JSON.stringify(instance.state.get("selectedTag"))) { - instance.state.set("selectedTag", null); - } else { - instance.state.set("selectedTag", selectedTag); - } - }, - ...TagNavHelpers - }; - }, - blankTagProps() { - const instance = Template.instance(); - - return { - isEditing: instance.state.equals("isEditing", true), - blank: true, - onTagCreate: TagNavHelpers.onTagCreate - }; - } -}); - - -Template.tagNav.events({ - "click .navbar-item .rui.tag.link"(event, instance) { - if (TagNavHelpers.isMobile()) { - const tagId = event.target.dataset.id; - const tags = instance.data.tags; - const selectedTag = instance.state.get("selectedTag"); - const hasSubTags = TagNavHelpers.hasSubTags(tagId, tags); - - if (hasSubTags === false) { - // click close button to make navbar left disappear - instance.closeNavbar(); - } else { - event.preventDefault(); - } - - if (selectedTag && selectedTag._id === tagId) { - instance.state.set("selectedTag", null); - } else if (hasSubTags) { - instance.state.set("selectedTag", TagNavHelpers.tagById(tagId, tags)); - } - } - }, - - "click [data-event-action=close-tagnav-overlay]"(event, instance) { - instance.closeNavbar(); - }, - - "mouseover .navbar-item, focus .navbar-item"(event, instance) { - const tagId = event.currentTarget.dataset.id; - const tags = instance.data.tags; - - if (TagNavHelpers.isMobile()) { - return; - } - - // While in edit mode, don't trigger the hover hide/show menu - if (instance.state.equals("isEditing", false)) { - // User mode - // Don't show dropdown if there are no subtags - if (TagNavHelpers.hasSubTags(tagId, tags) === false) { - instance.state.set("selectedTag", null); - return; - } - - // Otherwise, show the menu - // And Attach an event listener to the document body - // This will check to see if the dropdown should be closed if the user - // leaves the tag nav bar - instance.attachBodyListener(); - instance.state.set("selectedTag", TagNavHelpers.tagById(tagId, tags)); - } - } -}); diff --git a/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.html b/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.html deleted file mode 100644 index 14bed3f6e2a..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.js b/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.js deleted file mode 100644 index c4fd6e7a03e..00000000000 --- a/imports/plugins/core/ui-tagnav/client/components/tagTree/tagTree.js +++ /dev/null @@ -1,101 +0,0 @@ -import Sortable from "sortablejs"; -import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; - -Template.tagTree.onRendered(() => { - const instance = Template.instance(); - const list = instance.$(".content")[0]; - - instance._sortable = Sortable.create(list, { - group: "tagGroup", - handle: ".js-drag-handle", - draggable: ".rui.grouptag", - onSort(event) { - const tagIds = instance.data.subTagGroups.map(item => { - if (item) { - return item._id; - } - }); - - const newTagsOrder = TagHelpers.moveItem(tagIds, event.oldIndex, event.newIndex); - - if (newTagsOrder) { - if (instance.data.onTagSort) { - instance.data.onTagSort(newTagsOrder, instance.data.parentTag); - } - } - }, - - // On add from another list - onAdd(event) { - const toListId = event.to.dataset.id; - const movedTagId = event.item.dataset.id; - const tagIds = instance.data.subTagGroups.map(item => { - if (item) { - return item._id; - } - }); - - if (instance.data.onTagDragAdd) { - instance.data.onTagDragAdd(movedTagId, toListId, event.newIndex, tagIds); - } - }, - - // Tag removed from list becuase it was dragged to a different list - onRemove(event) { - const movedTagId = event.item.dataset.id; - - if (instance.data.onTagRemove) { - const foundTag = _.find(instance.data.subTagGroups, (tag) => { - return tag._id === movedTagId; - }); - - instance.data.onTagRemove(foundTag, instance.data.parentTag); - } - } - }); -}); - -Template.tagTree.helpers({ - isEditing() { - return Template.instance().data.isEditing; - }, - - tagGroupProps(groupTag) { - const instance = Template.instance(); - - return { - groupTag, - parentTag: instance.data.parentTag, - isEditing: instance.data.isEditing, - onTagCreate: instance.data.onTagCreate, - onTagDragAdd: instance.data.onTagDragAdd, - onTagRemove: instance.data.onTagRemove, - onTagSort: instance.data.onTagSort, - onTagUpdate: instance.data.onTagUpdate - }; - }, - newTagGroupProps(parentTag) { - const instance = Template.instance(); - - return { - blank: true, - onTagCreate(newGroupName) { - if (instance.data.onTagCreate) { - instance.data.onTagCreate(newGroupName, parentTag); - } - } - }; - } -}); - - -Template.tagTreeNewGroup.helpers({ - props() { - const instance = Template.instance(); - - return { - blank: true, - onTagCreate: instance.data.onTagCreate - }; - } -}); diff --git a/imports/plugins/core/ui-tagnav/client/containers/tagNavContainer.js b/imports/plugins/core/ui-tagnav/client/containers/tagNavContainer.js new file mode 100644 index 00000000000..f90fd0ab2fb --- /dev/null +++ b/imports/plugins/core/ui-tagnav/client/containers/tagNavContainer.js @@ -0,0 +1,454 @@ +import debounce from "lodash/debounce"; +import update from "react/lib/update"; +import React, { Component, PropTypes } from "react"; +import { Reaction, Router } from "/client/api"; +import { composeWithTracker } from "/lib/api/compose"; +import { getTagIds } from "/lib/selectors/tags"; +import { Overlay } from "/imports/plugins/core/ui/client/components"; +import { TagHelpers } from "/imports/plugins/core/ui-tagnav/client/helpers"; +import { Tags } from "/lib/collections"; +import TagNav from "../components/tagNav"; + +const navButtonStyles = { + editContainerItem: { + display: "flex", + marginLeft: 5 + } +}; + +const NavbarStates = { + Orientation: "stateNavbarOrientation", + Position: "stateNavbarPosition", + Anchor: "stateNavbarAnchor", + Visible: "stateNavbarVisible" +}; + +const NavbarOrientation = { + Vertical: "vertical", + Horizontal: "horizontal" +}; + +const NavbarPosition = { + Static: "static", + Fixed: "fixed" +}; + +const NavbarAnchor = { + Top: "top", + Right: "right", + Bottom: "bottom", + Left: "left", + None: "inline" +}; + +const TagNavHelpers = { + onTagCreate(tagName, parentTag) { + TagHelpers.createTag(tagName, undefined, parentTag); + }, + onTagRemove(tag, parentTag) { + TagHelpers.removeTag(tag, parentTag); + }, + onTagSort(tagIds, parentTag) { + TagHelpers.sortTags(tagIds, parentTag); + }, + onTagDragAdd(movedTagId, toListId, toIndex, ofList) { + TagHelpers.moveTagToNewParent(movedTagId, toListId, toIndex, ofList); + }, + onUpdateTag(tagId, tagName, parentTagId) { + TagHelpers.updateTag(tagId, tagName, parentTagId); + }, + isMobile() { + return window.matchMedia("(max-width: 991px)").matches; + }, + tagById(tagId, tags) { + return _.find(tags, (tag) => tag._id === tagId); + }, + updateSuggestions(suggestion, excludeTagsObj) { + return TagHelpers.updateSuggestions(suggestion, excludeTagsObj); + }, + hasSubTags(tagId, tags) { + const foundTag = this.tagById(tagId, tags); + + if (foundTag) { + if (Array.isArray(foundTag.relatedTagIds) && foundTag.relatedTagIds.length) { + return true; + } + } + return false; + } +}; + +class TagNavContainer extends Component { + constructor(props) { + super(props); + + this.state = { + attachedBodyListener: false, + editable: false, + tagIds: props.tagIds || [], + tagsByKey: props.tagsByKey || {}, + selectedTag: null, + suggestions: [], + [NavbarStates.Visible]: props.isVisible, + newTag: { + name: "" + } + }; + + this.onWindowResize = this.onWindowResize.bind(this); + } + + componentDidMount() { + window.addEventListener("resize", this.onWindowResize); + this.onWindowResize(); + } + + componentWillReceiveProps(nextProps) { + let selectedTag = {}; + const previousEdit = this.state.editable; + nextProps.tagsAsArray.map((tag) => { + if (this.isSelected(tag)) { + selectedTag = tag; + } + }); + + const { tagIds, tagsByKey, isVisible } = nextProps; + this.setState({ + [NavbarStates.Visible]: isVisible, + editable: previousEdit && this.canEdit, + tagIds, + tagsByKey, + selectedTag + }); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.onWindowResize); + } + + onWindowResize = () => { + const matchQuery = window.matchMedia("(max-width: 991px)"); + if (matchQuery.matches) { + this.setState({ + [NavbarStates.Orientation]: NavbarOrientation.Vertical, + [NavbarStates.Position]: NavbarPosition.Fixed, + [NavbarStates.Anchor]: NavbarAnchor.Left + }); + } else { + this.setState({ + [NavbarStates.Orientation]: NavbarOrientation.Horizontal, + [NavbarStates.Position]: NavbarPosition.Static, + [NavbarStates.Anchor]: NavbarAnchor.None, + [NavbarStates.Visible]: false + }); + } + } + + canSaveTag(tag) { + // Blank tags cannot be saved + if (typeof tag.name === "string" && tag.name.trim().length === 0) { + return false; + } + + // If the tag does not have an id, then allow the save + if (!tag._id) { + return true; + } + + // Get the original tag from the props + // Tags from props are not mutated, and come from an outside source + const originalTag = this.props.tagsByKey[tag._id]; + + if (originalTag && originalTag.name !== tag.name) { + return true; + } + + return false; + } + + handleNewTagSave = (tag, parentTag) => { + if (this.canSaveTag(tag)) { + TagNavHelpers.onTagCreate(tag.name, parentTag); + this.setState({ newTag: { name: "" } }); + } + } + + handleNewTagUpdate = (tag) => { // updates the current tag state being edited + this.setState({ + newTag: tag + }); + } + + handleTagRemove = (tag, parentTag) => { + TagNavHelpers.onTagRemove(tag, parentTag); + } + + handleTagUpdate = (tag) => { + const newState = update(this.state, { + tagsByKey: { + [tag._id]: { + $set: tag + } + } + }); + + this.setState(newState); + } + + handleTagSave = (tag) => { + TagNavHelpers.onUpdateTag(tag._id, tag.name); + } + + handleMoveTag = (dragIndex, hoverIndex) => { + const tag = this.state.tagIds[dragIndex]; + + // Apply new sort order to variant list + const newState = update(this.state, { + tagIds: { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, tag] + ] + } + }); + + // Set local state so the component does't have to wait for a round-trip + // to the server to get the updated list of variants + this.setState(newState, () => { + debounce(() => TagNavHelpers.onTagSort(this.state.tagIds), 500)(); // Save the updated positions + }); + } + + handleGetSuggestions = (suggestionUpdateRequest) => { + const suggestions = TagNavHelpers.updateSuggestions( + suggestionUpdateRequest.value, + { excludeTags: this.state.tagIds } + ); + + this.setState({ suggestions }); + } + + handleClearSuggestions = () => { + this.setState({ suggestions: [] }); + } + + get canEdit() { + return this.props.hasEditRights && Reaction.isPreview() === false; + } + + attachBodyListener = () => { + document.body.addEventListener("mouseover", this.closeDropdown); + this.setState({ attachedBodyListener: true }); + } + + detachhBodyListener = () => { + document.body.removeEventListener("mouseover", this.closeDropdown); + this.setState({ attachedBodyListener: false }); + } + + closeDropdown = (event) => { + const closestNavItem = event.target.closest(".navbar-item"); + + // on mouseover an element outside of tags, close dropdown + if (!closestNavItem) { + this.closeDropdownTimeout = setTimeout(() => { + this.setState({ selectedTag: null }); + this.detachhBodyListener(); + }, 500); + } else { + if (this.closeDropdownTimeout) { + clearTimeout(this.closeDropdownTimeout); + } + } + } + + get navbarOrientation() { + return this.state[NavbarStates.Orientation]; + } + + get navbarPosition() { + return this.state[NavbarStates.Position]; + } + + get navbarAnchor() { + return this.state[NavbarStates.Anchor]; + } + + get navbarVisibility() { + const isVisible = this.state[NavbarStates.Visible] === true; + + if (isVisible) { + return "open"; + } + return "closed"; + } + + onTagSelect = (currentSelectedTag) => { + if (_.isEqual(currentSelectedTag, this.state.selectedTag)) { + this.setState({ selectedTag: null }); + } else { + this.setState({ selectedTag: currentSelectedTag }); + } + } + + isSelected(tag) { + let isSelected = false; + if (this.state.selectedTag && tag) { + isSelected = this.state.selectedTag._id === tag._id; + } + return isSelected; + } + + handleTagMouseOver = (event, tag) => { + const tagId = tag._id; + const tags = this.props.tagsAsArray; + + if (TagNavHelpers.isMobile()) { + return; + } + // While in edit mode, don't trigger the hover hide/show menu + if (this.state.editable === false) { + // User mode + // Don't show dropdown if there are no subtags + if (TagNavHelpers.hasSubTags(tagId, tags) === false) { + this.setState({ selectedTag: null }); + return; + } + + // Otherwise, show the menu + // And Attach an event listener to the document body + // This will check to see if the dropdown should be closed if the user + // leaves the tag nav bar + this.attachBodyListener(); + this.setState({ selectedTag: TagNavHelpers.tagById(tagId, tags) }); + } + } + + handleTagClick = (event, tag) => { + if (TagNavHelpers.isMobile()) { + const tagId = tag._id; + const tags = this.props.tagsAsArray; + const selectedTag = this.state.selectedTag; + const hasSubTags = TagNavHelpers.hasSubTags(tagId, tags); + + if (hasSubTags === false) { + // click close button to make navbar left disappear + this.props.closeNavbar(); + } else { + event.preventDefault(); + } + + if (selectedTag && selectedTag._id === tagId) { + this.setState({ selectedTag: null }); + } else if (hasSubTags) { + this.setState({ selectedTag: TagNavHelpers.tagById(tagId, tags) }); + } + } + Router.go("tag", { slug: tag.slug }); + } + + handleEditButtonClick = () => { + this.setState({ editable: !this.state.editable }); + } + + hasDropdownClassName(tag) { + if (Array.isArray(tag.relatedTagIds)) { + return "has-dropdown"; + } + return ""; + } + + navbarSelectedClassName = (tag) => { + const currentSelectedTag = this.state.selectedTag; + + if (currentSelectedTag) { + if (currentSelectedTag._id === tag._id) { + return "selected"; + } + } + return ""; + } + + get tags() { + if (this.state.editable) { + return this.state.tagIds.map((tagId) => this.state.tagsByKey[tagId]); + } + + return this.props.tagsAsArray; + } + + render() { + return ( +
+ + +
+ ); + } +} + +TagNavContainer.propTypes = { + closeNavbar: PropTypes.func, + editButton: PropTypes.node, + editable: PropTypes.bool, + hasEditRights: PropTypes.bool, + isVisible: PropTypes.bool, + tagIds: PropTypes.arrayOf(PropTypes.string), + tagsAsArray: PropTypes.arrayOf(PropTypes.object), + tagsByKey: PropTypes.object +}; + +const composer = (props, onData) => { + let tags = Tags.find({ isTopLevel: true }, { sort: { position: 1 } }).fetch(); + tags = _.sortBy(tags, "position"); // puts tags without position at end of array + + const tagsByKey = {}; + + if (Array.isArray(tags)) { + for (const tag of tags) { + tagsByKey[tag._id] = tag; + } + } + + onData(null, { + name: "coreHeaderNavigation", + hasEditRights: Reaction.hasAdminAccess(), + tagsAsArray: tags, + isVisible: props.isVisible, + tagIds: getTagIds({ tags }), + tagsByKey + }); +}; + +export default composeWithTracker(composer, null)(TagNavContainer); diff --git a/imports/plugins/core/ui-tagnav/client/helpers/tags.js b/imports/plugins/core/ui-tagnav/client/helpers/tags.js index 9199a4fbd45..4885b13fb21 100644 --- a/imports/plugins/core/ui-tagnav/client/helpers/tags.js +++ b/imports/plugins/core/ui-tagnav/client/helpers/tags.js @@ -1,3 +1,4 @@ +import { Reaction, i18next } from "/client/api"; import { Tags } from "/lib/collections"; import { Meteor } from "meteor/meteor"; import { Session } from "meteor/session"; @@ -32,7 +33,7 @@ export const TagHelpers = { return subTags; } - return false; + return []; }, currentTag() { @@ -79,6 +80,9 @@ export const TagHelpers = { }, createTag(tagName, tagId, parentTag) { + if (!tagName) { + return; + } let parentTagId; if (parentTag) { @@ -173,6 +177,28 @@ export const TagHelpers = { } ); } + }, + + updateSuggestions(term, { excludeTags }) { + const slug = Reaction.getSlug(term); + + const selector = { + slug: new RegExp(slug, "i") + }; + + if (Array.isArray(excludeTags)) { + selector._id = { + $nin: excludeTags + }; + } + + const tags = Tags.find(selector).map((tag) => { + return { + label: tag.name + }; + }); + + return tags; } }; diff --git a/imports/plugins/core/ui-tagnav/client/index.js b/imports/plugins/core/ui-tagnav/client/index.js index 755d5335f9a..e69de29bb2d 100644 --- a/imports/plugins/core/ui-tagnav/client/index.js +++ b/imports/plugins/core/ui-tagnav/client/index.js @@ -1,8 +0,0 @@ -import "./components/tagGroup/tagGroup.html"; -import "./components/tagGroup/tagGroup.js"; - -import "./components/tagNav/tagNav.html"; -import "./components/tagNav/tagNav.js"; - -import "./components/tagTree/tagTree.html"; -import "./components/tagTree/tagTree.js"; diff --git a/imports/plugins/core/ui/client/components/button/button.jsx b/imports/plugins/core/ui/client/components/button/button.jsx index ba95e6a0d4f..163b5a6e742 100644 --- a/imports/plugins/core/ui/client/components/button/button.jsx +++ b/imports/plugins/core/ui/client/components/button/button.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import createFragment from "react-addons-create-fragment"; import classnames from "classnames/dedupe"; import Icon from "../icon/icon.jsx"; @@ -126,7 +127,7 @@ class Button extends Component { active, status, toggleOn, primary, bezelStyle, className, containerStyle, // Destructure these vars as they aren't valid as attributes on the HTML element button - iconAfter, label, i18nKeyTitle, i18nKeyLabel, i18nKeyTooltip, // eslint-disable-line no-unused-vars + iconAfter, label, i18nKeyTitle, i18nKeyLabel, i18nKeyTooltip, tabIndex, // eslint-disable-line no-unused-vars tooltip, icon, toggle, onIcon, eventAction, buttonType, // eslint-disable-line no-unused-vars toggleOnLabel, i18nKeyToggleOnLabel, tagName, onClick, onToggle, onValue, tooltipAttachment, // eslint-disable-line no-unused-vars @@ -230,6 +231,7 @@ Button.propTypes = { onValue: PropTypes.any, primary: PropTypes.bool, status: PropTypes.string, + tabIndex: PropTypes.string, tagName: PropTypes.string, title: PropTypes.string, toggle: PropTypes.bool, diff --git a/imports/plugins/core/ui/client/components/divider/divider.js b/imports/plugins/core/ui/client/components/divider/divider.js index df7a2ed0810..68f0e2a5f47 100644 --- a/imports/plugins/core/ui/client/components/divider/divider.js +++ b/imports/plugins/core/ui/client/components/divider/divider.js @@ -20,7 +20,7 @@ class Divider extends Component { if (label) { return ( -
+

@@ -40,6 +40,7 @@ class Divider extends Component { Divider.propTypes = { i18nKeyLabel: PropTypes.string, + id: PropTypes.string, label: PropTypes.string }; diff --git a/imports/plugins/core/ui/client/components/menu/dropDownMenu.js b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js index 3b9644eac37..257170d40f4 100644 --- a/imports/plugins/core/ui/client/components/menu/dropDownMenu.js +++ b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js @@ -1,4 +1,5 @@ -import React, { Children, Component, PropTypes } from "react"; +import React, { Children, Component } from "react"; +import PropTypes from "prop-types"; import { Button, Menu, @@ -10,13 +11,21 @@ class DropDownMenu extends Component { super(props); this.state = { - label: undefined + label: undefined, + isOpen: false }; } + handleDropdownToggle = () => { + this.setState({ + isOpen: !this.state.isOpen + }); + } + handleMenuItemChange = (event, value, menuItem) => { this.setState({ - label: menuItem.props.label || value + label: menuItem.props.label || value, + isOpen: false }); if (this.props.onChange) { @@ -53,8 +62,17 @@ class DropDownMenu extends Component { label={this.label} /> } + onClick={this.handleDropdownToggle} + isOpen={this.state.isOpen} + attachment={this.props.attachment} + targetAttachment={this.props.targetAttachment} > - + {this.props.children} @@ -63,12 +81,13 @@ class DropDownMenu extends Component { } DropDownMenu.propTypes = { + attachment: PropTypes.string, buttonElement: PropTypes.node, children: PropTypes.node, - isEnabled: PropTypes.bool, + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + menuStyle: PropTypes.object, onChange: PropTypes.func, - onPublishClick: PropTypes.func, - revisions: PropTypes.arrayOf(PropTypes.object), + targetAttachment: PropTypes.string, translation: PropTypes.shape({ lang: PropTypes.string }), diff --git a/imports/plugins/core/ui/client/components/menu/menu.js b/imports/plugins/core/ui/client/components/menu/menu.js index 0bcc68b16a2..cd9ea0d266b 100644 --- a/imports/plugins/core/ui/client/components/menu/menu.js +++ b/imports/plugins/core/ui/client/components/menu/menu.js @@ -1,4 +1,6 @@ -import React, { Children, Component, PropTypes } from "react"; +import React, { Children, Component } from "react"; +import PropTypes from "prop-types"; +import classnames from "classnames/dedupe"; class Menu extends Component { handleChange = (event, value, menuItem) => { @@ -11,12 +13,13 @@ class Menu extends Component { if (this.props.children) { return Children.map(this.props.children, (element) => { const newChild = React.cloneElement(element, { - onClick: this.handleChange, - active: element.props.value === this.props.value + onClick: this.handleChange }); - + const baseClassName = classnames({ + active: element.props.value === this.props.value + }, this.props.className); return ( -
  • {newChild}
  • +
  • {newChild}
  • ); }); } @@ -24,7 +27,7 @@ class Menu extends Component { render() { return ( -
    ) ); @@ -220,12 +244,14 @@ class Tag extends Component { }); return ( -
    -
    -
    ); } @@ -268,10 +294,10 @@ class Tag extends Component { * @return {JSX} tag component */ render() { - if (this.props.editable) { - return this.renderEditableTag(); - } else if (this.props.blank) { + if (this.props.blank) { return this.renderBlankEditableTag(); + } else if (this.props.editable) { + return this.renderEditableTag(); } return this.renderTag(); @@ -282,18 +308,22 @@ Tag.propTypes = { blank: PropTypes.bool, connectDragSource: PropTypes.func, connectDropTarget: PropTypes.func, + draggable: PropTypes.bool, editable: PropTypes.bool, fullWidth: PropTypes.bool, i18nKeyInputPlaceholder: PropTypes.string, index: PropTypes.number, inputPlaceholder: PropTypes.string, + isTagNav: PropTypes.bool, onClearSuggestions: PropTypes.func, onGetSuggestions: PropTypes.func, + onTagClick: PropTypes.func, onTagInputBlur: PropTypes.func, onTagMouseOut: PropTypes.func, onTagMouseOver: PropTypes.func, onTagRemove: PropTypes.func, onTagSave: PropTypes.func, + onTagSelect: PropTypes.func, onTagUpdate: PropTypes.func, parentTag: PropTypes.object, suggestions: PropTypes.arrayOf(PropTypes.object), diff --git a/imports/plugins/core/ui/client/components/tags/tags.jsx b/imports/plugins/core/ui/client/components/tags/tags.jsx index bd3b6df88e8..9b7564aec21 100644 --- a/imports/plugins/core/ui/client/components/tags/tags.jsx +++ b/imports/plugins/core/ui/client/components/tags/tags.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { PropTypes as ReactionPropTypes } from "/lib/api"; import { TagItem } from "./"; import classnames from "classnames"; @@ -62,46 +63,68 @@ class Tags extends Component { } }; + hasDropdownClassName = (tag) => { + if (this.props.hasDropdownClassName) { + return this.props.hasDropdownClassName(tag); + } + return ""; + } + + navbarSelectedClassName = (tag) => { + if (this.props.navbarSelectedClassName) { + return this.props.navbarSelectedClassName(tag); + } + return ""; + } + renderTags() { + const classes = (tag = {}) => classnames({ + "navbar-item": this.props.isTagNav, + [this.navbarSelectedClassName(tag)]: this.props.isTagNav, + [this.hasDropdownClassName(tag)]: this.props.isTagNav + }); + if (_.isArray(this.props.tags)) { - const tags = this.props.tags.map((tag, index) => { + const arrayProps = _.compact(this.props.tags); + const tags = arrayProps.map((tag, index) => { return ( - +
    + + {this.props.children} +
    ); }); // Render an blank tag for creating new tags if (this.props.editable && this.props.enableNewTagForm) { tags.push( - +
    + +
    ); } @@ -112,6 +135,14 @@ class Tags extends Component { } render() { + if (this.props.isTagNav) { + return ( +
    + {this.renderTags()} +
    + ); + } + const classes = classnames({ rui: true, tags: true, @@ -137,8 +168,13 @@ Tags.defaultProps = { // Prop Types Tags.propTypes = { + children: PropTypes.node, + draggable: PropTypes.bool, editable: PropTypes.bool, enableNewTagForm: PropTypes.bool, + hasDropdownClassName: PropTypes.func, + isTagNav: PropTypes.bool, + navbarSelectedClassName: PropTypes.func, newTag: PropTypes.object, onClearSuggestions: PropTypes.func, onGetSuggestions: PropTypes.func, @@ -154,7 +190,6 @@ Tags.propTypes = { parentTag: ReactionPropTypes.Tag, showBookmark: PropTypes.bool, suggestions: PropTypes.arrayOf(PropTypes.object), - tagProps: PropTypes.object, tags: ReactionPropTypes.arrayOfTags }; diff --git a/imports/plugins/core/ui/client/components/textfield/textfield.js b/imports/plugins/core/ui/client/components/textfield/textfield.js index 0569f5d5d50..628ee327386 100644 --- a/imports/plugins/core/ui/client/components/textfield/textfield.js +++ b/imports/plugins/core/ui/client/components/textfield/textfield.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import classnames from "classnames"; import TextareaAutosize from "react-textarea-autosize"; import { Translation } from "../translation"; @@ -72,6 +73,7 @@ class TextField extends Component { value={this.value} style={this.props.style} disabled={this.props.disabled} + id={this.props.id} /> ); } @@ -102,6 +104,7 @@ class TextField extends Component { value={this.value} style={this.props.style} disabled={this.props.disabled} + id={this.props.id} /> ); } @@ -182,6 +185,7 @@ TextField.propTypes = { i18nKeyHelpText: PropTypes.string, i18nKeyLabel: PropTypes.string, i18nKeyPlaceholder: PropTypes.string, + id: PropTypes.string, label: PropTypes.string, multiline: PropTypes.bool, name: PropTypes.string, diff --git a/imports/plugins/included/default-theme/client/styles/dropdowns.less b/imports/plugins/included/default-theme/client/styles/dropdowns.less index 658229f892d..1dd6bcebe3d 100644 --- a/imports/plugins/included/default-theme/client/styles/dropdowns.less +++ b/imports/plugins/included/default-theme/client/styles/dropdowns.less @@ -33,3 +33,9 @@ position: static; float: none; } + +.rui.menu.dropdown-menu { + margin-top: 10px; + max-height: 500; + overflow: "auto"; +} diff --git a/imports/plugins/included/default-theme/client/styles/navbar.less b/imports/plugins/included/default-theme/client/styles/navbar.less index b43d51b6e64..8886ac46db9 100644 --- a/imports/plugins/included/default-theme/client/styles/navbar.less +++ b/imports/plugins/included/default-theme/client/styles/navbar.less @@ -44,6 +44,10 @@ .rui.navbar .rui.tagnav { background: transparent; + + .tag-group { + display: inherit + } } .rui.navbar .rui.tagnav .edit-tag-button { @@ -89,8 +93,7 @@ .align-items(center); height: 100%; - padding: 0 @navbar-padding-vertical; - .padding-left(@navbar-padding-vertical); + padding: 0 14px; } .rui.navbar .languages, @@ -101,12 +104,6 @@ height: @navbar-height; } -.rui.navbar .languages:hover, -.rui.navbar .currencies:hover { - background-color: @navbar-default-toggle-icon-bar-bg; - border-color: @navbar-default-toggle-border-color; -} - .rui.navbar .languages .dropdown-menu { margin-top: 0px; min-width: 250px !important; @@ -135,20 +132,12 @@ html:not(.rtl) .rui.navbar .languages .dropdown-menu { } .rui.navbar .accounts { + .display(flex); + .align-items(center); .flex(0 0 auto); height: @navbar-height; } -.rui.navbar .accounts:hover { - background-color: @navbar-default-toggle-icon-bar-bg; - border-color: @navbar-default-toggle-border-color; -} - -.rui.navbar .accounts:active { - background-color: @navbar-default-toggle-icon-bar-bg; - border-color: @navbar-default-toggle-border-color; -} - .rui.navbar .accounts .dropdown-toggle { .display(flex); .align-items(center); @@ -262,3 +251,56 @@ html:not(.rtl) .rui.navbar .languages .dropdown-menu { .rui.navbar .search:hover button { outline: none; } + +.rui.navbar .accounts:hover { + background-color: @navbar-default-toggle-icon-bar-bg; + border-color: @navbar-default-toggle-border-color; +} + +.accounts-a-tag { + display: flex !important; + align-items: center; + text-decoration: none; + padding: 5px 0 !important; + min-height: 44px !important; + min-width: 220px; +} + +.accounts-a-tag:hover, +.accounts-a-tag:focus { + color: @brand-vivid-color !important; + text-decoration: none; + background-color: transparent !important; +} + +.accounts-li-tag { + cursor: pointer; + white-space: nowrap; + display: block; + border-bottom: 1px solid @black10; +} + +.accounts-li-tag:last-child { + border-bottom: none; +} + +.accounts-btn-tag { + padding: 6px 12px !important; + color: #fff !important; + background-color: @btn-primary-bg !important; + color: @btn-primary-color !important; + border-color: @btn-primary-bg !important; +} + +.accounts-btn-tag:hover { + background: darken(@btn-primary-bg, 5%) !important; +} + +.accounts-img-tag { + width: 30px; + height: 30px; + margin: -15px 5px; + border: 2px solid rgba(129, 122, 122, 0.62); + border-radius: 50%; + display: inline-block; +} diff --git a/imports/plugins/included/default-theme/client/styles/tagGroup.less b/imports/plugins/included/default-theme/client/styles/tagGroup.less index 3d1a3091b5d..6a568788d35 100644 --- a/imports/plugins/included/default-theme/client/styles/tagGroup.less +++ b/imports/plugins/included/default-theme/client/styles/tagGroup.less @@ -55,8 +55,10 @@ .rui.grouptag > .header .rui.tag.edit.create { text-transform: none; - input, button { - background-color: @tag-group-background-color; + input, button, .btn { + // added !important here because of un-needed inline style that gets added + // to react-autosuggest__container overriding the color. Couldn't find a better/faster approach + background-color: @tag-group-background-color !important; border-color: @tag-group-border-color; color: @tag-group-text-color; } diff --git a/imports/plugins/included/default-theme/client/styles/tagNav.less b/imports/plugins/included/default-theme/client/styles/tagNav.less index 64c1ca41621..1297aa589bc 100644 --- a/imports/plugins/included/default-theme/client/styles/tagNav.less +++ b/imports/plugins/included/default-theme/client/styles/tagNav.less @@ -250,6 +250,10 @@ color: @text-color; } +.navbar-item .btn.btn-drag-handle { + padding: 3px 6px; +} + .rui.tagnav.vertical .navbar-item > .rui.tag.link { display: block; height: auto; @@ -329,7 +333,7 @@ position: absolute; top: @navbar-height; left: 0; - z-index: @zindex-modal; + z-index: @zindex-dropdown; width: 100%; max-width: 100%; margin: 0; diff --git a/imports/plugins/included/notifications/client/styles/dropdown.css b/imports/plugins/included/notifications/client/styles/dropdown.css index f442dcea832..a25c693be35 100644 --- a/imports/plugins/included/notifications/client/styles/dropdown.css +++ b/imports/plugins/included/notifications/client/styles/dropdown.css @@ -313,9 +313,6 @@ a.notification:hover { .dropdown-notifications .dropdown-footer { background: #eeeeee; } -.notification-icon:hover { - background: #eeeeee !important; -} .notification-icon:after { position: absolute; content: attr(data-count); diff --git a/imports/plugins/included/notifications/client/styles/notifications.less b/imports/plugins/included/notifications/client/styles/notifications.less index e2e3b270cd4..675eb8fa3f8 100644 --- a/imports/plugins/included/notifications/client/styles/notifications.less +++ b/imports/plugins/included/notifications/client/styles/notifications.less @@ -20,8 +20,3 @@ .rui.navbar .notification-icon:after { background: none; } - -.rui.navbar .notification-icon:hover { - background-color: #4d4d4d; - border-color: #dddddd; -} \ No newline at end of file