pageflow-react 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.jshintrc +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +4 -0
- data/app/assets/javascripts/pageflow/react.js +8975 -0
- data/app/assets/javascripts/pageflow/react/components.js +4 -0
- data/app/views/pageflow/react/_widget.html.erb +1 -0
- data/app/views/pageflow/react/page.html.erb +7 -0
- data/js/.eslintrc +33 -0
- data/js/.gitignore +1 -0
- data/js/karma.conf.js +61 -0
- data/js/package.json +43 -0
- data/js/spec/.eslintrc +8 -0
- data/js/spec/components/background_image_spec.js +47 -0
- data/js/spec/components/page_thumbnail_spec.js +213 -0
- data/js/spec/create_container_spec.js +82 -0
- data/js/spec/resolve_spec.js +3 -0
- data/js/spec/resolvers/backbone_model_resolver_spec.js +256 -0
- data/js/spec/resolvers/create_recursive_resolver_spec.js +120 -0
- data/js/spec/resolvers/editor_file_ids_resolver_spec.js +49 -0
- data/js/spec/resolvers/i18n_resolver_spec.js +20 -0
- data/js/spec/resolvers/object_resolver_spec.js +165 -0
- data/js/spec/resolvers/page_type_resolver_spec.js +23 -0
- data/js/spec/resolvers/seed_resolver_spec.js +128 -0
- data/js/spec/stub_spec.js +16 -0
- data/js/spec/support/render_component.js +7 -0
- data/js/src/components/background_image.jsx +60 -0
- data/js/src/components/lazy_background_image.jsx +23 -0
- data/js/src/components/lazy_loaded_page_thumbnail.jsx +19 -0
- data/js/src/components/page_background.jsx +11 -0
- data/js/src/components/page_background_image.jsx +13 -0
- data/js/src/components/page_content.jsx +51 -0
- data/js/src/components/page_header.jsx +15 -0
- data/js/src/components/page_link.jsx +35 -0
- data/js/src/components/page_shadow.jsx +19 -0
- data/js/src/components/page_text.jsx +17 -0
- data/js/src/components/page_thumbnail.jsx +86 -0
- data/js/src/components/page_wrapper.jsx +11 -0
- data/js/src/components/scroller.js +43 -0
- data/js/src/create_container.jsx +53 -0
- data/js/src/create_page.jsx +38 -0
- data/js/src/create_page_component.jsx +45 -0
- data/js/src/create_page_type.js +57 -0
- data/js/src/create_resolver_root.jsx +21 -0
- data/js/src/create_widget.jsx +3 -0
- data/js/src/create_widget_type.js +12 -0
- data/js/src/index.js +69 -0
- data/js/src/mutate.js +17 -0
- data/js/src/mutations/mutation.js +5 -0
- data/js/src/mutations/update_page_link_mutation.js +30 -0
- data/js/src/mutations/update_page_mutation.js +19 -0
- data/js/src/resolve.js +45 -0
- data/js/src/resolvers/backbone_model_resolver.js +118 -0
- data/js/src/resolvers/create_recursive_resolver.js +20 -0
- data/js/src/resolvers/current_parent_page_resolver.js +38 -0
- data/js/src/resolvers/editor_chapter_resolver.js +10 -0
- data/js/src/resolvers/editor_file_ids_resolver.js +30 -0
- data/js/src/resolvers/editor_page_resolver.js +11 -0
- data/js/src/resolvers/i18n_resolver.js +11 -0
- data/js/src/resolvers/object_resolver.js +58 -0
- data/js/src/resolvers/page_type_resolver.js +12 -0
- data/js/src/resolvers/resolver.js +16 -0
- data/js/src/resolvers/seed_chapter_resolver.js +10 -0
- data/js/src/resolvers/seed_file_ids_resolver.js +11 -0
- data/js/src/resolvers/seed_page_resolver.js +11 -0
- data/js/src/resolvers/seed_resolver.js +75 -0
- data/js/src/utils/camelize.js +29 -0
- data/js/webpack.config.js +31 -0
- data/lib/pageflow-react.rb +13 -0
- data/lib/pageflow/react/engine.rb +15 -0
- data/lib/pageflow/react/page_type.rb +16 -0
- data/lib/pageflow/react/version.rb +5 -0
- data/lib/pageflow/react/widget_type.rb +21 -0
- data/pageflow-react.gemspec +29 -0
- metadata +205 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
import ObjectResolver from 'resolvers/object_resolver';
|
2
|
+
import Resolver from 'resolvers/resolver';
|
3
|
+
|
4
|
+
import sinon from 'sinon';
|
5
|
+
|
6
|
+
describe('ObjectResolver', () => {
|
7
|
+
class FakeResolver extends Resolver {
|
8
|
+
constructor(callback) {
|
9
|
+
super(callback);
|
10
|
+
this.dispose = sinon.spy();
|
11
|
+
}
|
12
|
+
|
13
|
+
get(props, seed) {
|
14
|
+
return `resolved value ${props.other} with ${seed}`;
|
15
|
+
}
|
16
|
+
|
17
|
+
triggerCallback() {
|
18
|
+
this._handleChange();
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
context('with flat fragment', () => {
|
23
|
+
it('it copies scalar values', () => {
|
24
|
+
var objectResolver = new ObjectResolver({});
|
25
|
+
|
26
|
+
var result = objectResolver.get({key: 123});
|
27
|
+
|
28
|
+
expect(result.key).to.eq(123)
|
29
|
+
});
|
30
|
+
|
31
|
+
it('resolves to null if null is passed as props', () => {
|
32
|
+
var objectResolver = new ObjectResolver({
|
33
|
+
key: (callback) => {
|
34
|
+
return new FakeResolver(callback);
|
35
|
+
}
|
36
|
+
});
|
37
|
+
|
38
|
+
var result = objectResolver.get(null);
|
39
|
+
|
40
|
+
expect(result).to.eq(null)
|
41
|
+
});
|
42
|
+
|
43
|
+
it('uses resolver in fragment to resolve value for key', () => {
|
44
|
+
var objectResolver = new ObjectResolver({
|
45
|
+
key: (callback) => {
|
46
|
+
return new FakeResolver(callback);
|
47
|
+
}
|
48
|
+
});
|
49
|
+
|
50
|
+
var result = objectResolver.get({other: 123}, 'SEED');
|
51
|
+
|
52
|
+
expect(result.key).to.eq('resolved value 123 with SEED');
|
53
|
+
});
|
54
|
+
|
55
|
+
it('invokes callback when some resolver signals a change', () => {
|
56
|
+
var callback = sinon.spy();
|
57
|
+
var resolver;
|
58
|
+
var objectResolver = new ObjectResolver({
|
59
|
+
key: (callback) => {
|
60
|
+
resolver = new FakeResolver(callback);
|
61
|
+
return resolver;
|
62
|
+
}
|
63
|
+
}, callback);
|
64
|
+
|
65
|
+
objectResolver.get({});
|
66
|
+
resolver.triggerCallback();
|
67
|
+
|
68
|
+
expect(callback).to.have.been.called;
|
69
|
+
});
|
70
|
+
});
|
71
|
+
|
72
|
+
context('with nested fragment', () => {
|
73
|
+
it('recreates structure with copied scalars ', () => {
|
74
|
+
var objectResolver = new ObjectResolver();
|
75
|
+
|
76
|
+
var result = objectResolver.get({
|
77
|
+
some: {
|
78
|
+
nested: {
|
79
|
+
key: 'hello'
|
80
|
+
}
|
81
|
+
}
|
82
|
+
});
|
83
|
+
|
84
|
+
expect(result.some.nested.key).to.eq('hello');
|
85
|
+
});
|
86
|
+
|
87
|
+
it('uses resolver in fragment to resolve nested key', () => {
|
88
|
+
var objectResolver = new ObjectResolver({
|
89
|
+
nested: {
|
90
|
+
key: (callback) => {
|
91
|
+
return new FakeResolver(callback);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
});
|
95
|
+
|
96
|
+
var result = objectResolver.get({nested: {other: 123}}, 'SEED');
|
97
|
+
|
98
|
+
expect(result.nested.key).to.eq('resolved value 123 with SEED');
|
99
|
+
});
|
100
|
+
|
101
|
+
it('keeps null event if resolver in nested fragment is present', () => {
|
102
|
+
var objectResolver = new ObjectResolver({
|
103
|
+
nested: {
|
104
|
+
key: (callback) => {
|
105
|
+
return new FakeResolver(callback);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
});
|
109
|
+
|
110
|
+
var result = objectResolver.get({nested: null}, 'SEED');
|
111
|
+
|
112
|
+
expect(result.nested).to.eq(null);
|
113
|
+
});
|
114
|
+
|
115
|
+
it('invokes callback when resolver of nested key signals a change', () => {
|
116
|
+
var callback = sinon.spy();
|
117
|
+
var resolver;
|
118
|
+
var objectResolver = new ObjectResolver({
|
119
|
+
nested: {
|
120
|
+
key: (callback) => {
|
121
|
+
resolver = new FakeResolver(callback);
|
122
|
+
return resolver;
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}, callback);
|
126
|
+
|
127
|
+
objectResolver.get({nested: {}});
|
128
|
+
resolver.triggerCallback();
|
129
|
+
|
130
|
+
expect(callback).to.have.been.called;
|
131
|
+
});
|
132
|
+
});
|
133
|
+
|
134
|
+
it('passes dispose to resolver created from fragment', () => {
|
135
|
+
var resolver;
|
136
|
+
var objectResolver = new ObjectResolver({
|
137
|
+
key: (callback) => {
|
138
|
+
resolver = new FakeResolver(callback);
|
139
|
+
return resolver;
|
140
|
+
}
|
141
|
+
});
|
142
|
+
|
143
|
+
objectResolver.get({});
|
144
|
+
objectResolver.dispose();
|
145
|
+
|
146
|
+
expect(resolver.dispose).to.have.been.called;
|
147
|
+
});
|
148
|
+
|
149
|
+
it('passes dispose to nested resolver', () => {
|
150
|
+
var resolver;
|
151
|
+
var objectResolver = new ObjectResolver({
|
152
|
+
nested: {
|
153
|
+
key: (callback) => {
|
154
|
+
resolver = new FakeResolver(callback);
|
155
|
+
return resolver;
|
156
|
+
}
|
157
|
+
}
|
158
|
+
});
|
159
|
+
|
160
|
+
objectResolver.get({nested: {}});
|
161
|
+
objectResolver.dispose();
|
162
|
+
|
163
|
+
expect(resolver.dispose).to.have.been.called;
|
164
|
+
});
|
165
|
+
});
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import PageTypeResolver from 'resolvers/page_type_resolver';
|
2
|
+
|
3
|
+
describe('PageTypeResolver', () => {
|
4
|
+
it('uses page type seed data', () => {
|
5
|
+
var seed = {page_types: {
|
6
|
+
audio_page: {some: 'data'}
|
7
|
+
}};
|
8
|
+
var pageTypeResolver = new PageTypeResolver();
|
9
|
+
|
10
|
+
var result = pageTypeResolver.get({type: 'audio_page'}, seed);
|
11
|
+
|
12
|
+
expect(result.some).to.eq('data');
|
13
|
+
});
|
14
|
+
|
15
|
+
it('sets name property to page type name', () => {
|
16
|
+
var seed = {page_types: {}};
|
17
|
+
var pageTypeResolver = new PageTypeResolver();
|
18
|
+
|
19
|
+
var result = pageTypeResolver.get({type: 'audio_page'}, seed);
|
20
|
+
|
21
|
+
expect(result.name).to.eq('audio_page');
|
22
|
+
});
|
23
|
+
});
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import SeedResolver from 'resolvers/seed_resolver';
|
2
|
+
|
3
|
+
describe('SeedResolver', () => {
|
4
|
+
it('gets props from seed data referenced by id', () => {
|
5
|
+
var seed = {pages: [{id: 1, title: 'Some title'}]};
|
6
|
+
var modelResolver = new SeedResolver({
|
7
|
+
seedProperty: 'pages',
|
8
|
+
attributesForProps: ['title'],
|
9
|
+
property: 'modelId'
|
10
|
+
});
|
11
|
+
|
12
|
+
var result = modelResolver.get({modelId: 1}, seed);
|
13
|
+
|
14
|
+
expect(result).to.deep.eq({title: 'Some title'})
|
15
|
+
});
|
16
|
+
|
17
|
+
it('can map attribute names', () => {
|
18
|
+
var seed = {pages: [{id: 1, template: 'video'}]};
|
19
|
+
var modelResolver = new SeedResolver({
|
20
|
+
seedProperty: 'pages',
|
21
|
+
attributesForProps: [['type', 'template']],
|
22
|
+
property: 'modelId'
|
23
|
+
});
|
24
|
+
|
25
|
+
var result = modelResolver.get({modelId: 1}, seed);
|
26
|
+
|
27
|
+
expect(result).to.deep.eq({type: 'video'})
|
28
|
+
});
|
29
|
+
|
30
|
+
it('camelizes attribute names', () => {
|
31
|
+
var seed = {pages: [{id: 1, background_image_id: 1}]};
|
32
|
+
var modelResolver = new SeedResolver({
|
33
|
+
seedProperty: 'pages',
|
34
|
+
attributesForProps: ['background_image_id'],
|
35
|
+
property: 'modelId'
|
36
|
+
});
|
37
|
+
|
38
|
+
var result = modelResolver.get({modelId: 1}, seed);
|
39
|
+
|
40
|
+
expect(result).to.deep.eq({backgroundImageId: 1})
|
41
|
+
});
|
42
|
+
|
43
|
+
it('gets null if property is missing', () => {
|
44
|
+
var modelResolver = new SeedResolver({
|
45
|
+
seedProperty: 'pages',
|
46
|
+
attributesForProps: ['title'],
|
47
|
+
property: 'modelId'
|
48
|
+
});
|
49
|
+
|
50
|
+
var result = modelResolver.get({}, {});
|
51
|
+
|
52
|
+
expect(result).to.eq(null)
|
53
|
+
});
|
54
|
+
|
55
|
+
it('gets null if seed is missing', () => {
|
56
|
+
var modelResolver = new SeedResolver({
|
57
|
+
seedProperty: 'pages',
|
58
|
+
attributesForProps: ['title'],
|
59
|
+
property: 'modelId'
|
60
|
+
});
|
61
|
+
|
62
|
+
var result = modelResolver.get({modelId: 1}, {});
|
63
|
+
|
64
|
+
expect(result).to.eq(null)
|
65
|
+
});
|
66
|
+
|
67
|
+
it('can use custom id attribute', () => {
|
68
|
+
var seed = {pages: [{perma_id: 1, title: 'Some title'}]};
|
69
|
+
var modelResolver = new SeedResolver({
|
70
|
+
seedProperty: 'pages',
|
71
|
+
attributesForProps: ['title'],
|
72
|
+
idAttribute: 'perma_id',
|
73
|
+
property: 'modelPermaId'
|
74
|
+
});
|
75
|
+
|
76
|
+
var result = modelResolver.get({modelPermaId: 1}, seed);
|
77
|
+
|
78
|
+
expect(result).to.deep.eq({title: 'Some title'})
|
79
|
+
});
|
80
|
+
|
81
|
+
context('with includeConfiguration option', () => {
|
82
|
+
it('includes attributes from a nested configuration object', () => {
|
83
|
+
var seed = {
|
84
|
+
pages: [
|
85
|
+
{id: 1, title: 'Some title', configuration: {setting: true}}
|
86
|
+
]
|
87
|
+
};
|
88
|
+
var modelResolver = new SeedResolver({
|
89
|
+
seedProperty: 'pages',
|
90
|
+
attributesForProps: ['title'],
|
91
|
+
includeConfiguration: true,
|
92
|
+
property: 'modelId'
|
93
|
+
});
|
94
|
+
|
95
|
+
var result = modelResolver.get({modelId: 1}, seed);
|
96
|
+
|
97
|
+
expect(result).to.deep.eq({title: 'Some title', setting: true})
|
98
|
+
});
|
99
|
+
|
100
|
+
it('deeply camelizes configuration attribute names', () => {
|
101
|
+
var seed = {
|
102
|
+
pages: [
|
103
|
+
{
|
104
|
+
id: 1,
|
105
|
+
title: 'Some title',
|
106
|
+
configuration: {
|
107
|
+
page_links: [
|
108
|
+
{
|
109
|
+
image_id: 1
|
110
|
+
}
|
111
|
+
]
|
112
|
+
}
|
113
|
+
}
|
114
|
+
]
|
115
|
+
};
|
116
|
+
var modelResolver = new SeedResolver({
|
117
|
+
seedProperty: 'pages',
|
118
|
+
attributesForProps: ['title'],
|
119
|
+
includeConfiguration: true,
|
120
|
+
property: 'modelId'
|
121
|
+
});
|
122
|
+
|
123
|
+
var result = modelResolver.get({modelId: 1}, seed);
|
124
|
+
|
125
|
+
expect(result).to.deep.eq({title: 'Some title', pageLinks: [{imageId: 1}]})
|
126
|
+
});
|
127
|
+
});
|
128
|
+
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
//import PageHeader from 'components/page-header.jsx';
|
2
|
+
//import React from 'react/addons';
|
3
|
+
//
|
4
|
+
//const {renderIntoDocument} = React.addons.TestUtils
|
5
|
+
//
|
6
|
+
//describe('stub', () => {
|
7
|
+
// it('passes', () => {
|
8
|
+
// expect(PageHeader).to.be.a('function');
|
9
|
+
// });
|
10
|
+
//
|
11
|
+
// it('renders', () => {
|
12
|
+
// var result = React.renderToStaticMarkup(<PageHeader text="Hello" />);
|
13
|
+
//
|
14
|
+
// expect(result).to.eq('<h1>Hello</h1>'); y
|
15
|
+
// });
|
16
|
+
//});
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import TestUtils from 'react-addons-test-utils';
|
2
|
+
|
3
|
+
export default function(component, props, ...children) {
|
4
|
+
const shallowRenderer = TestUtils.createRenderer();
|
5
|
+
shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0]));
|
6
|
+
return shallowRenderer.getRenderOutput();
|
7
|
+
};
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import classNames from 'classnames';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Display an element with a background image.
|
6
|
+
*/
|
7
|
+
export default class BackgroundImage extends React.Component {
|
8
|
+
static propTypes = {
|
9
|
+
/** The id of the image file to display */
|
10
|
+
imageFileId: React.PropTypes.number,
|
11
|
+
|
12
|
+
/** Background position */
|
13
|
+
position: React.PropTypes.arrayOf(React.PropTypes.number),
|
14
|
+
|
15
|
+
/** Additional CSS classes. */
|
16
|
+
className: React.PropTypes.string,
|
17
|
+
|
18
|
+
/** Used to lazy load images. */
|
19
|
+
loaded: React.PropTypes.bool
|
20
|
+
}
|
21
|
+
|
22
|
+
static defaultProps = {
|
23
|
+
position: [50, 50]
|
24
|
+
}
|
25
|
+
|
26
|
+
render() {
|
27
|
+
return (
|
28
|
+
<div className={this.cssClass()} style={this.style()}>
|
29
|
+
</div>
|
30
|
+
);
|
31
|
+
}
|
32
|
+
|
33
|
+
cssClass() {
|
34
|
+
return classNames(
|
35
|
+
this.props.className,
|
36
|
+
this.props.loaded ? 'load_image' : null,
|
37
|
+
this.imageCssClass()
|
38
|
+
);
|
39
|
+
}
|
40
|
+
|
41
|
+
imageCssClass() {
|
42
|
+
return ['image', this.props.imageFileId || 'none'].join('_');
|
43
|
+
}
|
44
|
+
|
45
|
+
style() {
|
46
|
+
return {
|
47
|
+
backgroundPosition: `${this.positionCoordinate(0)}% ${this.positionCoordinate(1)}%`
|
48
|
+
};
|
49
|
+
}
|
50
|
+
|
51
|
+
positionCoordinate(index) {
|
52
|
+
var coordinate = this.props.position[index];
|
53
|
+
|
54
|
+
if (typeof coordinate === 'undefined') {
|
55
|
+
return 50;
|
56
|
+
}
|
57
|
+
|
58
|
+
return coordinate;
|
59
|
+
}
|
60
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import createPageComponent from '../create_page_component.jsx';
|
4
|
+
|
5
|
+
import BackgroundImage from './background_image.jsx';
|
6
|
+
|
7
|
+
class LazyBackgroundImage extends React.Component {
|
8
|
+
static contextTypes = {
|
9
|
+
pageIsPreloaded: React.PropTypes.bool
|
10
|
+
}
|
11
|
+
|
12
|
+
constructor(props) {
|
13
|
+
super(props);
|
14
|
+
}
|
15
|
+
|
16
|
+
render() {
|
17
|
+
return (
|
18
|
+
<BackgroundImage {...this.props} loaded={this.context.pageIsPreloaded} />
|
19
|
+
);
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
export default createPageComponent(LazyBackgroundImage);
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import createPageComponent from '../create_page_component.jsx';
|
4
|
+
|
5
|
+
import PageThumbnail from './page_thumbnail.jsx';
|
6
|
+
|
7
|
+
class LazyLoadedPageThumbnail extends React.Component {
|
8
|
+
static contextTypes = {
|
9
|
+
pageIsPreloaded: React.PropTypes.bool
|
10
|
+
}
|
11
|
+
|
12
|
+
render() {
|
13
|
+
return (
|
14
|
+
<PageThumbnail {...this.props} lazy={true} loaded={this.context.pageIsPreloaded} />
|
15
|
+
);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
export default createPageComponent(LazyLoadedPageThumbnail);
|