diff --git a/.gitignore b/.gitignore index 2e66b4486e..7741507ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ web/.project _site .sass-cache .jekyll-metadata -*.lock \ No newline at end of file +*.lock +docs/developer-guide/reference/ diff --git a/package.json b/package.json index 9d22f11080..2c7a2bb269 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "babel-preset-es2015": "6.6.0", "babel-preset-react": "6.5.0", "babel-preset-stage-0": "6.5.0", + "copy-webpack-plugin": "4.0.1", "css-loader": "0.19.0", "docma": "1.5.1", "download-cli": "1.0.1", @@ -28,8 +29,8 @@ "glob": "7.1.1", "html-webpack-plugin": "2.17.0", "istanbul-instrumenter-loader": "2.0.0", - "jsdoc": "^3.4.3", - "jsdoc-jsx": "^0.1.0", + "jsdoc": "3.4.3", + "jsdoc-jsx": "0.1.0", "karma": "1.5.0", "karma-chrome-launcher": "2.0.0", "karma-cli": "1.0.1", @@ -48,6 +49,9 @@ "mocha": "2.4.5", "ncp": "2.0.0", "parallelshell": "1.2.0", + "postcss": "5.2.16", + "postcss-loader": "1.3.3", + "postcss-prefix-selector": "1.6.0", "raw-loader": "0.5.1", "react-addons-css-transition-group": "15.4.2", "react-addons-test-utils": "15.4.2", @@ -76,7 +80,7 @@ "axios": "0.11.1", "babel-polyfill": "6.8.0", "babel-standalone": "6.7.7", - "bootstrap": "3.3.6", + "bootstrap": "3.3.5", "canvas-to-blob": "0.0.0", "canvg-browser": "1.0.0", "classnames": "2.2.5", @@ -130,6 +134,7 @@ "react-select": "1.0.0-rc.1", "react-selectize": "2.0.3", "react-share": "1.8.0", + "react-side-effect": "1.1.0", "react-sidebar": "2.3.0", "react-sortable-items": "https://github.com/geosolutions-it/react-sortable-items/tarball/react15", "react-spinkit": "2.1.1", diff --git a/prod-webpack.config.js b/prod-webpack.config.js index 713c080e56..1b9efb377e 100644 --- a/prod-webpack.config.js +++ b/prod-webpack.config.js @@ -6,12 +6,24 @@ var DefinePlugin = require("webpack/lib/DefinePlugin"); var NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin"); const extractThemesPlugin = require('./themes.js').extractThemesPlugin; var assign = require('object-assign'); +var CopyWebpackPlugin = require('copy-webpack-plugin'); assign(webpackConfig.entry, require('./examples.js')); webpackConfig.plugins = [ + new CopyWebpackPlugin([ + { from: path.join(__dirname, 'node_modules', 'bootstrap', 'less'), to: path.join(__dirname, "web", "client", "dist", "bootstrap", "less") } + ]), new LoaderOptionsPlugin({ - debug: false + debug: false, + options: { + postcss: { + plugins: [ + require('postcss-prefix-selector')({prefix: '.ms2', exclude: ['.ms2']}) + ] + }, + context: __dirname + } }), new DefinePlugin({ "__DEVTOOLS__": false diff --git a/themes.js b/themes.js index 2002b8cc97..2d60d60196 100644 --- a/themes.js +++ b/themes.js @@ -9,8 +9,8 @@ const extractThemesPlugin = new ExtractTextPlugin({ const themeEntries = (() => { const globPath = path.join(__dirname, "web", "client", "themes", "*"); - var files = glob.sync(globPath); - return files.reduce((res, curr) => { + var files = glob.sync(globPath, {mark: true}); + return files.filter((f) => f.lastIndexOf('/') === f.length - 1).reduce((res, curr) => { var finalRes = res || {}; finalRes["themes/" + path.basename(curr, path.extname(curr))] = path.join(__dirname, "web", "client", "themes", `${path.basename(curr, path.extname(curr))}`, "theme.less"); return finalRes; diff --git a/web/client/components/app/StandardRouter.jsx b/web/client/components/app/StandardRouter.jsx index d47046cff3..17e27e8e51 100644 --- a/web/client/components/app/StandardRouter.jsx +++ b/web/client/components/app/StandardRouter.jsx @@ -14,17 +14,23 @@ const {Router, Route, hashHistory} = require('react-router'); const Localized = require('../I18N/Localized'); +const Theme = connect((state) => ({ + theme: state.theme && state.theme.selectedTheme && state.theme.selectedTheme.id +}))(require('../theme/Theme')); + const StandardRouter = React.createClass({ propTypes: { plugins: React.PropTypes.object, locale: React.PropTypes.object, - pages: React.PropTypes.array + pages: React.PropTypes.array, + className: React.PropTypes.string }, getDefaultProps() { return { plugins: {}, locale: {messages: {}, current: 'en-US'}, - pages: [] + pages: [], + className: "ms2 fill" }; }, renderPages() { @@ -40,7 +46,8 @@ const StandardRouter = React.createClass({ render() { return ( -
+
+ {this.renderPages()} diff --git a/web/client/components/app/__tests__/StandardRouter-test.jsx b/web/client/components/app/__tests__/StandardRouter-test.jsx index 7e4f49b37e..b6bc941c0e 100644 --- a/web/client/components/app/__tests__/StandardRouter-test.jsx +++ b/web/client/components/app/__tests__/StandardRouter-test.jsx @@ -48,7 +48,12 @@ describe('StandardApp', () => { }); it('creates a default router app', () => { - const app = ReactDOM.render(, document.getElementById("container")); + const store = { + dispatch: () => {}, + subscribe: () => {}, + getState: () => ({}) + }; + const app = ReactDOM.render(, document.getElementById("container")); expect(app).toExist(); }); diff --git a/web/client/components/theme/Theme.jsx b/web/client/components/theme/Theme.jsx new file mode 100644 index 0000000000..06dfc55d0b --- /dev/null +++ b/web/client/components/theme/Theme.jsx @@ -0,0 +1,46 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const withSideEffect = require('react-side-effect'); + +const reducePropsToState = (props) => { + const innermostProps = props[props.length - 1]; + if (innermostProps) { + return {theme: innermostProps.theme || 'default', themeElement: innermostProps.themeElement || 'theme_stylesheet'}; + } + return null; +}; + +const handleStateChangeOnClient = (themeCfg) => { + if (themeCfg) { + const link = document.getElementById(themeCfg.themeElement); + if (link && themeCfg.theme) { + const basePath = link.href && link.href.substring(0, link.href.lastIndexOf("/")); + link.setAttribute('href', basePath + "/" + themeCfg.theme + ".css"); + } + } +}; + +const Theme = React.createClass({ + propTypes: { + theme: React.PropTypes.string.isRequired + }, + getDefaultProps() { + return { + theme: 'default' + }; + }, + render() { + if (this.props.children) { + return React.Children.only(this.props.children); + } + return null; + } +}); + +module.exports = withSideEffect(reducePropsToState, handleStateChangeOnClient)(Theme); diff --git a/web/client/components/theme/ThemeSwitcher.jsx b/web/client/components/theme/ThemeSwitcher.jsx new file mode 100644 index 0000000000..631daaa962 --- /dev/null +++ b/web/client/components/theme/ThemeSwitcher.jsx @@ -0,0 +1,48 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const {head} = require('lodash'); +const {FormGroup, Label, FormControl} = require('react-bootstrap'); +const Message = require('../I18N/Message'); + +const ThemeSwitcher = React.createClass({ + propTypes: { + themes: React.PropTypes.array, + selectedTheme: React.PropTypes.object, + onThemeSelected: React.PropTypes.func, + style: React.PropTypes.object + }, + getDefaultProps() { + return { + onThemeSelected: () => {}, + style: { + width: "300px", + margin: "20px auto" + } + }; + }, + + onChangeTheme(themeId) { + const theme = head(this.props.themes.filter((t) => t.id === themeId)); + this.props.onThemeSelected(theme); + }, + render() { + return ( + + + this.onChangeTheme(e.target.value)}> + {this.props.themes.map( (t) => )} + + ); + } + }); + +module.exports = ThemeSwitcher; diff --git a/web/client/examples/3dviewer/index.html b/web/client/examples/3dviewer/index.html index fb68868d86..6e091c737e 100644 --- a/web/client/examples/3dviewer/index.html +++ b/web/client/examples/3dviewer/index.html @@ -18,7 +18,7 @@ } - +
diff --git a/web/client/examples/api/icons/icons.eot b/web/client/examples/api/icons/icons.eot new file mode 100644 index 0000000000..c7ead8ba9c Binary files /dev/null and b/web/client/examples/api/icons/icons.eot differ diff --git a/web/client/examples/api/icons/icons.svg b/web/client/examples/api/icons/icons.svg new file mode 100644 index 0000000000..589ffa8059 --- /dev/null +++ b/web/client/examples/api/icons/icons.svg @@ -0,0 +1,1003 @@ + + + + +Created by FontForge 20161003 at Mon Feb 13 15:08:14 2017 + By www-data +Copyright (c) 2017 by Chef Studio. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/client/examples/api/icons/icons.ttf b/web/client/examples/api/icons/icons.ttf new file mode 100644 index 0000000000..4041edc148 Binary files /dev/null and b/web/client/examples/api/icons/icons.ttf differ diff --git a/web/client/examples/api/icons/icons.woff b/web/client/examples/api/icons/icons.woff new file mode 100644 index 0000000000..6edf76bcb2 Binary files /dev/null and b/web/client/examples/api/icons/icons.woff differ diff --git a/web/client/examples/api/img/toggle.png b/web/client/examples/api/img/toggle.png new file mode 100644 index 0000000000..ce0edd8ff8 Binary files /dev/null and b/web/client/examples/api/img/toggle.png differ diff --git a/web/client/examples/api/img/toggle.svg b/web/client/examples/api/img/toggle.svg new file mode 100644 index 0000000000..75138bd163 --- /dev/null +++ b/web/client/examples/api/img/toggle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/client/examples/api/index.html b/web/client/examples/api/index.html index 32a56f689e..65a2a7164f 100644 --- a/web/client/examples/api/index.html +++ b/web/client/examples/api/index.html @@ -9,7 +9,7 @@ - + @@ -29,6 +29,7 @@ bottom: 100px; left: 100px; right: 600px; + overflow: hidden; } #help { position: absolute; diff --git a/web/client/examples/api/init.js b/web/client/examples/api/init.js index 778ff50ae0..d28e8fede2 100644 --- a/web/client/examples/api/init.js +++ b/web/client/examples/api/init.js @@ -32,6 +32,7 @@ function init() { plugins: pluginsCfg, initialState: cfg && cfg.state && { defaultState: cfg.state - } || null + } || null, + style: cfg && cfg.customStyle }); } diff --git a/web/client/examples/featuregrid/index.html b/web/client/examples/featuregrid/index.html index d15af5f1b5..98534043c9 100644 --- a/web/client/examples/featuregrid/index.html +++ b/web/client/examples/featuregrid/index.html @@ -20,7 +20,7 @@ } - +
diff --git a/web/client/examples/layertree/index.html b/web/client/examples/layertree/index.html index 51000f821d..0b89c8e9b9 100644 --- a/web/client/examples/layertree/index.html +++ b/web/client/examples/layertree/index.html @@ -342,7 +342,7 @@ } - +
diff --git a/web/client/examples/login/index.html b/web/client/examples/login/index.html index 8f716ee703..23472c689c 100644 --- a/web/client/examples/login/index.html +++ b/web/client/examples/login/index.html @@ -64,7 +64,7 @@ } - +
diff --git a/web/client/examples/mouseposition/index.html b/web/client/examples/mouseposition/index.html index 34b1843740..42685e7292 100644 --- a/web/client/examples/mouseposition/index.html +++ b/web/client/examples/mouseposition/index.html @@ -37,7 +37,7 @@ } - +
diff --git a/web/client/examples/myapp/index.html b/web/client/examples/myapp/index.html index 9496988b56..ecb543ea65 100644 --- a/web/client/examples/myapp/index.html +++ b/web/client/examples/myapp/index.html @@ -18,7 +18,7 @@ - +
diff --git a/web/client/examples/plugins/app.jsx b/web/client/examples/plugins/app.jsx index 701c4d191d..ac65e3486f 100644 --- a/web/client/examples/plugins/app.jsx +++ b/web/client/examples/plugins/app.jsx @@ -5,6 +5,7 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ + const startApp = () => { const React = require('react'); const ReactDOM = require('react-dom'); @@ -13,16 +14,29 @@ const startApp = () => { const ConfigUtils = require('../../utils/ConfigUtils'); const LocaleUtils = require('../../utils/LocaleUtils'); const PluginsUtils = require('../../utils/PluginsUtils'); + const ThemeUtils = require('../../utils/ThemeUtils'); const {changeBrowserProperties} = require('../../actions/browser'); const {loadMapConfig} = require('../../actions/config'); const {loadLocale} = require('../../actions/locale'); const {loadPrintCapabilities} = require('../../actions/print'); + const {selectTheme} = require('../../actions/theme'); const PluginsContainer = connect((state) => ({ pluginsState: state && state.controls || {} }))(require('../../components/plugins/PluginsContainer')); + const ThemeSwitcher = connect((state) => ({ + selectedTheme: state.theme && state.theme.selectedTheme || 'default', + themes: require('../../themes') + }), { + onThemeSelected: selectTheme + })(require('../../components/theme/ThemeSwitcher')); + + const Theme = connect((state) => ({ + theme: state.theme && state.theme.selectedTheme && state.theme.selectedTheme.id || 'default' + }))(require('../../components/theme/Theme')); + const {plugins} = require('./plugins'); let userPlugin; @@ -32,6 +46,7 @@ const startApp = () => { standard: ['Map', 'Toolbar'] }; + let customStyle = null; let userCfg = {}; const {Provider} = require('react-redux'); @@ -44,6 +59,7 @@ const startApp = () => { const assign = require('object-assign'); const codeSample = require("raw-loader!./sample.js.raw"); + const themeSample = require("raw-loader!./sample.less.raw"); let customReducers; @@ -95,6 +111,22 @@ const startApp = () => { callback(); }; + const applyStyle = (theme, callback) => { + if (theme) { + ThemeUtils.renderFromLess(theme, 'custom_theme', 'themes/default/', callback); + } else { + document.getElementById('custom_theme').innerText = ''; + if (callback) { + callback(); + } + } + }; + + const customTheme = (callback, theme) => { + customStyle = theme; + applyStyle(theme, callback); + }; + const customPlugin = (callback, code) => { /*eslint-disable */ const require = context; @@ -128,6 +160,10 @@ const startApp = () => { error: state.pluginsConfig && state.pluginsConfig.error }))(require('./components/PluginCreator')); + const ThemeCreator = connect((state) => ({ + error: state.pluginsConfig && state.pluginsConfig.error + }))(require('./components/ThemeCreator')); + const renderPlugins = (callback) => { return Object.keys(plugins).map((plugin) => { const pluginName = plugin.substring(0, plugin.length - 6); @@ -165,7 +201,8 @@ const startApp = () => { pluginsCfg, userCfg, mapType, - state: state + state: state, + customStyle })); callback(); }; @@ -177,10 +214,11 @@ const startApp = () => { pluginsCfg = obj.pluginsCfg; userCfg = obj.userCfg; mapType = obj.mapType || mapType; + customStyle = obj.customStyle || null; if (obj.state) { store.dispatch({type: 'LOADED_STATE', state: obj.state}); } - callback(); + applyStyle(customStyle, callback); } }; @@ -196,15 +234,28 @@ const startApp = () => {
Configure application plugins
- - - - - - - -
    + + + + + + + + + + + +
+
    + +
+
    + + +
+
    + {renderPlugins(renderPage)}
diff --git a/web/client/examples/plugins/assets/css/plugins.css b/web/client/examples/plugins/assets/css/plugins.css index 182ee734db..1ea8e4acc4 100644 --- a/web/client/examples/plugins/assets/css/plugins.css +++ b/web/client/examples/plugins/assets/css/plugins.css @@ -76,3 +76,15 @@ html, body, #container, #map { .checkbox { padding-left: 5px; } + +.theme-switcher span { + display: none; +} + +#plugins-list ul { + margin-bottom: 10px; +} + +#plugins-list .form-group { + padding-left: 0; +} diff --git a/web/client/examples/plugins/components/PluginCreator.jsx b/web/client/examples/plugins/components/PluginCreator.jsx index 29ad2d677f..02a9a9e7ad 100644 --- a/web/client/examples/plugins/components/PluginCreator.jsx +++ b/web/client/examples/plugins/components/PluginCreator.jsx @@ -42,7 +42,7 @@ const PluginCreator = React.createClass({ componentWillReceiveProps(newProps) { if (newProps.pluginCode !== this.props.pluginCode) { this.setState({ - code: newProps.pluginConfig + code: newProps.pluginCode }); } }, diff --git a/web/client/examples/plugins/components/SaveAndLoad.jsx b/web/client/examples/plugins/components/SaveAndLoad.jsx index 657c2fc7f3..b07b4d3af7 100644 --- a/web/client/examples/plugins/components/SaveAndLoad.jsx +++ b/web/client/examples/plugins/components/SaveAndLoad.jsx @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ const React = require('react'); -const {FormControl, Button} = require('react-bootstrap'); +const {FormControl, FormGroup, Button} = require('react-bootstrap'); const SaveButton = React.createClass({ @@ -40,13 +40,17 @@ const SaveButton = React.createClass({ render() { const embedded = (Load in embedded version!); return (
- - - - {(this.state.loadname !== '') ? embedded : } - - {this.renderSaved()} - + + + + + + + {(this.state.loadname !== '') ? embedded : } + + {this.renderSaved()} + +
); }, load() { diff --git a/web/client/examples/plugins/components/ThemeCreator.jsx b/web/client/examples/plugins/components/ThemeCreator.jsx new file mode 100644 index 0000000000..685204ac15 --- /dev/null +++ b/web/client/examples/plugins/components/ThemeCreator.jsx @@ -0,0 +1,92 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); + +const {Button, Glyphicon, Modal, FormGroup, Checkbox} = require('react-bootstrap'); + +const Codemirror = require('react-codemirror'); + + +require('codemirror/lib/codemirror.css'); + +require('codemirror/mode/css/css'); + +const ThemeCreator = React.createClass({ + propTypes: { + themeCode: React.PropTypes.string, + error: React.PropTypes.string, + onApplyTheme: React.PropTypes.func + }, + getDefaultProps() { + return { + themeCode: '', + onApplyTheme: () => {} + }; + }, + getInitialState() { + return { + code: "", + configVisible: false + }; + }, + componentWillMount() { + this.setState({ + code: this.props.themeCode + }); + }, + componentWillReceiveProps(newProps) { + if (newProps.themeCode !== this.props.themeCode) { + this.setState({ + code: newProps.themeCode + }); + } + }, + render() { + return (
  • + + + + Live edit your theme + + + { + this.setState({ + configVisible: false + }); + }}> + + Live edit your own theme + + + + +
    {this.props.error}
    +
    +
    +
  • ); + }, + updateCode(newCode) { + this.setState({ + code: newCode + }); + }, + applyCode() { + this.props.onApplyTheme(this.state.code); + }, + toggleCfg() { + this.setState({configVisible: !this.state.configVisible}); + } +}); + +module.exports = ThemeCreator; diff --git a/web/client/examples/plugins/icons/icons.eot b/web/client/examples/plugins/icons/icons.eot new file mode 100644 index 0000000000..c7ead8ba9c Binary files /dev/null and b/web/client/examples/plugins/icons/icons.eot differ diff --git a/web/client/examples/plugins/icons/icons.svg b/web/client/examples/plugins/icons/icons.svg new file mode 100644 index 0000000000..589ffa8059 --- /dev/null +++ b/web/client/examples/plugins/icons/icons.svg @@ -0,0 +1,1003 @@ + + + + +Created by FontForge 20161003 at Mon Feb 13 15:08:14 2017 + By www-data +Copyright (c) 2017 by Chef Studio. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/client/examples/plugins/icons/icons.ttf b/web/client/examples/plugins/icons/icons.ttf new file mode 100644 index 0000000000..4041edc148 Binary files /dev/null and b/web/client/examples/plugins/icons/icons.ttf differ diff --git a/web/client/examples/plugins/icons/icons.woff b/web/client/examples/plugins/icons/icons.woff new file mode 100644 index 0000000000..6edf76bcb2 Binary files /dev/null and b/web/client/examples/plugins/icons/icons.woff differ diff --git a/web/client/examples/plugins/img/toggle.png b/web/client/examples/plugins/img/toggle.png new file mode 100644 index 0000000000..ce0edd8ff8 Binary files /dev/null and b/web/client/examples/plugins/img/toggle.png differ diff --git a/web/client/examples/plugins/img/toggle.svg b/web/client/examples/plugins/img/toggle.svg new file mode 100644 index 0000000000..75138bd163 --- /dev/null +++ b/web/client/examples/plugins/img/toggle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/client/examples/plugins/index.html b/web/client/examples/plugins/index.html index 5a0f959c6d..78c22960a3 100644 --- a/web/client/examples/plugins/index.html +++ b/web/client/examples/plugins/index.html @@ -8,8 +8,7 @@ - - + @@ -17,8 +16,9 @@ + - +
    diff --git a/web/client/examples/plugins/sample.less.raw b/web/client/examples/plugins/sample.less.raw new file mode 100644 index 0000000000..4a4c1d2d84 --- /dev/null +++ b/web/client/examples/plugins/sample.less.raw @@ -0,0 +1,76 @@ +@import "../../themes/default/base.less"; +@import "../../themes/default/variables.less"; + +.ms2 { + @import "../../themes/default/icons.less"; + @import "../../themes/default/bootstrap-theme.less"; + @import "../../themes/default/ms2-theme.less"; +} + +@ms2-color-background: #ffffff; +@ms2-color-code: #c7254e; + +@ms2-color-text-primary: #ffffff; +@ms2-color-text-placeholder: #999999; +@ms2-color-text: #333333; + +@ms2-color-disabled: #5C9FB4; +@ms2-color-text-disabled: #ffffff; + +@ms2-color-primary: #078aa3; +@ms2-color-info: #5a9aab; +@ms2-color-success: #398439; +@ms2-color-warning: #ebbc35; +@ms2-color-danger: #bb4940; + +@ms2-color-primary-hover: lighten(@ms2-color-primary, 10%); +@ms2-color-info-hover: lighten(@ms2-color-info, 10%); +@ms2-color-success-hover: lighten(@ms2-color-success, 10%); +@ms2-color-warning-hover: lighten(@ms2-color-warning, 10%); +@ms2-color-danger-hover: lighten(@ms2-color-danger, 10%); + +@ms2-color-primary-light: lighten(@ms2-color-primary, 40%); +@ms2-color-info-light: lighten(@ms2-color-info, 40%); +@ms2-color-success-light: lighten(@ms2-color-success, 40%); +@ms2-color-warning-light: lighten(@ms2-color-warning, 40%); +@ms2-color-danger-light: lighten(@ms2-color-danger, 40%); + +@ms2-color-primary-active: darken(@ms2-color-primary, 20%); + +@ms2-color-shade: #555555; +@ms2-color-shade-darker: #222222; +@ms2-color-shade-dark: #333333; +@ms2-color-shade-light: #999999; +@ms2-color-shade-lighter: #eeeeee; + +@font-family-base: 'Raleway', sans-serif; +@font-size-base: 14px; +@font-size-large: ceil(@font-size-base * 1.25); +@font-size-small: ceil(@font-size-base * 0.85); +@font-size-h1: floor((@font-size-base * 1.9)); +@font-size-h2: floor((@font-size-base * 1.7)); +@font-size-h3: floor((@font-size-base * 1.5)); +@font-size-h4: floor((@font-size-base * 1.25)); +@font-size-h5: @font-size-base; +@font-size-h6: floor((@font-size-base * 0.85)); +@line-height-computed: floor(@font-size-base * 1.428571429); + +@btn-font-weight: normal; +@icon-margin-ratio: 2; +@icon-resize-ratio: 1.5; + +@icon-size: 26px; +@padding-left-square: floor(@icon-size/@icon-margin-ratio); +@square-btn-size: @padding-left-square * 2 + @icon-size; + +@icon-size-md: floor(@icon-size / @icon-resize-ratio); +@padding-left-square-md: floor(@icon-size-md / @icon-margin-ratio); +@square-btn-medium-size: @padding-left-square-md * 2 + @icon-size-md; + +@icon-size-sm: floor(@icon-size-md / @icon-resize-ratio); +@padding-left-square-sm: floor(@icon-size-sm / @icon-margin-ratio); +@square-btn-small-size: @padding-left-square-sm * 2 + @icon-size-sm; + +@grid-icon-size: 18px; +@grid-btn-padding-left: 6px; +@grid-btn-size: @grid-btn-padding-left * 2 + @grid-icon-size; diff --git a/web/client/examples/plugins/store.js b/web/client/examples/plugins/store.js index e1ae17bf41..0685bed27b 100644 --- a/web/client/examples/plugins/store.js +++ b/web/client/examples/plugins/store.js @@ -20,6 +20,7 @@ module.exports = (plugins, custom) => { const allReducers = combineReducers(plugins, { locale: require('../../reducers/locale'), browser: require('../../reducers/browser'), + theme: require('../../reducers/theme'), map: () => {return null; }, mapInitialConfig: () => {return null; }, layers: () => {return null; }, diff --git a/web/client/examples/print/index.html b/web/client/examples/print/index.html index 5b1b17cd5d..faead1dc7b 100644 --- a/web/client/examples/print/index.html +++ b/web/client/examples/print/index.html @@ -68,7 +68,7 @@ } - +
    diff --git a/web/client/examples/queryform/index.html b/web/client/examples/queryform/index.html index fe17317252..f438faae4d 100644 --- a/web/client/examples/queryform/index.html +++ b/web/client/examples/queryform/index.html @@ -30,7 +30,7 @@ } - +
    diff --git a/web/client/examples/rasterstyler/index.html b/web/client/examples/rasterstyler/index.html index 268dfa9010..cbbf4d7269 100644 --- a/web/client/examples/rasterstyler/index.html +++ b/web/client/examples/rasterstyler/index.html @@ -16,10 +16,8 @@ - +
    - - diff --git a/web/client/examples/scalebar/index.html b/web/client/examples/scalebar/index.html index 06f4e54756..09d0fec72d 100644 --- a/web/client/examples/scalebar/index.html +++ b/web/client/examples/scalebar/index.html @@ -43,7 +43,7 @@ } - +
    diff --git a/web/client/examples/styler/index.html b/web/client/examples/styler/index.html index eee6b5b290..50f95e3540 100644 --- a/web/client/examples/styler/index.html +++ b/web/client/examples/styler/index.html @@ -18,7 +18,7 @@ - +
    diff --git a/web/client/index.html b/web/client/index.html index 44b5c7b66b..df1007c487 100644 --- a/web/client/index.html +++ b/web/client/index.html @@ -9,7 +9,6 @@ - @@ -19,7 +18,7 @@ - +
    diff --git a/web/client/jsapi/MapStore2.js b/web/client/jsapi/MapStore2.js index 3525e77d32..d7671631f0 100644 --- a/web/client/jsapi/MapStore2.js +++ b/web/client/jsapi/MapStore2.js @@ -16,6 +16,8 @@ const {configureMap} = require('../actions/config'); const url = require('url'); +const ThemeUtils = require('../utils/ThemeUtils'); + require('./mapstore2.css'); const defaultConfig = { @@ -197,6 +199,7 @@ const MapStore2 = { const embedded = require('../containers/Embedded'); const {initialState, storeOpts} = options; + const pluginsDef = require('./plugins'); const pages = [{ name: "embedded", @@ -222,8 +225,17 @@ const MapStore2 = { appComponent: StandardRouter, printingEnabled: false }; - - ReactDOM.render(, document.getElementById(container)); + const className = options.className || 'ms2'; + if (options.style) { + let dom = document.getElementById('custom_theme'); + if (!dom) { + dom = document.createElement('style'); + dom.id = 'custom_theme'; + document.head.appendChild(dom); + } + ThemeUtils.renderFromLess(options.style, 'custom_theme', 'themes/default/'); + } + ReactDOM.render(, document.getElementById(container)); }, buildPluginsCfg, getMapNameFromRequest, diff --git a/web/client/plugins/ThemeSwitcher.jsx b/web/client/plugins/ThemeSwitcher.jsx index 833f2cf3c4..ac1c12ce52 100644 --- a/web/client/plugins/ThemeSwitcher.jsx +++ b/web/client/plugins/ThemeSwitcher.jsx @@ -5,62 +5,17 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -const React = require('react'); const {connect} = require('react-redux'); -const Message = require('../components/I18N/Message'); -const {FormGroup, FormControl, Label} = require('react-bootstrap'); const {selectTheme} = require('../actions/theme'); -const ThemeSwitcher = React.createClass({ - propTypes: { - themes: React.PropTypes.array, - selectedTheme: React.PropTypes.object, - defaultSelectedTheme: React.PropTypes.string, - onThemeSelected: React.PropTypes.func - }, - getDefaultProps() { - return { - onThemeSelected: () => {}, - themes: [{ - id: "default" - }, { - id: "console" - }, { - id: "wasabi" - }, { - id: "dark" - }, { - id: "geosolutions" - }], - defaultSelectedTheme: "default" - }; - }, - onChangeTheme(themeId) { - let theme = this.props.themes.reduce((prev, curr) => curr.id === themeId ? curr : prev); - let link = document.getElementById('theme_stylesheet'); - if (link) { - let basePath = link.href && link.href.substring(0, link.href.lastIndexOf("/")); - link.setAttribute('href', basePath + "/" + theme.id + ".css"); - } - this.props.onThemeSelected(theme); - }, - render() { - return ( - - this.onChangeTheme(e.target.value)}> - {this.props.themes.map( (t) => )} - - ); - } -}); +const themes = require('../themes'); const ThemeSwitcherPlugin = connect((s) => ({ - selectedTheme: s && s.theme && s.theme.selectedTheme + selectedTheme: (s && s.theme && s.theme.selectedTheme) || themes[0], + themes }), { onThemeSelected: selectTheme -})(ThemeSwitcher); +})(require('../components/theme/ThemeSwitcher')); module.exports = { ThemeSwitcherPlugin: ThemeSwitcherPlugin, diff --git a/web/client/reducers/theme.js b/web/client/reducers/theme.js index 6d128e7d7c..4a042b4fbc 100644 --- a/web/client/reducers/theme.js +++ b/web/client/reducers/theme.js @@ -12,9 +12,9 @@ const assign = require('object-assign'); function controls(state = {}, action) { switch (action.type) { case THEME_SELECTED: - return assign({}, state, { - selectedTheme: action.theme - }); + return assign({}, state, { + selectedTheme: action.theme + }); default: return state; } diff --git a/web/client/stores/StandardStore.js b/web/client/stores/StandardStore.js index 90f07dfe7b..e87a1e6c78 100644 --- a/web/client/stores/StandardStore.js +++ b/web/client/stores/StandardStore.js @@ -31,6 +31,7 @@ module.exports = (initialState = {defaultState: {}, mobile: {}}, appReducers = { locale: require('../reducers/locale'), browser: require('../reducers/browser'), controls: require('../reducers/controls'), + theme: require('../reducers/theme'), help: require('../reducers/help'), map: () => {return null; }, mapInitialConfig: () => {return null; }, diff --git a/web/client/themes/console/theme.less b/web/client/themes/console/theme.less index a2127ee51a..abdaddee6a 100644 --- a/web/client/themes/console/theme.less +++ b/web/client/themes/console/theme.less @@ -1,4 +1,4 @@ - +@import "../default/base.less"; @import "../default/icons.less"; @import "../default/bootstrap-theme.less"; @import "../default/ms2-theme.less"; diff --git a/web/client/themes/dark/theme.less b/web/client/themes/dark/theme.less index 868600e7a8..abdaddee6a 100644 --- a/web/client/themes/dark/theme.less +++ b/web/client/themes/dark/theme.less @@ -1,3 +1,4 @@ +@import "../default/base.less"; @import "../default/icons.less"; @import "../default/bootstrap-theme.less"; @import "../default/ms2-theme.less"; diff --git a/web/client/themes/default/base.less b/web/client/themes/default/base.less new file mode 100644 index 0000000000..da85eb7b11 --- /dev/null +++ b/web/client/themes/default/base.less @@ -0,0 +1,13 @@ +.ms2 { + font-family: @font-family-base; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + height:100%; + width:100%; + font-size: @font-size-base; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + line-height: 1.428571429; + color: @ms2-color-text; + background-color: @ms2-color-background; +} diff --git a/web/client/themes/default/bootstrap-theme.less b/web/client/themes/default/bootstrap-theme.less index d6812f14d3..1d49574a30 100644 --- a/web/client/themes/default/bootstrap-theme.less +++ b/web/client/themes/default/bootstrap-theme.less @@ -1,11 +1,5 @@ -html,body { - font-family: @font-family-base; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - height:100%; - width:100%; -} +@import "~bootstrap/less/bootstrap"; +@import "~bootstrap/less/theme"; .shadow{ -webkit-box-shadow: -1px 1px 5px 1px rgba(94,94,94,1); @@ -274,16 +268,7 @@ th { -moz-box-sizing: border-box; box-sizing: border-box; } -html { - font-size: 62.5%; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -body { - font-size: @font-size-base; - line-height: 1.428571429; - color: @ms2-color-text; - background-color: @ms2-color-background; -} + input, button, select, diff --git a/web/client/themes/default/icons.less b/web/client/themes/default/icons.less index 2452ef45a2..a65143c72c 100644 --- a/web/client/themes/default/icons.less +++ b/web/client/themes/default/icons.less @@ -2,11 +2,11 @@ @font-face { font-family: "mapstore2"; - src:url("../default/icons/icons.eot"); - src:url("../default/icons/icons.eot?#iefix") format("embedded-opentype"), - url("../default/icons/icons.woff") format("woff"), - url("../default/icons/icons.ttf") format("truetype"), - url("../default/icons/icons.svg#mapstore2") format("svg"); + src:url("icons/icons.eot"); + src:url("icons/icons.eot?#iefix") format("embedded-opentype"), + url("icons/icons.woff") format("woff"), + url("icons/icons.ttf") format("truetype"), + url("icons/icons.svg#mapstore2") format("svg"); font-weight: normal; font-style: normal; } diff --git a/web/client/themes/default/ms2-theme.less b/web/client/themes/default/ms2-theme.less index 4407527b30..3c3e6eee53 100644 --- a/web/client/themes/default/ms2-theme.less +++ b/web/client/themes/default/ms2-theme.less @@ -391,7 +391,7 @@ div#mapstore-globalspinner { } .leaflet-control-minimap-toggle-display { - background-image: url("../default/img/toggle.svg") !important; + background-image: url("img/toggle.svg") !important; } .leaflet-oldie .leaflet-control-minimap-toggle-display { diff --git a/web/client/themes/default/theme.less b/web/client/themes/default/theme.less index 7380c44c4d..aa6c1a9d91 100644 --- a/web/client/themes/default/theme.less +++ b/web/client/themes/default/theme.less @@ -1,5 +1,5 @@ -// For LESS file includes, +@import "variables.less"; +@import "base.less"; @import "icons.less"; @import "bootstrap-theme.less"; @import "ms2-theme.less"; -@import "variables.less"; diff --git a/web/client/themes/geosolutions/theme.less b/web/client/themes/geosolutions/theme.less index 868600e7a8..abdaddee6a 100644 --- a/web/client/themes/geosolutions/theme.less +++ b/web/client/themes/geosolutions/theme.less @@ -1,3 +1,4 @@ +@import "../default/base.less"; @import "../default/icons.less"; @import "../default/bootstrap-theme.less"; @import "../default/ms2-theme.less"; diff --git a/web/client/themes/index.js b/web/client/themes/index.js new file mode 100644 index 0000000000..3b94b8ca70 --- /dev/null +++ b/web/client/themes/index.js @@ -0,0 +1,11 @@ +module.exports = [{ + id: "default" + }, { + id: "console" + }, { + id: "wasabi" + }, { + id: "dark" + }, { + id: "geosolutions" +}]; diff --git a/web/client/themes/wasabi/theme.less b/web/client/themes/wasabi/theme.less index 868600e7a8..abdaddee6a 100644 --- a/web/client/themes/wasabi/theme.less +++ b/web/client/themes/wasabi/theme.less @@ -1,3 +1,4 @@ +@import "../default/base.less"; @import "../default/icons.less"; @import "../default/bootstrap-theme.less"; @import "../default/ms2-theme.less"; diff --git a/web/client/utils/ThemeUtils.js b/web/client/utils/ThemeUtils.js new file mode 100644 index 0000000000..458a6a8e10 --- /dev/null +++ b/web/client/utils/ThemeUtils.js @@ -0,0 +1,44 @@ +function LessNodeResolve(options) { + this.options = options; +} + +function NodeProcessor(options) { + this.options = options || {}; +} + +NodeProcessor.prototype = { + process: function(src, extra) { + const basePath = extra.fileInfo.currentDirectory.replace(this.options.path, ''); + return src.replace(/\"~(.*)\"/g, '"' + basePath + 'dist/$1"'); + } +}; + +LessNodeResolve.prototype = { + install: function(less, pluginManager) { + pluginManager.addPreProcessor(new NodeProcessor(this.options)); + }, + printUsage: function() { + // TODO + }, + setOptions: function(options) { + this.options = options; + }, + minVersion: [2, 4, 0] +}; + +const less = require('less'); + +module.exports = { + renderFromLess: (theme, container, path, callback) => { + less.render(theme, { + plugins: [new LessNodeResolve({path: path})], + filename: 'custom.theme.less', + compress: true + }, (e, output) => { + document.getElementById(container).innerText = output.css; + if (callback) { + callback(); + } + }); + } +}; diff --git a/web/pom.xml b/web/pom.xml index b895de8018..1050004060 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -104,6 +104,7 @@ **/*.html **/*.json **/img/* + **/*.less diff --git a/webpack.config.js b/webpack.config.js index effa737849..f3925c122f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,8 @@ var DefinePlugin = require("webpack/lib/DefinePlugin"); var LoaderOptionsPlugin = require("webpack/lib/LoaderOptionsPlugin"); var NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin"); var NoEmitOnErrorsPlugin = require("webpack/lib/NoEmitOnErrorsPlugin"); +var CopyWebpackPlugin = require('copy-webpack-plugin'); + const assign = require('object-assign'); const themeEntries = require('./themes.js').themeEntries; const extractThemesPlugin = require('./themes.js').extractThemesPlugin; @@ -23,8 +25,19 @@ module.exports = { filename: "[name].js" }, plugins: [ + new CopyWebpackPlugin([ + { from: path.join(__dirname, 'node_modules', 'bootstrap', 'less'), to: path.join(__dirname, "web", "client", "dist", "bootstrap", "less") } + ]), new LoaderOptionsPlugin({ - debug: true + debug: true, + options: { + postcss: { + plugins: [ + require('postcss-prefix-selector')({prefix: '.ms2', exclude: ['.ms2']}) + ] + }, + context: __dirname + } }), new DefinePlugin({ "__DEVTOOLS__": true, @@ -49,6 +62,8 @@ module.exports = { loader: 'style-loader' }, { loader: 'css-loader' + }, { + loader: 'postcss-loader' }] }, { @@ -66,7 +81,7 @@ module.exports = { test: /themes[\\\/]?.+\.less$/, use: extractThemesPlugin.extract({ fallback: 'style-loader', - use: ['css-loader', 'less-loader'] + use: ['css-loader', 'postcss-loader', 'less-loader'] }) }, {