pageflow-react 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.jshintrc +15 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +40 -0
  8. data/Rakefile +4 -0
  9. data/app/assets/javascripts/pageflow/react.js +8975 -0
  10. data/app/assets/javascripts/pageflow/react/components.js +4 -0
  11. data/app/views/pageflow/react/_widget.html.erb +1 -0
  12. data/app/views/pageflow/react/page.html.erb +7 -0
  13. data/js/.eslintrc +33 -0
  14. data/js/.gitignore +1 -0
  15. data/js/karma.conf.js +61 -0
  16. data/js/package.json +43 -0
  17. data/js/spec/.eslintrc +8 -0
  18. data/js/spec/components/background_image_spec.js +47 -0
  19. data/js/spec/components/page_thumbnail_spec.js +213 -0
  20. data/js/spec/create_container_spec.js +82 -0
  21. data/js/spec/resolve_spec.js +3 -0
  22. data/js/spec/resolvers/backbone_model_resolver_spec.js +256 -0
  23. data/js/spec/resolvers/create_recursive_resolver_spec.js +120 -0
  24. data/js/spec/resolvers/editor_file_ids_resolver_spec.js +49 -0
  25. data/js/spec/resolvers/i18n_resolver_spec.js +20 -0
  26. data/js/spec/resolvers/object_resolver_spec.js +165 -0
  27. data/js/spec/resolvers/page_type_resolver_spec.js +23 -0
  28. data/js/spec/resolvers/seed_resolver_spec.js +128 -0
  29. data/js/spec/stub_spec.js +16 -0
  30. data/js/spec/support/render_component.js +7 -0
  31. data/js/src/components/background_image.jsx +60 -0
  32. data/js/src/components/lazy_background_image.jsx +23 -0
  33. data/js/src/components/lazy_loaded_page_thumbnail.jsx +19 -0
  34. data/js/src/components/page_background.jsx +11 -0
  35. data/js/src/components/page_background_image.jsx +13 -0
  36. data/js/src/components/page_content.jsx +51 -0
  37. data/js/src/components/page_header.jsx +15 -0
  38. data/js/src/components/page_link.jsx +35 -0
  39. data/js/src/components/page_shadow.jsx +19 -0
  40. data/js/src/components/page_text.jsx +17 -0
  41. data/js/src/components/page_thumbnail.jsx +86 -0
  42. data/js/src/components/page_wrapper.jsx +11 -0
  43. data/js/src/components/scroller.js +43 -0
  44. data/js/src/create_container.jsx +53 -0
  45. data/js/src/create_page.jsx +38 -0
  46. data/js/src/create_page_component.jsx +45 -0
  47. data/js/src/create_page_type.js +57 -0
  48. data/js/src/create_resolver_root.jsx +21 -0
  49. data/js/src/create_widget.jsx +3 -0
  50. data/js/src/create_widget_type.js +12 -0
  51. data/js/src/index.js +69 -0
  52. data/js/src/mutate.js +17 -0
  53. data/js/src/mutations/mutation.js +5 -0
  54. data/js/src/mutations/update_page_link_mutation.js +30 -0
  55. data/js/src/mutations/update_page_mutation.js +19 -0
  56. data/js/src/resolve.js +45 -0
  57. data/js/src/resolvers/backbone_model_resolver.js +118 -0
  58. data/js/src/resolvers/create_recursive_resolver.js +20 -0
  59. data/js/src/resolvers/current_parent_page_resolver.js +38 -0
  60. data/js/src/resolvers/editor_chapter_resolver.js +10 -0
  61. data/js/src/resolvers/editor_file_ids_resolver.js +30 -0
  62. data/js/src/resolvers/editor_page_resolver.js +11 -0
  63. data/js/src/resolvers/i18n_resolver.js +11 -0
  64. data/js/src/resolvers/object_resolver.js +58 -0
  65. data/js/src/resolvers/page_type_resolver.js +12 -0
  66. data/js/src/resolvers/resolver.js +16 -0
  67. data/js/src/resolvers/seed_chapter_resolver.js +10 -0
  68. data/js/src/resolvers/seed_file_ids_resolver.js +11 -0
  69. data/js/src/resolvers/seed_page_resolver.js +11 -0
  70. data/js/src/resolvers/seed_resolver.js +75 -0
  71. data/js/src/utils/camelize.js +29 -0
  72. data/js/webpack.config.js +31 -0
  73. data/lib/pageflow-react.rb +13 -0
  74. data/lib/pageflow/react/engine.rb +15 -0
  75. data/lib/pageflow/react/page_type.rb +16 -0
  76. data/lib/pageflow/react/version.rb +5 -0
  77. data/lib/pageflow/react/widget_type.rb +21 -0
  78. data/pageflow-react.gemspec +29 -0
  79. 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);