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,4 @@
1
+ //= require_self
2
+ //= require pageflow/react
3
+
4
+ var PAGEFLOW_EDITOR = false;
@@ -0,0 +1 @@
1
+ <%= content_tag(:div, '', data: {widget: name}) %>
@@ -0,0 +1,7 @@
1
+ <% if page.template.present? %>
2
+ <% concat React::ServerRendering.render(
3
+ Pageflow.config.page_types.find_by_name!(page.template).component_name, MultiJson.dump(
4
+ resolverSeed: @_pageflow_react_entry_seed ||= entry_seed(@entry),
5
+ pageId: page.perma_id
6
+ ), true) %>
7
+ <% end %>
data/js/.eslintrc ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "parser": "babel-eslint",
3
+ "plugins": ["react"],
4
+ "env": {
5
+ "browser": true,
6
+ "es6": true,
7
+ "node": true
8
+ },
9
+ "ecmaFeatures": {
10
+ "arrowFunctions": true,
11
+ "blockBindings": true,
12
+ "classes": true,
13
+ "defaultParams": true,
14
+ "destructuring": true,
15
+ "forOf": true,
16
+ "generators": true,
17
+ "modules": true,
18
+ "spread": true,
19
+ "templateStrings": true,
20
+ "jsx": true
21
+ },
22
+ "rules": {
23
+ "no-undef": [2],
24
+ "consistent-return": [0],
25
+ "key-spacing": [0],
26
+ "quotes": [0],
27
+ "new-cap": [0],
28
+ "no-multi-spaces": [0],
29
+ "no-shadow": [0],
30
+ "no-unused-vars": [1],
31
+ "no-use-before-define": [2, "nofunc"]
32
+ }
33
+ }
data/js/.gitignore ADDED
@@ -0,0 +1 @@
1
+ /node_modules
data/js/karma.conf.js ADDED
@@ -0,0 +1,61 @@
1
+ var webpack = require('webpack');
2
+ var path = require('path');
3
+
4
+ module.exports = function (config) {
5
+ config.set({
6
+ browsers: ['PhantomJS'],
7
+ singleRun: false,
8
+ frameworks: ['mocha', 'chai', 'chai-sinon', 'phantomjs-shim'],
9
+ files: [
10
+ 'node_modules/babel-polyfill/dist/polyfill.js',
11
+ 'spec/*spec.js',
12
+ 'spec/**/*spec.js'
13
+ ],
14
+ exclude: [
15
+ 'flycheck_*.js',
16
+ '**/flycheck_*.js',
17
+ '*/**/flycheck_*.js',
18
+ ],
19
+ preprocessors: {
20
+ 'spec/*spec.js': ['webpack', 'sourcemap'],
21
+ 'spec/**/*spec.js': ['webpack', 'sourcemap']
22
+ },
23
+ reporters: ['dots'],
24
+ plugins: [
25
+ 'karma-webpack',
26
+ 'karma-sourcemap-loader',
27
+ 'karma-mocha',
28
+ 'karma-chai',
29
+ 'karma-chai-sinon',
30
+ require('karma-phantomjs-shim'),
31
+ 'karma-phantomjs-launcher'
32
+ ],
33
+ webpack: {
34
+ devtool: 'inline-source-map',
35
+ plugins: [
36
+ new webpack.DefinePlugin({
37
+ PAGEFLOW_EDITOR: false
38
+ })
39
+ ],
40
+ module: {
41
+ loaders: [
42
+ {
43
+ test: /\.jsx?$/,
44
+ exclude: /node_modules/,
45
+ loader: 'babel-loader?stage=0&optional[]=runtime'
46
+ },
47
+ {
48
+ test: require.resolve('react'),
49
+ loader: 'expose?React'
50
+ }
51
+ ]
52
+ },
53
+ resolve: {
54
+ root: path.resolve('./src')
55
+ }
56
+ },
57
+ webpackServer: {
58
+ noInfo: true
59
+ }
60
+ });
61
+ };
data/js/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "pageflow-react",
3
+ "version": "1.0.0",
4
+ "description": "TODO: Write a gem description",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "devDependencies": {
12
+ "babel-core": "^5.8.22",
13
+ "babel-loader": "^5.3.2",
14
+ "backbone": "^1.2.2",
15
+ "chai": "^3.2.0",
16
+ "expose-loader": "^0.7.1",
17
+ "imports-loader": "^0.6.4",
18
+ "karma": "^0.13.9",
19
+ "karma-chai": "^0.1.0",
20
+ "karma-chai-sinon": "^0.1.5",
21
+ "karma-es5-shim": "0.0.4",
22
+ "karma-mocha": "^0.2.0",
23
+ "karma-phantomjs-launcher": "^0.2.1",
24
+ "karma-phantomjs-shim": "^1.0.0",
25
+ "karma-sourcemap-loader": "^0.3.5",
26
+ "karma-webpack": "^1.7.0",
27
+ "mocha": "^2.2.5",
28
+ "phantomjs": "^1.9.18",
29
+ "react-addons-test-utils": "^0.14.7",
30
+ "sinon": "git://github.com/cjohansen/Sinon.JS#b672042043517b9f84e14ed0fb8265126168778a",
31
+ "sinon-chai": "^2.8.0",
32
+ "underscore": "^1.8.3",
33
+ "webpack": "^1.11.0"
34
+ },
35
+ "dependencies": {
36
+ "babel-polyfill": "^6.2.0",
37
+ "babel-runtime": "^5.8.20",
38
+ "classnames": "^2.1.3",
39
+ "react": "^0.14.3",
40
+ "react-dom": "^0.14.3",
41
+ "react-draggable": "git://github.com/tf/react-draggable#on-drag-fix"
42
+ }
43
+ }
data/js/spec/.eslintrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "env": {
3
+ "mocha": true
4
+ },
5
+ "globals": {
6
+ "expect": true
7
+ }
8
+ }
@@ -0,0 +1,47 @@
1
+ import BackgroundImage from 'components/background_image.jsx';
2
+
3
+ import renderComponent from '../support/render_component';
4
+
5
+ describe('BackgroundImage', () => {
6
+ it('has image file css class', () => {
7
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5});
8
+
9
+ expect(backgroundImage.props.className.split(' ')).to.contain('image_5');
10
+ });
11
+
12
+ it('has image_none css class if not imageFileId is given', () => {
13
+ var backgroundImage = renderComponent(BackgroundImage);
14
+
15
+ expect(backgroundImage.props.className.split(' ')).to.contain('image_none');
16
+ });
17
+
18
+ it('has load_image css class if loaded prop is present', () => {
19
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5, loaded: true});
20
+
21
+ expect(backgroundImage.props.className.split(' ')).to.contain('load_image');
22
+ });
23
+
24
+ it('has image file css class', () => {
25
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5});
26
+
27
+ expect(backgroundImage.props.className.split(' ')).to.contain('image_5');
28
+ });
29
+
30
+ it('defaults background position to center', () => {
31
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5});
32
+
33
+ expect(backgroundImage.props.style.backgroundPosition).to.eq('50% 50%');
34
+ });
35
+
36
+ it('defaults background position to center', () => {
37
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5, position: [undefined, undefined]});
38
+
39
+ expect(backgroundImage.props.style.backgroundPosition).to.eq('50% 50%');
40
+ });
41
+
42
+ it('sets background position inline styles', () => {
43
+ var backgroundImage = renderComponent(BackgroundImage, {imageFileId: 5, position: [10, 20]});
44
+
45
+ expect(backgroundImage.props.style.backgroundPosition).to.eq('10% 20%');
46
+ });
47
+ });
@@ -0,0 +1,213 @@
1
+ import {PageThumbnail} from 'components/page_thumbnail.jsx';
2
+
3
+ import renderComponent from '../support/render_component';
4
+
5
+ describe('PageThumbnail', () => {
6
+ it('has class names for thumbnail candidate', () => {
7
+ const page = {
8
+ type: {
9
+ thumbnailCandidates: [
10
+ {
11
+ attribute: 'thumbnailFileId',
12
+ collectionName: 'image_files',
13
+ cssClassPrefix: 'pageflow_image_file'
14
+ }
15
+ ]
16
+ },
17
+ thumbnailFileId: 10
18
+ };
19
+ const fileIds = {
20
+ 'image_files': [10]
21
+ };
22
+
23
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds, imageStyle: 'thumbnail'});
24
+ const className = pageThumbnail.props.className;
25
+
26
+ expect(className).to.contain('pageflow_image_file_thumbnail_10');
27
+ });
28
+
29
+ it('supports lazy thumbnail css class', () => {
30
+ const page = {
31
+ type: {
32
+ thumbnailCandidates: [
33
+ {
34
+ attribute: 'thumbnailFileId',
35
+ collectionName: 'image_files',
36
+ cssClassPrefix: 'pageflow_image_file'
37
+ }
38
+ ]
39
+ },
40
+ thumbnailFileId: 10
41
+ };
42
+ const fileIds = {
43
+ 'image_files': [10]
44
+ };
45
+
46
+ const pageThumbnail = renderComponent(PageThumbnail, {
47
+ page,
48
+ fileIds,
49
+ imageStyle: 'thumbnail',
50
+ lazy: true
51
+ });
52
+ const className = pageThumbnail.props.className;
53
+
54
+ expect(className).to.contain('lazy_pageflow_image_file_thumbnail_10');
55
+ });
56
+
57
+ it('adds load_image class if loaded prop is present', () => {
58
+ const page = {
59
+ type: {
60
+ thumbnailCandidates: [
61
+ {
62
+ attribute: 'thumbnailFileId',
63
+ collectionName: 'image_files',
64
+ cssClassPrefix: 'pageflow_image_file'
65
+ }
66
+ ]
67
+ },
68
+ thumbnailFileId: 10
69
+ };
70
+ const fileIds = {
71
+ 'image_files': [10]
72
+ };
73
+
74
+ const pageThumbnail = renderComponent(PageThumbnail, {
75
+ page,
76
+ fileIds,
77
+ imageStyle: 'thumbnail',
78
+ lazy: true,
79
+ loaded: true
80
+ });
81
+ const className = pageThumbnail.props.className;
82
+
83
+ expect(className).to.contain('load_image');
84
+ });
85
+
86
+ it('skips candidates whose attributes point to non existing files', () => {
87
+ const page = {
88
+ type: {
89
+ thumbnailCandidates: [
90
+ {
91
+ attribute: 'thumbnailFileId',
92
+ collectionName: 'image_files',
93
+ cssClassPrefix: 'pageflow_image_file'
94
+ },
95
+ {
96
+ attribute: 'videoId',
97
+ collectionName: 'video_files',
98
+ cssClassPrefix: 'pageflow_video_file'
99
+ }
100
+ ]
101
+ },
102
+ thumbnailFileId: 10,
103
+ videoId: 5
104
+ };
105
+ const fileIds = {
106
+ 'image_files': [],
107
+ 'video_files': [5, 10],
108
+ };
109
+
110
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds, imageStyle: 'thumbnail'});
111
+ const className = pageThumbnail.props.className;
112
+
113
+ expect(className).to.contain('pageflow_video_file_thumbnail_5');
114
+ });
115
+
116
+ it('skips candidates whose attributes are not defined', () => {
117
+ const page = {
118
+ type: {
119
+ thumbnailCandidates: [
120
+ {
121
+ attribute: 'thumbnailFileId',
122
+ collectionName: 'image_files',
123
+ cssClassPrefix: 'pageflow_image_file'},
124
+ ]
125
+ },
126
+ };
127
+ const fileIds = {};
128
+
129
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds});
130
+ const className = pageThumbnail.props.className;
131
+
132
+ expect(className).not.to.contain('pageflow_image_file');
133
+ });
134
+
135
+ it('prefers custom thumbnail id', () => {
136
+ const page = {
137
+ type: {
138
+ thumbnailCandidates: [
139
+ {
140
+ attribute: 'thumbnailFileId',
141
+ collectionName: 'image_files',
142
+ cssClassPrefix: 'pageflow_image_file'
143
+ }
144
+ ]
145
+ },
146
+ thumbnailFileId: 10
147
+ };
148
+ const fileIds = {
149
+ 'image_files': [10, 11]
150
+ };
151
+
152
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds, customThumbnailId: 11, imageStyle: 'thumbnail'});
153
+ const className = pageThumbnail.props.className;
154
+
155
+ expect(className).to.contain('pageflow_image_file_thumbnail_11');
156
+ });
157
+
158
+ it('skips custom thumbnail id pointing to non existent file', () => {
159
+ const page = {
160
+ type: {
161
+ thumbnailCandidates: []
162
+ }
163
+ };
164
+ const fileIds = {
165
+ 'image_files': []
166
+ };
167
+
168
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds, customThumbnailId: 11, imageStyle: 'thumbnail'});
169
+ const className = pageThumbnail.props.className;
170
+
171
+ expect(className).not.to.contain('pageflow_image_file_thumbnail_11');
172
+ });
173
+
174
+ it('takes className prop', () => {
175
+ const page = {
176
+ type: {
177
+ thumbnailCandidates: []
178
+ }
179
+ };
180
+ const fileIds = {};
181
+
182
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds, className: 'custom'});
183
+ const className = pageThumbnail.props.className;
184
+
185
+ expect(className).to.contain('custom');
186
+ });
187
+
188
+ it('sets page type modifier class name', () => {
189
+ const page = {
190
+ type: {
191
+ name: 'video',
192
+ thumbnailCandidates: []
193
+ }
194
+ };
195
+ const fileIds = {};
196
+
197
+ const pageThumbnail = renderComponent(PageThumbnail, {page, fileIds});
198
+ const className = pageThumbnail.props.className;
199
+
200
+ expect(className).to.contain('is_video');
201
+ });
202
+
203
+ context('when page is null', function() {
204
+ it('sets is_dangling class name', () => {
205
+ const fileIds = {};
206
+
207
+ const pageThumbnail = renderComponent(PageThumbnail, {page: null, fileIds});
208
+ const className = pageThumbnail.props.className;
209
+
210
+ expect(className).to.contain('is_dangling');
211
+ });
212
+ });
213
+ })
@@ -0,0 +1,82 @@
1
+ import createContainer from 'create_container.jsx';
2
+ import Resolver from 'resolvers/resolver';
3
+
4
+ import React from 'react';
5
+ import renderComponent from './support/render_component';
6
+
7
+ import TestUtils from 'react-addons-test-utils';
8
+
9
+ describe('createContainer', () => {
10
+ it('creates a wrapper component that passes props', () => {
11
+ var Component = class extends React.Component {};
12
+
13
+ var Container = createContainer(Component);
14
+ var result = renderComponent(Container, {some: 'prop'});
15
+
16
+ expect(result.type).to.eq(Component);
17
+ expect(result.props.some).to.eq('prop');
18
+ });
19
+
20
+ it('resolved fragments with given resolver', () => {
21
+ var Component = class extends React.Component {};
22
+ var FakeResolver = class extends Resolver {
23
+ get(props) {
24
+ return 'resolved page ' + props.targetPageId;
25
+ }
26
+ };
27
+
28
+ var Container = createContainer(Component, {
29
+ fragments: {
30
+ pageLink: {
31
+ targetPage: (callback) => {
32
+ return new FakeResolver(callback)
33
+ }
34
+ }
35
+ }
36
+ });
37
+ var result = renderComponent(Container, {
38
+ pageLink: {
39
+ targetPageId: 100
40
+ }
41
+ });
42
+
43
+ expect(result.props.pageLink.targetPage).to.eq('resolved page 100');
44
+ });
45
+
46
+ it('rerenders component when resolver invokes callback', () => {
47
+ var Component = class extends React.Component {};
48
+ var FakeResolver = class extends Resolver {
49
+ get(props) {
50
+ return `resolved page ${this.data}`;
51
+ }
52
+ };
53
+ var resolver;
54
+
55
+ var Container = createContainer(Component, {
56
+ fragments: {
57
+ pageLink: {
58
+ targetPage: (callback) => {
59
+ resolver = new FakeResolver(callback);
60
+ return resolver;
61
+ }
62
+ }
63
+ }
64
+ });
65
+
66
+ const shallowRenderer = TestUtils.createRenderer();
67
+ const component = React.createElement(Container, {
68
+ pageLink: {
69
+ targetPageId: 100
70
+ }
71
+ });
72
+
73
+ shallowRenderer.render(component);
74
+
75
+ resolver.data = 2;
76
+ resolver._handleChange();
77
+
78
+ var result = shallowRenderer.getRenderOutput();
79
+
80
+ expect(result.props.pageLink.targetPage).to.eq('resolved page 2');
81
+ });
82
+ });