mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-19 04:31:11 +00:00
First version of the explorer on top of admin API
This commit is contained in:
parent
ded36c5634
commit
d675807cf8
43 changed files with 1888 additions and 174 deletions
24
.eslintrc
24
.eslintrc
|
|
@ -1,25 +1,3 @@
|
|||
{
|
||||
"extends": "airbnb",
|
||||
|
||||
"rules": {
|
||||
"indent": [2, 2],
|
||||
"max-len": [1, 120, 4, {"ignoreUrls": true}],
|
||||
"id-length": [1, {"min": 2, "exceptions": ["x", "y", "e", "i", "j", "k", "d", "n", "_", "$"]}],
|
||||
"object-shorthand": [2, "methods"],
|
||||
"no-new": [1],
|
||||
"comma-dangle": [0],
|
||||
"no-multi-spaces": [0],
|
||||
"prefer-template": [0],
|
||||
"no-var": [0],
|
||||
"prefer-arrow-callback": [1],
|
||||
"no-undef": [1],
|
||||
"no-unused-vars": [1],
|
||||
"no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],
|
||||
"react/sort-comp": [0],
|
||||
"react/jsx-boolean-value": [0],
|
||||
"react/jsx-no-bind": [0],
|
||||
"react/prefer-es6-class": [0, 'never'],
|
||||
"react/jsx-indent-props": [2, 4],
|
||||
"jsx-quotes": [1, "prefer-double"]
|
||||
}
|
||||
"extends": "wagtail"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ var TEMPLATES = path.join(__dirname, '..', '..', 'template');
|
|||
|
||||
var files = [
|
||||
{
|
||||
name: 'index.js',
|
||||
template: 'component.mst'
|
||||
name: 'component.js',
|
||||
template: 'component.mst',
|
||||
suffix: '.js',
|
||||
},
|
||||
{
|
||||
name: 'style.scss',
|
||||
|
|
@ -16,6 +17,11 @@ var files = [
|
|||
{
|
||||
name: 'README.md',
|
||||
template: 'README.mst'
|
||||
},
|
||||
{
|
||||
name: 'component.test.js',
|
||||
template: 'component.test.mst',
|
||||
suffix: '.test.js',
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -47,7 +53,7 @@ function write(name, data) {
|
|||
// Write files!
|
||||
// =============================================================================
|
||||
function run(argv) {
|
||||
var name = argv.name;
|
||||
var name = argv.name[0].toUpperCase() + argv.name.substring(1);
|
||||
var slug = slugify(name);
|
||||
var directory = path.join(argv.dir, slug);
|
||||
|
||||
|
|
@ -59,8 +65,9 @@ function run(argv) {
|
|||
}
|
||||
|
||||
files.forEach(function(file) {
|
||||
var fileName = file.suffix ? name + file.suffix : file.name;
|
||||
var template = fs.readFileSync(path.join(TEMPLATES, file.template), 'utf8');
|
||||
var newPath = path.join(directory, file.name);
|
||||
var newPath = path.join(directory, fileName);
|
||||
var context = {
|
||||
name: name,
|
||||
slug: slug
|
||||
|
|
|
|||
126
client/src/components/explorer/Explorer.js
Normal file
126
client/src/components/explorer/Explorer.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import CSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import * as actions from './actions';
|
||||
import { EXPLORER_ANIM_DURATION } from 'config';
|
||||
import ExplorerPanel from './ExplorerPanel';
|
||||
|
||||
|
||||
class Explorer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._init = this._init.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.defaultPage) {
|
||||
this.props.setDefaultPage(this.props.defaultPage);
|
||||
}
|
||||
}
|
||||
|
||||
_init(id) {
|
||||
if (this.props.page && this.props.page.isLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onShow(this.props.page ? this.props.page : this.props.defaultPage);
|
||||
}
|
||||
|
||||
_getPage() {
|
||||
let { nodes, depth, path } = this.props;
|
||||
let id = path[path.length - 1];
|
||||
return nodes[id];
|
||||
}
|
||||
|
||||
render() {
|
||||
let { visible, depth, nodes, path, pageTypes, items, type, filter, fetching, resolved } = this.props;
|
||||
let page = this._getPage();
|
||||
|
||||
const explorerProps = {
|
||||
path,
|
||||
pageTypes,
|
||||
page,
|
||||
type,
|
||||
fetching,
|
||||
filter,
|
||||
nodes,
|
||||
resolved,
|
||||
ref: 'explorer',
|
||||
left: this.props.left,
|
||||
top: this.props.top,
|
||||
onPop: this.props.onPop,
|
||||
onItemClick: this.props.onItemClick,
|
||||
onClose: this.props.onClose,
|
||||
transport: this.props.transport,
|
||||
onFilter: this.props.onFilter,
|
||||
getChildren: this.props.getChildren,
|
||||
loadItemWithChildren: this.props.loadItemWithChildren,
|
||||
pushPage: this.props.pushPage,
|
||||
init: this._init
|
||||
}
|
||||
|
||||
const transProps = {
|
||||
component: 'div',
|
||||
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionName: 'explorer-toggle'
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup {...transProps}>
|
||||
{ visible ? <ExplorerPanel {...explorerProps} /> : null }
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Explorer.propTypes = {
|
||||
onPageSelect: PropTypes.func,
|
||||
initialPath: PropTypes.string,
|
||||
apiPath: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
position: PropTypes.object,
|
||||
page: PropTypes.number,
|
||||
defaultPage: PropTypes.number,
|
||||
};
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Connector
|
||||
// =============================================================================
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
visible: state.explorer.isVisible,
|
||||
page: state.explorer.currentPage,
|
||||
depth: state.explorer.depth,
|
||||
loading: state.explorer.isLoading,
|
||||
fetching: state.explorer.isFetching,
|
||||
resolved: state.explorer.isResolved,
|
||||
path: state.explorer.path,
|
||||
pageTypes: state.explorer.pageTypes,
|
||||
// page: state.explorer.page
|
||||
// indexes: state.entities.indexes,
|
||||
nodes: state.nodes,
|
||||
animation: state.explorer.animation,
|
||||
filter: state.explorer.filter,
|
||||
transport: state.transport
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
setDefaultPage: (id) => { dispatch(actions.setDefaultPage(id)) },
|
||||
getChildren: (id) => { dispatch(actions.fetchChildren(id)) },
|
||||
onShow: (id) => { dispatch(actions.fetchRoot()) },
|
||||
onFilter: (filter) => { dispatch(actions.setFilter(filter)) },
|
||||
loadItemWithChildren: (id) => { dispatch(actions.fetchPage(id)) },
|
||||
pushPage: (id) => { dispatch(actions.pushPage(id)) },
|
||||
onPop: () => { dispatch(actions.popPage()) },
|
||||
onClose: () => { dispatch(actions.toggleExplorer()) }
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Explorer);
|
||||
7
client/src/components/explorer/ExplorerEmpty.js
Normal file
7
client/src/components/explorer/ExplorerEmpty.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
const ExplorerEmpty = () => (
|
||||
<div className="c-explorer__placeholder">No results</div>
|
||||
);
|
||||
|
||||
export default ExplorerEmpty;
|
||||
81
client/src/components/explorer/ExplorerHeader.js
Normal file
81
client/src/components/explorer/ExplorerHeader.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React, { Component } from 'react';
|
||||
import CSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import { EXPLORER_ANIM_DURATION, EXPLORER_FILTERS } from 'config';
|
||||
|
||||
import Icon from 'components/icon/Icon';
|
||||
import Filter from './Filter';
|
||||
|
||||
class ExplorerHeader extends Component {
|
||||
|
||||
constructor(p) {
|
||||
super(p)
|
||||
this.onFilter = this.onFilter.bind(this);
|
||||
}
|
||||
|
||||
_getBackBtn() {
|
||||
let { onPop } = this.props;
|
||||
|
||||
return (
|
||||
<span className='c-explorer__back' onClick={onPop}>
|
||||
<Icon name="arrow-left" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
onFilter(e) {
|
||||
this.props.onFilter(e.target.value);
|
||||
}
|
||||
|
||||
_getClass() {
|
||||
let cls = ['c-explorer__trigger'];
|
||||
|
||||
if (this.props.depth > 1) {
|
||||
cls.push('c-explorer__trigger--enabled');
|
||||
}
|
||||
return cls.join(' ');
|
||||
}
|
||||
|
||||
_getTitle() {
|
||||
let { page, depth } = this.props;
|
||||
|
||||
if (depth < 2 || !page) {
|
||||
return 'EXPLORER';
|
||||
}
|
||||
|
||||
return page.title;
|
||||
}
|
||||
|
||||
render() {
|
||||
let { page, depth, filter, onPop, onFilter, transName } = this.props;
|
||||
|
||||
const transitionProps = {
|
||||
component: 'span',
|
||||
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionName: `explorer-${transName}`,
|
||||
className: 'c-explorer__rel',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="c-explorer__header">
|
||||
<span className={this._getClass()} onClick={onPop}>
|
||||
{ depth > 1 ? this._getBackBtn() : null }
|
||||
<span className='u-overflow c-explorer__overflow'>
|
||||
<CSSTransitionGroup {...transitionProps}>
|
||||
<span className='c-explorer__parent-name' key={depth}>
|
||||
{this._getTitle()}
|
||||
</span>
|
||||
</CSSTransitionGroup>
|
||||
</span>
|
||||
</span>
|
||||
<span className="c-explorer__filter">
|
||||
{EXPLORER_FILTERS.map(props => {
|
||||
return <Filter key={props.id} {...props} activeFilter={filter} onFilter={onFilter} />
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExplorerHeader;
|
||||
63
client/src/components/explorer/ExplorerItem.js
Normal file
63
client/src/components/explorer/ExplorerItem.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { ADMIN_PAGES } from 'config';
|
||||
import Icon from 'components/icon/Icon';
|
||||
import PublishStatus from 'components/publish-status/PublishStatus';
|
||||
import PublishedTime from 'components/published-time/PublishedTime';
|
||||
import StateIndicator from 'components/state-indicator/StateIndicator';
|
||||
|
||||
export default class ExplorerItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._loadChildren = this._loadChildren.bind(this);
|
||||
}
|
||||
|
||||
_onNavigate(id) {
|
||||
window.location.href = `${ADMIN_PAGES}${id}`;
|
||||
}
|
||||
|
||||
_loadChildren(e) {
|
||||
e.stopPropagation();
|
||||
let { onItemClick, data } = this.props;
|
||||
onItemClick(data.id, data.title);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, typeName, data, index } = this.props;
|
||||
const { meta } = data;
|
||||
|
||||
let count = meta.children.count;
|
||||
|
||||
// TODO refactor.
|
||||
// If we only want pages with children, get this info by
|
||||
// looking at the descendants count vs children count.
|
||||
if (this.props.filter && this.props.filter.match(/has_children/)) {
|
||||
count = meta.descendants.count - meta.children.count;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={this._onNavigate.bind(this, data.id)} className="c-explorer__item">
|
||||
{count > 0 ?
|
||||
<span className="c-explorer__children" onClick={this._loadChildren}>
|
||||
<Icon name="folder-inverse" />
|
||||
<span aria-role='presentation'>
|
||||
See Children
|
||||
</span>
|
||||
</span> : null }
|
||||
<h3 className="c-explorer__title">
|
||||
<StateIndicator state={data.state} />
|
||||
{title}
|
||||
</h3>
|
||||
<p className='c-explorer__meta'>
|
||||
<span className="c-explorer__meta__type">{typeName}</span> | <PublishedTime publishedAt={meta.latest_revision_created_at} /> | <PublishStatus status={meta.status} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExplorerItem.propTypes = {
|
||||
title: PropTypes.string,
|
||||
data: PropTypes.object
|
||||
};
|
||||
226
client/src/components/explorer/ExplorerPanel.js
Normal file
226
client/src/components/explorer/ExplorerPanel.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import CSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import { EXPLORER_ANIM_DURATION } from 'config';
|
||||
|
||||
import ExplorerEmpty from './ExplorerEmpty';
|
||||
import ExplorerHeader from './ExplorerHeader';
|
||||
import ExplorerItem from './ExplorerItem';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
export default class ExplorerPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._clickOutside = this._clickOutside.bind(this);
|
||||
this._onItemClick = this._onItemClick.bind(this);
|
||||
this.closeModal = this.closeModal.bind(this);
|
||||
|
||||
this.state = {
|
||||
modalIsOpen: false,
|
||||
animation: 'push',
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
let oldProps = this.props;
|
||||
|
||||
if (!oldProps.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newProps.path.length > oldProps.path.length) {
|
||||
return this.setState({ animation: 'push' });
|
||||
} else {
|
||||
return this.setState({ animation: 'pop' });
|
||||
}
|
||||
}
|
||||
|
||||
_loadChildren() {
|
||||
let { page } = this.props;
|
||||
|
||||
if (!page || page.children.isFetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (page.meta.children.count && !page.children.length && !page.children.isFetching && !page.children.isLoaded) {
|
||||
this.props.getChildren(page.id);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._loadChildren();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.init();
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.classList.add('u-explorer-open');
|
||||
document.addEventListener('click', this._clickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.body.style.overflow = '';
|
||||
document.body.classList.remove('u-explorer-open');
|
||||
document.removeEventListener('click', this._clickOutside);
|
||||
}
|
||||
|
||||
_clickOutside(e) {
|
||||
let { explorer } = this.refs;
|
||||
|
||||
if (!explorer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!explorer.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
_getStyle() {
|
||||
const { top, left } = this.props;
|
||||
return {
|
||||
left: left + 'px',
|
||||
top: top + 'px'
|
||||
};
|
||||
}
|
||||
|
||||
_getClass() {
|
||||
let { type } = this.props;
|
||||
let cls = ['c-explorer'];
|
||||
|
||||
if (type) {
|
||||
cls.push(`c-explorer--${type}`);
|
||||
}
|
||||
|
||||
return cls.join(' ');
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(clearError());
|
||||
this.setState({
|
||||
modalIsOpen: false
|
||||
});
|
||||
}
|
||||
|
||||
_onItemClick(id) {
|
||||
let node = this.props.nodes[id];
|
||||
|
||||
if (node.isLoaded) {
|
||||
this.props.pushPage(id);
|
||||
} else {
|
||||
this.props.loadItemWithChildren(id);
|
||||
}
|
||||
}
|
||||
|
||||
renderChildren(page) {
|
||||
let { nodes, pageTypes, filter } = this.props;
|
||||
|
||||
if (!page || !page.children.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return page.children.items.map(index => {
|
||||
return nodes[index];
|
||||
}).map(item => {
|
||||
const typeName = pageTypes[item.meta.type] ? pageTypes[item.meta.type].verbose_name : item.meta.type;
|
||||
const props = {
|
||||
onItemClick: this._onItemClick,
|
||||
parent: page,
|
||||
key: item.id,
|
||||
title: item.title,
|
||||
typeName,
|
||||
data: item,
|
||||
filter,
|
||||
};
|
||||
|
||||
return <ExplorerItem {...props} />
|
||||
});
|
||||
}
|
||||
|
||||
_getContents() {
|
||||
let { page } = this.props;
|
||||
let contents = null;
|
||||
|
||||
if (page) {
|
||||
if (page.children.items.length) {
|
||||
return this.renderChildren(page)
|
||||
} else {
|
||||
return <ExplorerEmpty />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
page,
|
||||
onPop,
|
||||
onClose,
|
||||
loading,
|
||||
type,
|
||||
pageData,
|
||||
transport,
|
||||
onFilter,
|
||||
filter,
|
||||
path,
|
||||
resolved
|
||||
} = this.props;
|
||||
|
||||
// Don't show anything until the tree is resolved.
|
||||
if (!this.props.resolved) {
|
||||
return <div />
|
||||
}
|
||||
|
||||
const headerProps = {
|
||||
depth: path.length,
|
||||
page,
|
||||
onPop,
|
||||
onClose,
|
||||
onFilter,
|
||||
filter
|
||||
}
|
||||
|
||||
const transitionTargetProps = {
|
||||
key: path.length,
|
||||
className: 'c-explorer__transition-group'
|
||||
}
|
||||
|
||||
const transitionProps = {
|
||||
component: 'div',
|
||||
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionName: `explorer-${this.state.animation}`
|
||||
}
|
||||
|
||||
const innerTransitionProps = {
|
||||
component: 'div',
|
||||
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
|
||||
transitionName: `explorer-fade`
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={this._getStyle()} className={this._getClass()} ref='explorer'>
|
||||
<ExplorerHeader {...headerProps} transName={this.state.animation} />
|
||||
<div className='c-explorer__drawer'>
|
||||
<CSSTransitionGroup {...transitionProps}>
|
||||
<div {...transitionTargetProps}>
|
||||
<CSSTransitionGroup {...innerTransitionProps}>
|
||||
{page.isFetching ? <LoadingSpinner key={1} /> : (
|
||||
<div key={0}>
|
||||
{this._getContents()}
|
||||
</div>
|
||||
)}
|
||||
</CSSTransitionGroup>
|
||||
|
||||
</div>
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExplorerPanel.propTypes = {
|
||||
|
||||
}
|
||||
9
client/src/components/explorer/LoadingSpinner.js
Normal file
9
client/src/components/explorer/LoadingSpinner.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className="c-explorer__loading">
|
||||
<span className="c-explorer__spinner icon icon-spinner" /> Loading...
|
||||
</div>
|
||||
);
|
||||
|
||||
export default LoadingSpinner;
|
||||
31
client/src/components/explorer/PageCount.js
Normal file
31
client/src/components/explorer/PageCount.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ADMIN_PAGES } from 'config';
|
||||
|
||||
const PageCount = ({ id, count }) => {
|
||||
let prefix = '';
|
||||
let suffix = 'pages';
|
||||
|
||||
if (count === 0) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (count > 1) {
|
||||
prefix = 'all ';
|
||||
}
|
||||
|
||||
if (count === 1) {
|
||||
suffix = 'page';
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={() => {
|
||||
window.location.href = `${ADMIN_PAGES}${id}/`
|
||||
}}
|
||||
className="c-explorer__see-more">
|
||||
See {prefix}{ count } {suffix}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageCount;
|
||||
142
client/src/components/explorer/actions/index.js
Normal file
142
client/src/components/explorer/actions/index.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
|
||||
import { API_PAGES, PAGES_ROOT_ID } from 'config';
|
||||
|
||||
function _getHeaders() {
|
||||
const headers = new Headers();
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
return {
|
||||
credentials: 'same-origin',
|
||||
headers: headers,
|
||||
method: 'GET'
|
||||
};
|
||||
}
|
||||
|
||||
function _get(url) {
|
||||
return fetch(url, _getHeaders()).then(response => response.json());
|
||||
}
|
||||
|
||||
export const fetchStart = createAction('FETCH_START');
|
||||
|
||||
export const fetchSuccess = createAction('FETCH_SUCCESS', (id, body) => {
|
||||
return { id, body };
|
||||
});
|
||||
|
||||
export const fetchFailure = createAction('FETCH_FAILURE');
|
||||
|
||||
export const pushPage = createAction('PUSH_PAGE');
|
||||
|
||||
export const popPage = createAction('POP_PAGE');
|
||||
|
||||
export const fetchBranchSuccess = createAction('FETCH_BRANCH_SUCCESS', (id, json) => {
|
||||
return { id, json };
|
||||
});
|
||||
|
||||
export const fetchBranchStart = createAction('FETCH_BRANCH_START');
|
||||
|
||||
export const clearError = createAction('CLEAR_TRANSPORT_ERROR');
|
||||
|
||||
export const resetTree = createAction('RESET_TREE');
|
||||
|
||||
export const treeResolved = createAction('TREE_RESOLVED');
|
||||
|
||||
// Make this a bit better... hmm....
|
||||
export function fetchTree(id = 1) {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchBranchStart(id));
|
||||
|
||||
return _get(`${API_PAGES}${id}/`)
|
||||
.then(json => {
|
||||
dispatch(fetchBranchSuccess(id, json));
|
||||
|
||||
// Recursively walk up the tree to the root, to figure out how deep
|
||||
// in the tree we are.
|
||||
if (json.meta.parent) {
|
||||
dispatch(fetchTree(json.meta.parent.id));
|
||||
} else {
|
||||
dispatch(treeResolved());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchRoot() {
|
||||
return (dispatch) => {
|
||||
// TODO Should not need an id.
|
||||
dispatch(resetTree(1));
|
||||
|
||||
return _get(`${API_PAGES}?child_of=${PAGES_ROOT_ID}`)
|
||||
.then(json => {
|
||||
// TODO right now, only works for a single homepage.
|
||||
// TODO What do we do if there is no homepage?
|
||||
const rootId = json.items[0].id;
|
||||
|
||||
dispatch(fetchTree(rootId));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleExplorer = createAction('TOGGLE_EXPLORER');
|
||||
|
||||
export const fetchChildrenSuccess = createAction('FETCH_CHILDREN_SUCCESS', (id, json) => {
|
||||
return { id, json };
|
||||
});
|
||||
|
||||
export const fetchChildrenStart = createAction('FETCH_CHILDREN_START');
|
||||
|
||||
/**
|
||||
* Gets the children of a node from the API
|
||||
*/
|
||||
export function fetchChildren(id = 'root') {
|
||||
return (dispatch, getState) => {
|
||||
const { explorer } = getState();
|
||||
|
||||
let api = `${API_PAGES}?child_of=${id}`;
|
||||
|
||||
if (explorer.fields) {
|
||||
api += `&fields=${explorer.fields.map(global.encodeURIComponent).join(',')}`;
|
||||
}
|
||||
|
||||
if (explorer.filter) {
|
||||
api = `${api}&${explorer.filter}`;
|
||||
}
|
||||
|
||||
dispatch(fetchChildrenStart(id));
|
||||
|
||||
return _get(api)
|
||||
.then(json => dispatch(fetchChildrenSuccess(id, json)));
|
||||
};
|
||||
}
|
||||
|
||||
export function setFilter(filter) {
|
||||
return (dispatch, getState) => {
|
||||
const { explorer } = getState();
|
||||
const id = explorer.path[explorer.path.length - 1];
|
||||
|
||||
dispatch({
|
||||
payload: {
|
||||
filter,
|
||||
id
|
||||
},
|
||||
type: 'SET_FILTER'
|
||||
});
|
||||
|
||||
dispatch(fetchChildren(id));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: determine if page is already loaded, don't load it again, just push.
|
||||
*/
|
||||
export function fetchPage(id = 1) {
|
||||
return dispatch => {
|
||||
dispatch(fetchStart(id));
|
||||
return _get(`${API_PAGES}${id}/`)
|
||||
.then(json => dispatch(fetchSuccess(id, json)))
|
||||
.then(json => dispatch(fetchChildren(id, json)))
|
||||
.catch(json => dispatch(fetchFailure(new Error(JSON.stringify(json)))));
|
||||
};
|
||||
}
|
||||
|
||||
export const setDefaultPage = createAction('SET_DEFAULT_PAGE');
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import StateIndicator from 'components/state-indicator';
|
||||
|
||||
export default class ExplorerItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, data } = this.props;
|
||||
|
||||
return (
|
||||
<div className="c-explorer__item">
|
||||
<h3 className="c-explorer__title">
|
||||
<StateIndicator state={data.state} />
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ExplorerItem.propTypes = {
|
||||
title: PropTypes.string,
|
||||
data: PropTypes.object
|
||||
};
|
||||
18
client/src/components/explorer/filter.js
Normal file
18
client/src/components/explorer/filter.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
const Filter = ({label, filter=null, activeFilter, onFilter}) => {
|
||||
let click = onFilter.bind(this, filter);
|
||||
let isActive = activeFilter === filter;
|
||||
let cls = ['c-filter'];
|
||||
|
||||
if (isActive) {
|
||||
cls.push('c-filter--active');
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={cls.join(' ')} onClick={click}>{label}</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default Filter;
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import LoadingIndicator from 'components/loading-indicator';
|
||||
import ExplorerItem from './explorer-item';
|
||||
|
||||
import { API } from 'config';
|
||||
|
||||
|
||||
class Explorer extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { cursor: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch(`${API}/pages/?child_of=root`)
|
||||
.then(res => res.json())
|
||||
.then(body => {
|
||||
this.setState({
|
||||
cursor: body
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(cursor) {
|
||||
|
||||
}
|
||||
|
||||
_getPages(cursor) {
|
||||
if (!cursor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return cursor.pages.map(item =>
|
||||
<ExplorerItem key={item.id} title={item.title} data={item} />
|
||||
);
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
const { position } = this.props;
|
||||
return {
|
||||
left: position.right + 'px',
|
||||
top: position.top + 'px'
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cursor } = this.state;
|
||||
const pages = this._getPages(cursor);
|
||||
|
||||
return (
|
||||
<div style={this.getPosition()} className="c-explorer">
|
||||
{cursor ? pages : <LoadingIndicator />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Explorer.propTypes = {
|
||||
onPageSelect: PropTypes.func,
|
||||
initialPath: PropTypes.string,
|
||||
apiPath: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
position: PropTypes.object
|
||||
};
|
||||
|
||||
export default Explorer;
|
||||
95
client/src/components/explorer/reducers/explorer.js
Normal file
95
client/src/components/explorer/reducers/explorer.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
const stateDefaults = {
|
||||
isVisible: false,
|
||||
isFetching: false,
|
||||
isResolved: false,
|
||||
path: [],
|
||||
currentPage: 1,
|
||||
defaultPage: 1,
|
||||
// Specificies which fields are to be fetched in the API calls.
|
||||
fields: ['title', 'latest_revision_created_at', 'status', 'descendants', 'children'],
|
||||
filter: 'has_children=1',
|
||||
// Coming from the API in order to get translated / pluralised labels.
|
||||
pageTypes: {},
|
||||
}
|
||||
|
||||
export default function explorer(state = stateDefaults, action) {
|
||||
|
||||
let newNodes = state.path;
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_DEFAULT_PAGE':
|
||||
return Object.assign({}, state, {
|
||||
defaultPage: action.payload
|
||||
});
|
||||
|
||||
case 'RESET_TREE':
|
||||
return Object.assign({}, state, {
|
||||
isFetching: true,
|
||||
isResolved: false,
|
||||
currentPage: action.payload,
|
||||
path: [],
|
||||
});
|
||||
|
||||
case 'TREE_RESOLVED':
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
isResolved: true
|
||||
});
|
||||
|
||||
case 'TOGGLE_EXPLORER':
|
||||
return Object.assign({}, state, {
|
||||
isVisible: !state.isVisible,
|
||||
currentPage: action.payload ? action.payload : state.defaultPage,
|
||||
});
|
||||
|
||||
case 'FETCH_START':
|
||||
return Object.assign({}, state, {
|
||||
isFetching: true
|
||||
});
|
||||
|
||||
case 'FETCH_BRANCH_SUCCESS':
|
||||
if (state.path.indexOf(action.payload.id) < 0) {
|
||||
newNodes = [action.payload.id].concat(state.path);
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
path: newNodes,
|
||||
currentPage: state.currentPage ? state.currentPage : action.payload.id
|
||||
});
|
||||
|
||||
// called on fetch page...
|
||||
case 'FETCH_SUCCESS':
|
||||
if (state.path.indexOf(action.payload.id) < 0) {
|
||||
newNodes = state.path.concat([action.payload.id]);
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
path: newNodes,
|
||||
});
|
||||
|
||||
case 'PUSH_PAGE':
|
||||
return Object.assign({}, state, {
|
||||
path: state.path.concat([action.payload])
|
||||
});
|
||||
return state;
|
||||
|
||||
case 'POP_PAGE':
|
||||
let poppedNodes = state.path.length > 1 ? state.path.slice(0, -1) : state.path;
|
||||
return Object.assign({}, state, {
|
||||
path: poppedNodes,
|
||||
});
|
||||
|
||||
case 'FETCH_CHILDREN_SUCCESS':
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
pageTypes: action.payload.json.__types,
|
||||
});
|
||||
|
||||
case 'SET_FILTER':
|
||||
return Object.assign({}, state, {
|
||||
filter: action.filter
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
13
client/src/components/explorer/reducers/index.js
Normal file
13
client/src/components/explorer/reducers/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import explorer from './explorer';
|
||||
import nodes from './nodes';
|
||||
import transport from './transport';
|
||||
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
explorer,
|
||||
transport,
|
||||
nodes,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
99
client/src/components/explorer/reducers/nodes.js
Normal file
99
client/src/components/explorer/reducers/nodes.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
function children(state={
|
||||
items: [],
|
||||
count: 0,
|
||||
isFetching: false
|
||||
}, action) {
|
||||
|
||||
switch(action.type) {
|
||||
case 'FETCH_CHILDREN_START':
|
||||
return Object.assign({}, state, {
|
||||
isFetching: true
|
||||
});
|
||||
|
||||
case 'FETCH_CHILDREN_SUCCESS':
|
||||
return Object.assign({}, state, {
|
||||
items: action.payload.json.items.map(item => { return item.id }),
|
||||
count: action.payload.json.meta.total_count,
|
||||
isFetching: false,
|
||||
isLoaded: true
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
export default function nodes(state = {}, action) {
|
||||
let defaults = {
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
isLoaded: false,
|
||||
children: children(undefined, {})
|
||||
};
|
||||
|
||||
switch(action.type) {
|
||||
case 'FETCH_CHILDREN_START':
|
||||
return Object.assign({}, state, {
|
||||
[action.payload]: Object.assign({}, state[action.payload], {
|
||||
isFetching: true,
|
||||
children: children(state[action.payload] ? state[action.payload].children : undefined, action)
|
||||
})
|
||||
});
|
||||
|
||||
case 'FETCH_CHILDREN_SUCCESS':
|
||||
let map = {};
|
||||
|
||||
action.payload.json.items.forEach(item => {
|
||||
map = Object.assign({}, map, {
|
||||
[item.id]: Object.assign({}, defaults, state[item.id], item, {
|
||||
isLoaded: true
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return Object.assign({}, state, map, {
|
||||
[action.payload.id]: Object.assign({}, state[action.payload.id], {
|
||||
isFetching: false,
|
||||
children: children(state[action.payload.id].children, action)
|
||||
})
|
||||
});
|
||||
|
||||
case 'RESET_TREE':
|
||||
return Object.assign({}, {});
|
||||
|
||||
case 'SET_FILTER':
|
||||
// Unset all isLoaded states when the filter changes
|
||||
let updatedState = {};
|
||||
|
||||
for (let _key in state) {
|
||||
if (state.hasOwnProperty( _key )) {
|
||||
let _obj = state[_key];
|
||||
_obj.children.isLoaded = false;
|
||||
updatedState[_obj.id] = Object.assign({}, _obj, { isLoaded: false })
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign({}, updatedState);
|
||||
|
||||
case 'FETCH_START':
|
||||
return Object.assign({}, state, {
|
||||
[action.payload]: Object.assign({}, defaults, state[action.payload], {
|
||||
isFetching: true,
|
||||
isError: false,
|
||||
})
|
||||
});
|
||||
|
||||
case 'FETCH_BRANCH_SUCCESS':
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.id]: Object.assign({}, defaults, state[action.payload.id], action.payload.json, {
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isLoaded: true
|
||||
})
|
||||
});
|
||||
|
||||
case 'FETCH_SUCCESS':
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
15
client/src/components/explorer/reducers/transport.js
Normal file
15
client/src/components/explorer/reducers/transport.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default function transport(state={error: null, showMessage: false}, action) {
|
||||
switch(action.type) {
|
||||
case 'FETCH_FAILURE':
|
||||
return Object.assign({}, state, {
|
||||
error: action.payload.message,
|
||||
showMessage: true
|
||||
});
|
||||
case 'CLEAR_TRANSPORT_ERROR':
|
||||
return Object.assign({}, state, {
|
||||
error: null,
|
||||
showMessage: false
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -1,13 +1,377 @@
|
|||
$c-explorer-bg: #4C4E4D;
|
||||
$c-explorer-secondary: #aaa;
|
||||
$c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
|
||||
|
||||
.c-explorer * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.c-explorer {
|
||||
width: 320px;
|
||||
height: 500px;
|
||||
background: #333;
|
||||
background: $c-explorer-bg;
|
||||
position: absolute;
|
||||
z-index: 25;
|
||||
top: 0;
|
||||
left: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c-explorer--sidebar {
|
||||
height: 100vh;
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
|
||||
left: 180px;
|
||||
top: 0;
|
||||
z-index: 150;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.c-explorer__header {
|
||||
border-bottom: solid 1px #676767;
|
||||
overflow: hidden;
|
||||
color: $c-explorer-secondary;
|
||||
}
|
||||
|
||||
.c-explorer__trigger {
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 80%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.c-explorer__trigger--enabled {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.c-explorer__filter {
|
||||
float: right;
|
||||
width: 50px;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.c-filter {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0 .25em;
|
||||
border: solid 1px rgba(255,255,255,0.1);
|
||||
border-radius: 2px;
|
||||
line-height: 1;
|
||||
margin-left: .25rem;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-color: rgba(0,0,0,0.5);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.c-filter--active {
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__back {
|
||||
cursor: pointer;
|
||||
margin-right: .25rem;
|
||||
float: left;
|
||||
margin-top: -1px;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.icon {
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.c-explorer__title {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.c-explorer__loading {
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.c-explorer__item {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: solid 1px #676767;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c-explorer__placeholder {
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.c-explorer__meta {
|
||||
font-size: 12px;
|
||||
color: $c-explorer-secondary;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// TODO Could be a utility class
|
||||
.c-explorer__meta__type {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.c-explorer__item:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.c-explorer__see-more {
|
||||
cursor: pointer;
|
||||
padding: .5rem 1rem;
|
||||
background: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__children {
|
||||
display: inline-block;
|
||||
border-radius: 50rem;
|
||||
border: solid 1px #aaa;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
padding: .5em .3em .5em .5em;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
> [aria-role='presentation'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.c-status {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03rem;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.c-status--live {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__drawer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 36px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__overflow {
|
||||
max-width: 12rem;
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// TODO: move to their own component..
|
||||
// =============================================================================
|
||||
|
||||
.o-pill {
|
||||
display: inline-block;
|
||||
padding: 0 .5em;
|
||||
border-radius: .25em;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.u-overflow {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__rel {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 19px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.c-explorer__parent-name {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c-explorer__spinner:after {
|
||||
display: inline-block;
|
||||
animation: spin 0.5s infinite linear;
|
||||
line-height: 1
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Transitions
|
||||
// =============================================================================
|
||||
|
||||
// $out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000);
|
||||
// $in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335);
|
||||
|
||||
$out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
|
||||
$in-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
|
||||
$c-explorer-duration: 200ms;
|
||||
|
||||
.c-explorer__transition-group {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.explorer-push-enter {
|
||||
transform: translateX(100%);
|
||||
transition: transform $c-explorer-duration $out-circ, opacity $c-explorer-duration linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.explorer-push-enter-active {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-push-leave {
|
||||
transform: translateX(0);
|
||||
transition: transform $c-explorer-duration $in-circ, opacity $c-explorer-duration linear;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-push-leave-active {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Pop transition
|
||||
// =============================================================================
|
||||
|
||||
.explorer-pop-enter {
|
||||
transform: translateX(-100%);
|
||||
transition: transform $c-explorer-duration $out-circ, opacity $c-explorer-duration linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.explorer-pop-enter-active {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-pop-leave {
|
||||
transform: translateX(0);
|
||||
transition: transform $c-explorer-duration $in-circ, opacity $c-explorer-duration linear;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-pop-leave-active {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.explorer-toggle-enter {
|
||||
opacity: 0;
|
||||
transition: all $c-explorer-duration;
|
||||
}
|
||||
|
||||
.explorer-toggle-enter-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-toggle-leave {
|
||||
opacity: 1;
|
||||
transition: all $c-explorer-duration;
|
||||
}
|
||||
|
||||
.explorer-toggle-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Fade transition
|
||||
// =============================================================================
|
||||
|
||||
.explorer-fade-enter {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease .1s;
|
||||
}
|
||||
|
||||
.explorer-fade-enter-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.explorer-fade-leave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity .1s ease;
|
||||
}
|
||||
|
||||
.explorer-fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Header transitions
|
||||
// =============================================================================
|
||||
|
||||
.header-push-enter {
|
||||
opacity: 0;
|
||||
transition: opacity .1s linear .1s;
|
||||
}
|
||||
|
||||
.header-push-enter-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header-push-leave {
|
||||
opacity: 1;
|
||||
transition: opacity .1s;
|
||||
}
|
||||
|
||||
.header-push-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
|||
60
client/src/components/explorer/toggle.js
Normal file
60
client/src/components/explorer/toggle.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as actions from './actions';
|
||||
|
||||
class Toggle extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this._sandbox = this._sandbox.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.visible) {
|
||||
this.refs.btn.addEventListener('click', this._sandbox);
|
||||
} else {
|
||||
this.refs.btn.removeEventListener('click', this._sandbox);
|
||||
}
|
||||
}
|
||||
|
||||
_sandbox(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onToggle(this.props.page);
|
||||
}
|
||||
|
||||
render() {
|
||||
const cls = ['icon icon-folder-open-inverse dl-trigger'];
|
||||
|
||||
if (this.props.loading) {
|
||||
cls.push('icon-spinner');
|
||||
}
|
||||
|
||||
return (
|
||||
<a ref="btn" onClick={this._sandbox} className={cls.join(' ')}>
|
||||
{this.props.label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Toggle.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
const mapStateToProps = (store) => {
|
||||
return {
|
||||
loading: store.explorer.isFetching,
|
||||
visible: store.explorer.isVisible,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onToggle: (id) => {
|
||||
dispatch(actions.toggleExplorer());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Toggle);
|
||||
17
client/src/components/icon/Icon.js
Normal file
17
client/src/components/icon/Icon.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
// TODO Add support for accessible label.
|
||||
const Icon = ({ name, className }) => (
|
||||
<span className={`icon icon-${name} ${className}`} />
|
||||
);
|
||||
|
||||
Icon.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Icon.defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
13
client/src/components/icon/README.md
Normal file
13
client/src/components/icon/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Icon
|
||||
|
||||
A simple component to render an icon. Abstracts away the actual icon implementation (font icons, SVG icons, CSS sprite).
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { Icon } from 'wagtail';
|
||||
|
||||
render(
|
||||
<Icon name="arrow-left" className="icon--active icon--warning" />
|
||||
);
|
||||
```
|
||||
5
client/src/components/icon/style.scss
Normal file
5
client/src/components/icon/style.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Icon
|
||||
|
||||
.c-icon {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Explorer from './explorer';
|
||||
import LoadingIndicator from './loading-indicator';
|
||||
import StateIndicator from './state-indicator';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import StateIndicator from './StateIndicator';
|
||||
|
||||
export { Explorer };
|
||||
export { LoadingIndicator };
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
const LoadingIndicator = () =>
|
||||
const LoadingIndicator = () => (
|
||||
<div className="o-icon c-indicator is-spinning">
|
||||
<span ariaRole="presentation">Loading...</span>
|
||||
</div>;
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
export default LoadingIndicator;
|
||||
17
client/src/components/publish-status/PublishStatus.js
Normal file
17
client/src/components/publish-status/PublishStatus.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
const PublishStatus = ({ status }) => {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let classes = ['o-pill', 'c-status', 'c-status--' + status.status];
|
||||
|
||||
return (
|
||||
<span className={classes.join(' ')}>
|
||||
{status.status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublishStatus;
|
||||
9
client/src/components/publish-status/README.md
Normal file
9
client/src/components/publish-status/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# PublishStatus
|
||||
|
||||
About this component
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { PublishStatus } from 'wagtail';
|
||||
```
|
||||
5
client/src/components/publish-status/style.scss
Normal file
5
client/src/components/publish-status/style.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// PublishStatus
|
||||
|
||||
.c-publish-status {
|
||||
display: block;
|
||||
}
|
||||
14
client/src/components/published-time/PublishedTime.js
Normal file
14
client/src/components/published-time/PublishedTime.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
const PublishedTime = ({publishedAt}) => {
|
||||
let date = moment(publishedAt);
|
||||
let str = publishedAt ? date.format('DD.MM.YYYY') : 'No date';
|
||||
|
||||
return (
|
||||
<span>{str}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublishedTime;
|
||||
9
client/src/components/published-time/README.md
Normal file
9
client/src/components/published-time/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# PublishedTime
|
||||
|
||||
About this component
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { PublishedTime } from 'wagtail';
|
||||
```
|
||||
5
client/src/components/published-time/style.scss
Normal file
5
client/src/components/published-time/style.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// PublishedTime
|
||||
|
||||
.c-published-time {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1 +1,14 @@
|
|||
export const API = '/admin/api/v2beta/';
|
||||
export const API = global.wagtailConfig.api;
|
||||
export const API_PAGES = global.wagtailConfig.api.pages;
|
||||
|
||||
export const PAGES_ROOT_ID = 'root';
|
||||
|
||||
export const EXPLORER_ANIM_DURATION = 220;
|
||||
|
||||
export const ADMIN_PAGES = global.wagtailConfig.urls.pages;
|
||||
|
||||
export const EXPLORER_FILTERS = [
|
||||
// TODO Add back in when we want to support explorer without has_children=1
|
||||
// { id: 1, label: 'A', filter: null },
|
||||
// { id: 2, label: 'B', filter: 'has_children=1' }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default class {{ name }} extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
const {{ name }} = (props) => {
|
||||
return (
|
||||
<div className="c-{{ slug }}">
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="c-{{ slug }}">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
{{ name }}.propTypes = {
|
||||
};
|
||||
|
||||
export default {{ name }};
|
||||
|
|
|
|||
25
client/template/component.test.mst
Normal file
25
client/template/component.test.mst
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// TODO Move this file to the client/tests/components directory.
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { shallow, mount, render } from 'enzyme';
|
||||
import '../stubs';
|
||||
|
||||
import {{ name }} from '../../src/components/{{ slug }}/{{ name }}';
|
||||
|
||||
describe('{{ name }}', () => {
|
||||
it('exists', () => {
|
||||
expect({{ name }}).to.exist;
|
||||
});
|
||||
|
||||
it('contains spec with an expectation', () => {
|
||||
expect(shallow(<{{ name }} />).contains(<div className="c-{{ slug }}" />)).to.equal(true);
|
||||
});
|
||||
|
||||
it('contains spec with an expectation', () => {
|
||||
expect(shallow(<{{ name }} />).is('.c-{{ slug }}')).to.equal(true);
|
||||
});
|
||||
|
||||
it('contains spec with an expectation', () => {
|
||||
expect(mount(<{{ name }} />).find('.c-{{ slug }}').length).to.equal(1);
|
||||
});
|
||||
});
|
||||
21
client/tests/components/Icon.test.js
Normal file
21
client/tests/components/Icon.test.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { shallow } from 'enzyme';
|
||||
import '../stubs';
|
||||
|
||||
import Icon from '../../src/components/icon/Icon';
|
||||
|
||||
describe('Icon', () => {
|
||||
it('exists', () => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(Icon).to.exist;
|
||||
});
|
||||
|
||||
it('has just icon classes by default', () => {
|
||||
expect(shallow(<Icon name="test" />).is('.icon.icon-test')).to.equal(true);
|
||||
});
|
||||
|
||||
it('has additional classes if specified', () => {
|
||||
expect(shallow(<Icon name="test" className="icon-red icon-big" />).prop('className')).to.contain('icon-red icon-big');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,39 @@
|
|||
/*eslint-disable */
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import Explorer from '../../src/components/explorer';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import '../stubs';
|
||||
import Explorer from '../../src/components/explorer/Explorer';
|
||||
import ExplorerItem from '../../src/components/explorer/ExplorerItem';
|
||||
|
||||
describe('Explorer', () => {
|
||||
it('exists', () => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(Explorer).to.exist;
|
||||
});
|
||||
|
||||
describe('ExplorerItem', () => {
|
||||
const props = {
|
||||
data: {
|
||||
meta: {
|
||||
children: {
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
it('exists', () => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(ExplorerItem).to.exist;
|
||||
});
|
||||
|
||||
it('has item metadata', () => {
|
||||
expect(shallow(<ExplorerItem {...props} />).find('.c-explorer__meta')).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('metadata contains item type', () => {
|
||||
expect(shallow(<ExplorerItem {...props} typeName="Foo" />).find('.c-explorer__meta').text()).to.contain('Foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
12
client/tests/stubs.js
Normal file
12
client/tests/stubs.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
global.wagtailConfig = {
|
||||
api: {
|
||||
documents: '/admin/api/v1beta/documents/',
|
||||
images: '/admin/api/v1beta/images/',
|
||||
pages: '/admin/api/v1beta/pages/',
|
||||
},
|
||||
urls: {
|
||||
pages: '/admin/pages/',
|
||||
}
|
||||
};
|
||||
|
||||
global.wagtailVersion = '1.6a1';
|
||||
19
package.json
19
package.json
|
|
@ -20,9 +20,12 @@
|
|||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"chai": "^3.5.0",
|
||||
"eslint": "^2.2.0",
|
||||
"eslint-config-airbnb": "^6.0.2",
|
||||
"eslint-plugin-react": "^4.1.0",
|
||||
"enzyme": "^2.3.0",
|
||||
"eslint": "^2.9.0",
|
||||
"eslint-config-wagtail": "^0.1.0",
|
||||
"eslint-plugin-import": "^1.8.1",
|
||||
"eslint-plugin-jsx-a11y": "^1.5.3",
|
||||
"eslint-plugin-react": "^4.3.0",
|
||||
"glob": "^7.0.0",
|
||||
"gulp": "~3.8.11",
|
||||
"gulp-autoprefixer": "~3.0.2",
|
||||
|
|
@ -34,17 +37,26 @@
|
|||
"lodash": "^4.5.1",
|
||||
"mocha": "^2.4.5",
|
||||
"mustache": "^2.2.1",
|
||||
"react-addons-test-utils": "^0.14.8",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"require-dir": "^0.3.0",
|
||||
"sinon": "^1.17.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.5.0",
|
||||
"exports-loader": "^0.6.3",
|
||||
"imports-loader": "^0.6.5",
|
||||
"moment": "^2.11.2",
|
||||
"react": "^0.14.7",
|
||||
"react-accessible-modal": "0.0.5",
|
||||
"react-addons-css-transition-group": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-onclickoutside": "^4.5.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"redux": "^3.3.1",
|
||||
"redux-actions": "^0.10.0",
|
||||
"redux-logger": "^2.6.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"webpack": "^1.12.14",
|
||||
"whatwg-fetch": "^0.11.0"
|
||||
},
|
||||
|
|
@ -57,6 +69,7 @@
|
|||
"lint": "npm run lint:js",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "env NODE_PATH=$NODE_PATH:$PWD/client/src mocha --compilers js:babel-core/register client/tests/**/*.test.js",
|
||||
"test:unit:watch": "env NODE_PATH=$NODE_PATH:$PWD/client/src mocha --watch --compilers js:babel-core/register client/tests/**/*.test.js",
|
||||
"test:unit:coverage": "env NODE_PATH=$NODE_PATH:$PWD/client/src babel-node $(npm bin)/isparta cover node_modules/mocha/bin/_mocha -- client/tests/**/*.test.js",
|
||||
"component": "node ./client/src/cli/index.js component --dir ./client/src/components/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Explorer from 'components/explorer';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import createLogger from 'redux-logger'
|
||||
import thunkMiddleware from 'redux-thunk'
|
||||
|
||||
import Explorer from 'components/explorer/Explorer';
|
||||
import ExplorerToggle from 'components/explorer/toggle';
|
||||
import rootReducer from 'components/explorer/reducers';
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', e => {
|
||||
|
|
@ -8,16 +16,32 @@ document.addEventListener('DOMContentLoaded', e => {
|
|||
const div = document.createElement('div');
|
||||
const trigger = document.querySelector('[data-explorer-menu-url]');
|
||||
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!div.childNodes.length) {
|
||||
ReactDOM.render(<Explorer position={trigger.getBoundingClientRect()} />, div);
|
||||
} else {
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
}
|
||||
});
|
||||
let rect = trigger.getBoundingClientRect();
|
||||
let triggerParent = trigger.parentNode;
|
||||
let label = trigger.innerText;
|
||||
|
||||
top.parentNode.appendChild(div);
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(loggerMiddleware, thunkMiddleware)
|
||||
);
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<ExplorerToggle label={label} />
|
||||
</Provider>
|
||||
),
|
||||
triggerParent
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Explorer type={'sidebar'} top={0} left={rect.right} defaultPage={1} />
|
||||
</Provider>,
|
||||
div
|
||||
);
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ $(function() {
|
|||
var $body = $('body');
|
||||
|
||||
// Dynamically load menu on request.
|
||||
$(document).on('click', '.dl-trigger', function() {
|
||||
$(document).on('click', '.dl-trigger--unused', function() {
|
||||
var $this = $(this);
|
||||
|
||||
// Close all submenus
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ body.explorer-open {
|
|||
height: 100%;
|
||||
position: fixed;
|
||||
width: $menu-width;
|
||||
z-index: 26;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,33 @@
|
|||
}
|
||||
// scss-lint:enable all
|
||||
|
||||
@keyframes matteIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.u-explorer-open {
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
// content: '';
|
||||
// position: fixed;
|
||||
// background: rgba(255, 255, 255, 0.5);
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// opacity: 1;
|
||||
// animation: matteIn .2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
html {
|
||||
background: $color-grey-4;
|
||||
height: 100%;
|
||||
|
|
@ -191,20 +218,22 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
background: $color-grey-1;
|
||||
}
|
||||
// Let's not, for now...
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $color-grey-2;
|
||||
-webkit-border-radius: 1ex;
|
||||
}
|
||||
// ::-webkit-scrollbar {
|
||||
// height: 10px;
|
||||
// width: 10px;
|
||||
// background: $color-grey-1;
|
||||
// }
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: $color-grey-1;
|
||||
}
|
||||
// ::-webkit-scrollbar-thumb {
|
||||
// background: $color-grey-2;
|
||||
// -webkit-border-radius: 1ex;
|
||||
// }
|
||||
|
||||
// ::-webkit-scrollbar-corner {
|
||||
// background: $color-grey-1;
|
||||
// }
|
||||
|
||||
.breadcrumb {
|
||||
@include unlist();
|
||||
|
|
@ -526,3 +555,201 @@ footer,
|
|||
// a {
|
||||
// @include transition(color 0.2s ease, background-color 0.2s ease);
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
// -----------------------------------------------------------------------------
|
||||
// Modal lightboxes
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// As of 2015, the vertical-align: middle table is still the best cross-browser
|
||||
// way to vertically centre stuff. This modal component uses this pattern with
|
||||
// the following structure:
|
||||
//
|
||||
// <div class="modal modal--active">
|
||||
// <div class="modal__table">
|
||||
// <div class="modal__center">
|
||||
// <div class="modal__content">
|
||||
// Hello!
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
//
|
||||
// Requires '_animations.scss';
|
||||
$z-index-modal: 1;
|
||||
$z-index-modal-matte: 2;
|
||||
$z-index-modal-content: 3;
|
||||
$color-modal-close-bg: #333;
|
||||
$color-modal-close-text: #fff;
|
||||
$color-modal-content-bg: #fff;
|
||||
$color-black-opacity-093: rgba(255, 255, 255, .93);
|
||||
$color-dark-grey: #222;
|
||||
*/
|
||||
|
||||
.u-body-modal-active {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
animation: modal-in .15s ease-out 0s backwards;
|
||||
}
|
||||
|
||||
.modal--active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.modal--exit {
|
||||
animation: modal-out .4s ease-out .4s forwards;
|
||||
}
|
||||
|
||||
.modal--exit .modal__content {
|
||||
animation: affordance-out .4s ease-in 0s forwards;
|
||||
}
|
||||
|
||||
.modal--exit .modal__close {
|
||||
animation: affordance-out-right .4s ease-in 0s forwards;
|
||||
}
|
||||
|
||||
|
||||
.modal__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, .93);
|
||||
}
|
||||
|
||||
|
||||
.modal__table {
|
||||
display: table;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal__center {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
animation: modal-in .15s ease-out .25s backwards;
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
max-width: 32em;
|
||||
min-width: 10.5em;
|
||||
min-height: 6em;
|
||||
padding: 1em 2em;
|
||||
background: #fff;
|
||||
animation: affordance-in .5s cubic-bezier(.075, .82, .165, 0) .3s backwards;
|
||||
}
|
||||
|
||||
|
||||
.modal__close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
padding: .9rem 1.35rem 1.1rem;
|
||||
font-size: 2em;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
animation: affordance-in-right .5s cubic-bezier(.075, .82, .165, 0) .25s backwards;
|
||||
}
|
||||
|
||||
.modal__close:hover,
|
||||
.modal__close:active {
|
||||
color: #fff;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Animation keyframes
|
||||
*/
|
||||
|
||||
@keyframes modal-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes affordance-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(5%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes affordance-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(5%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes affordance-in-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes affordance-out-right {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
(function(document, window) {
|
||||
window.wagtailConfig = window.wagtailConfig || {};
|
||||
wagtailConfig.api = {
|
||||
pages: '{% url "wagtailadmin_api_v1:pages:listing" %}',
|
||||
documents: '{% url "wagtailadmin_api_v1:documents:listing" %}',
|
||||
images: '{% url "wagtailadmin_api_v1:images:listing" %}'
|
||||
};
|
||||
wagtailConfig.urls = {
|
||||
pages: '{% url "wagtailadmin_explore_root" %}'
|
||||
};
|
||||
})(document, window);
|
||||
</script>
|
||||
<script src="{% static 'wagtailadmin/js/vendor/jquery-2.2.1.min.js' %}"></script>
|
||||
<script src="{% static 'wagtailadmin/js/vendor/jquery-ui-1.10.3.min.js' %}"></script>
|
||||
<script src="{% static 'wagtailadmin/js/vendor/jquery.datetimepicker.js' %}"></script>
|
||||
|
|
@ -27,6 +40,11 @@
|
|||
<script src="{% static 'wagtailadmin/js/core.js' %}"></script>
|
||||
{% hook_output 'insert_global_admin_js' %}
|
||||
|
||||
<script src="{% static 'wagtailadmin/js/common.js' %}"></script>
|
||||
<script src="{% static 'wagtailadmin/js/wagtailadmin.js' %}"></script>
|
||||
|
||||
|
||||
|
||||
{% main_nav_js %}
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue