markuapad 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +3 -0
  3. data/.gitignore +30 -0
  4. data/.jshintrc +4 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +21 -0
  8. data/README.md +30 -0
  9. data/Rakefile +1 -0
  10. data/app/assets/javascripts/markuapad.js +72 -0
  11. data/app/assets/stylesheets/markuapad.css +1 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/bower.json +30 -0
  15. data/build/app.css +1 -0
  16. data/build/app.js +72 -0
  17. data/build/browser-polyfill.min.js +3 -0
  18. data/build/dist.css +1 -0
  19. data/build/dist.js +72 -0
  20. data/build/index.css +1 -0
  21. data/build/index.html +30 -0
  22. data/build/index.js +1 -0
  23. data/build/web.css +1 -0
  24. data/build/web.js +72 -0
  25. data/lib/markuapad.rb +5 -0
  26. data/lib/markuapad/engine.rb +4 -0
  27. data/lib/markuapad/version.rb +3 -0
  28. data/markuapad.gemspec +32 -0
  29. data/package.json +55 -0
  30. data/prepare_gem.sh +3 -0
  31. data/src/dist.js +7 -0
  32. data/src/file_accessor.js +77 -0
  33. data/src/index.js +180 -0
  34. data/src/jsx/editor.jsx +122 -0
  35. data/src/jsx/file_browser.jsx +252 -0
  36. data/src/jsx/file_browser_list_item.jsx +86 -0
  37. data/src/jsx/image_modal.jsx +49 -0
  38. data/src/jsx/live_preview.jsx +36 -0
  39. data/src/jsx/main.jsx +145 -0
  40. data/src/jsx/preview.jsx +32 -0
  41. data/src/jsx/toolbar.jsx +24 -0
  42. data/src/markuapad.js +39 -0
  43. data/src/styles/_editor.scss +27 -0
  44. data/src/styles/_extendables.scss +115 -0
  45. data/src/styles/_file_browser.scss +160 -0
  46. data/src/styles/_grid-settings.scss +13 -0
  47. data/src/styles/_layout.scss +18 -0
  48. data/src/styles/_modal.scss +145 -0
  49. data/src/styles/_preview.scss +65 -0
  50. data/src/styles/_toolbar.scss +99 -0
  51. data/src/styles/_variables.scss +25 -0
  52. data/src/styles/_workspace.scss +31 -0
  53. data/src/styles/app.scss +52 -0
  54. data/src/styles/index.scss +41 -0
  55. data/src/util.js +43 -0
  56. data/src/web.js +4 -0
  57. data/test/editor.es6 +6 -0
  58. data/webpack.config.js +48 -0
  59. metadata +129 -0
@@ -0,0 +1,122 @@
1
+ import React from "react";
2
+ import ace from "brace";
3
+ import javascript from "brace/mode/markdown";
4
+ import monokai from "brace/theme/xcode";
5
+ import emacsKeys from "brace/keybinding/emacs"
6
+ import FileAccessor from "../file_accessor";
7
+ import _ from "underscore";
8
+
9
+ class Editor extends React.Component {
10
+ constructor(props) {
11
+ super(props);
12
+ this.onCurrentFileLoaded = this.onCurrentFileLoaded.bind(this);
13
+
14
+ // Make the editor changed function not spammy
15
+ this.onEditorChanged = _.debounce(this.onEditorChanged.bind(this), 100);
16
+ this.onCursorChanged = this.onCursorChanged.bind(this)
17
+ }
18
+
19
+ // Setup all the editor options when we mount.
20
+ componentDidMount() {
21
+ this.setupEditor();
22
+ }
23
+
24
+ // If we get a file change from outside our domain, switch to it.
25
+ // TODO: maybe show a merge warning or something?
26
+ componentDidUpdate(lastProps, lastState) {
27
+ if (lastProps.currentFile !== this.props.currentFile) {
28
+ this.setupEditor();
29
+ }
30
+ }
31
+
32
+ setupEditor() {
33
+ if (!this.props.currentFile) return;
34
+
35
+ // Setup the ace editor
36
+ this.editor = ace.edit(this.refs.editor.getDOMNode());
37
+ this.editor.getSession().setMode('ace/mode/markdown');
38
+ this.editor.setTheme('ace/theme/xcode');
39
+ this.editor.setKeyboardHandler('ace/keyboard/emacs');
40
+ this.editor.renderer.setShowGutter(false);
41
+ this.editor.renderer.setPadding(20);
42
+ this.editor.renderer.setScrollMargin(20);
43
+ this.editor.setShowPrintMargin(false);
44
+ this.editor.setFontSize(14);
45
+ this.editor.$blockScrolling = Infinity;
46
+ this.editor.session.setUseWrapMode(true);
47
+ this.editor.setHighlightActiveLine(false);
48
+
49
+ // When the editor changes, alert us
50
+ this.editor.on("change", this.onEditorChanged)
51
+ this.editor.getSession().selection.on("changeCursor", this.onCursorChanged)
52
+
53
+ // Get the current file to put in the ace editor
54
+ FileAccessor.get(this.props.currentFile.filename, this.onCurrentFileLoaded, this.props.currentFile.type);
55
+ }
56
+
57
+ // When someone changes the cursor, we need to tell the file that we have a changed cursor
58
+ onCursorChanged() {
59
+ let count = 0,
60
+ position = this.editor.getCursorPosition(),
61
+ i = 0,
62
+ src = this.editor.getValue();
63
+
64
+ while(count < position.row && (i = src.indexOf('\n', i) + 1)) { count++; }
65
+ FileAccessor.setCursor(this.props.currentFile.filename, i);
66
+ }
67
+
68
+ // When the editor value changes, then we have to set our current state,
69
+ // then update the file through the data store.
70
+ onEditorChanged() {
71
+ let value = this.editor.getValue();
72
+
73
+ if (value === this.state.currentFileValue)
74
+ return
75
+ else {
76
+ this.setState({ currentFileValue: value })
77
+ FileAccessor.save(this.props.currentFile.filename, this.props.currentFile.type, value, this.props.onBookContentChanged);
78
+ }
79
+ }
80
+
81
+ // After we load a file, set the value of the editor to it
82
+ onCurrentFileLoaded(error, contents) {
83
+ if (error)
84
+ return console.error(error)
85
+ else {
86
+ // Focus the editor
87
+ this.editor.focus;
88
+
89
+ // Set the value of it
90
+ this.setState({ currentFileValue: contents }, () => this.editor.setValue(contents, -1));
91
+ }
92
+ }
93
+
94
+ renderEditor() {
95
+ return (
96
+ <section ref="editor" className="editor"></section>
97
+ );
98
+ }
99
+
100
+ renderNoFileHelp() {
101
+ return (
102
+ <section className="editor nofile">
103
+ <h3>No file selected.</h3>
104
+ <p>Click one in the list on the left to start editing</p>
105
+ </section>
106
+ );
107
+ }
108
+
109
+ render() {
110
+ return (
111
+ this.props.currentFile ? this.renderEditor() : this.renderNoFileHelp()
112
+ );
113
+ }
114
+ }
115
+
116
+ Editor.propTypes = {
117
+ currentFile: React.PropTypes.object,
118
+ inLiveMode: React.PropTypes.bool.isRequired,
119
+ onBookContentChanged: React.PropTypes.func.isRequired
120
+ }
121
+
122
+ export default Editor;
@@ -0,0 +1,252 @@
1
+ import React from "react";
2
+ import _ from "underscore";
3
+ import FileBrowserListItem from "./file_browser_list_item";
4
+ import FileAccessor from "../file_accessor";
5
+ import HTML5Backend from 'react-dnd/modules/backends/HTML5';
6
+ import { DragDropContext } from 'react-dnd';
7
+
8
+ import _string from "underscore.string";
9
+ _.string = _string;
10
+
11
+ class FileBrowser extends React.Component {
12
+ constructor(props) {
13
+ super(props);
14
+
15
+ this.state = {
16
+ files: [],
17
+ closed: false,
18
+ fileMode: 'manuscript',
19
+ busy: false
20
+ };
21
+
22
+ // Autobind
23
+ this.toggleClose = this.toggleClose.bind(this);
24
+ this.newFile = this.newFile.bind(this);
25
+ this.createFile = this.createFile.bind(this);
26
+ this.createImageFile = this.createImageFile.bind(this);
27
+ this.listManuscript = this.listManuscript.bind(this);
28
+ this.onDeleteFile = this.onDeleteFile.bind(this);
29
+ this.onChangeMode = this.onChangeMode.bind(this);
30
+ this.onFileDeleted = this.onFileDeleted.bind(this);
31
+ this.listCode = this.listCode.bind(this);
32
+ this.listManuscript = this.listManuscript.bind(this);
33
+ this.listImage = this.listImage.bind(this);
34
+ this.moveFile = this.moveFile.bind(this);
35
+ this.saveManuscript = this.saveManuscript.bind(this);
36
+
37
+ // Hook up events
38
+ FileAccessor.onDelete(this.onFileDeleted);
39
+ }
40
+
41
+ componentDidMount() {
42
+ this.listManuscript();
43
+ }
44
+
45
+ componentDidUpdate(lastProps, lastState) {
46
+ if (lastState.fileMode && lastState.fileMode !== this.state.fileMode) {
47
+ this[`list${_.string.capitalize(this.state.fileMode)}`]()
48
+ }
49
+ }
50
+
51
+ // We need to rechoose an appropriate file
52
+ onFileDeleted() {
53
+ this.props.onChangeFile(this.state.files[0])
54
+ }
55
+
56
+ // List what files we have
57
+ listManuscript() {
58
+ this.busy(true);
59
+ FileAccessor.listFiles((error, files) => {
60
+ this.busy(false);
61
+ if (error)
62
+ console.error(error);
63
+ else {
64
+ this.setState({ files: files });
65
+ }
66
+ });
67
+ }
68
+
69
+ // List what files we have
70
+ listImage() {
71
+ this.busy(true);
72
+ FileAccessor.listImages((error, files) => {
73
+ this.busy(false);
74
+ if (error)
75
+ console.error(error);
76
+ else {
77
+ this.setState({ files: files });
78
+ }
79
+ });
80
+ }
81
+
82
+ // List what files we have
83
+ listCode() {
84
+ this.busy(true);
85
+ FileAccessor.listCode((error, files) => {
86
+ this.busy(false);
87
+ if (error)
88
+ console.error(error);
89
+ else {
90
+ this.setState({ files: files });
91
+ }
92
+ });
93
+ }
94
+
95
+ // Set busy (loading) state
96
+ busy(busyState) {
97
+ this.setState({ busy: busyState });
98
+ }
99
+
100
+ // No longer in the loading state
101
+ free() {
102
+ this.setState({ busy: false });
103
+ }
104
+
105
+ // Close the browser
106
+ toggleClose(e) {
107
+ this.setState({ closed: !this.state.closed });
108
+ }
109
+
110
+ onChangeMode(fileMode) {
111
+ this.setState({ fileMode: fileMode });
112
+ }
113
+
114
+ // Create a new image file
115
+ createImageFile(e) {
116
+ let fileNode = this.refs.imageFile.getDOMNode()
117
+
118
+ FileAccessor.newImage(fileNode, () => {
119
+ fileNode.value = '';
120
+ this.setState({ creatingFile: false });
121
+ this[`list${_.string.capitalize(this.state.fileMode)}`]()
122
+ });
123
+
124
+ e.stopPropagation();
125
+ e.preventDefault();
126
+ return false
127
+ }
128
+
129
+ // Actually create a new file
130
+ createFile(e) {
131
+ let fileNode = this.refs.filename.getDOMNode()
132
+ FileAccessor.new(fileNode.value, this.state.fileMode, '', (error, file) => {
133
+ fileNode.value = '';
134
+ this.setState({ creatingFile: false });
135
+ this[`list${_.string.capitalize(this.state.fileMode)}`]()
136
+
137
+ // Change to the new file
138
+ this.props.onChangeFile(file)
139
+ });
140
+
141
+ e.stopPropagation();
142
+ e.preventDefault();
143
+ }
144
+
145
+ // Open the new file form
146
+ newFile(e) {
147
+ this.setState({ creatingFile: !this.state.creatingFile })
148
+ }
149
+
150
+ // Delete a file
151
+ onDeleteFile(file) {
152
+ FileAccessor.delete(file.filename, this.state.fileMode, this[`list${_.string.capitalize(this.state.fileMode)}`]);
153
+ }
154
+
155
+ // Save the manuscript, usually to preserve the order of the files since
156
+ // The manuscript files order in the ui is a mapping to book.txt
157
+ saveManuscript() {
158
+ if (this.state.fileMode !== "manuscript") return;
159
+ FileAccessor.saveManuscript(this.state.files);
160
+ }
161
+
162
+ // Reorder a file in the list
163
+ moveFile(draggedItem, targetItem) {
164
+ let files = _.clone(this.state.files)
165
+
166
+ let draggedItemIndex = files.indexOf(draggedItem)
167
+ let targetItemIndex = files.indexOf(targetItem)
168
+
169
+
170
+ files.splice(draggedItemIndex, 1);
171
+ files.splice(targetItemIndex, 0, draggedItem);
172
+ this.setState({ files: files });
173
+ }
174
+
175
+ renderFileCreator() {
176
+ if (!this.state.creatingFile) return;
177
+
178
+ if (this.state.fileMode === "image") {
179
+ let imageForm = (
180
+ <form onSubmit={this.createImageFile} className="file-name">
181
+ <input type="file" ref="imageFile" />
182
+ <button>Submit</button>
183
+ </form>
184
+ );
185
+ let noUploadHelp = (<p>Sorry, image uploads are not supported</p>);
186
+ return FileAccessor.supportsImageUploads() ? imageForm : noUploadHelp;
187
+ }
188
+ else {
189
+ return (
190
+ <form onSubmit={this.createFile} className="file-name">
191
+ <input type="text" ref="filename" placeholder="Type a filename..."></input>
192
+ <button>Create</button>
193
+ </form>
194
+ );
195
+ }
196
+ }
197
+
198
+ renderFilesList() {
199
+ if (this.state.busy)
200
+ return <i className="busy-indicator fa fa-spin fa-2x fa-circle-o-notch"></i>;
201
+ else {
202
+ return (
203
+ <ul className="files-list">
204
+ {
205
+ _.map(this.state.files, (file, i) => {
206
+ return (
207
+ <FileBrowserListItem
208
+ saveManuscript={this.saveManuscript}
209
+ moveFile={this.moveFile}
210
+ key={i}
211
+ fileMode={this.state.fileMode}
212
+ onDeleteFile={this.onDeleteFile}
213
+ onChangeFile={this.props.onChangeFile}
214
+ onPreviewImage={this.props.onPreviewImage}
215
+ isCurrent={this.props.currentFile === file}
216
+ file={file} />
217
+ )
218
+ })
219
+ }
220
+ </ul>
221
+ )
222
+ }
223
+ }
224
+
225
+ render() {
226
+ let clazz = `file-browser ${this.state.closed ? "closed" : "open"}`
227
+ let newFileClassName = `new-file${ this.state.creatingFile ? " active" : ""}`;
228
+ return (
229
+ <section className={clazz}>
230
+ <ul className="file-types-list">
231
+ <li><a onClick={_.partial(this.onChangeMode, 'manuscript')}><h4 className={`title${this.state.fileMode === 'manuscript' ? ' selected' : ''}`}>files</h4></a></li>
232
+ <li><a onClick={_.partial(this.onChangeMode, 'image')}><h4 className={`title${this.state.fileMode === 'image' ? ' selected' : ''}`}>images</h4></a></li>
233
+ <li><a onClick={_.partial(this.onChangeMode, 'code')}><h4 className={`title${this.state.fileMode === 'code' ? ' selected' : ''}`}>code</h4></a></li>
234
+ </ul>
235
+ <div className={newFileClassName} onClick={this.newFile}>
236
+ <i className="fa fa-plus"> </i>
237
+ </div>
238
+ { this.renderFileCreator() }
239
+ { this.renderFilesList() }
240
+ <button className="close-button" onClick={this.toggleClose}><span></span></button>
241
+ </section>
242
+ );
243
+ }
244
+ }
245
+
246
+ FileBrowser.propTypes = {
247
+ projectRoot: React.PropTypes.string.isRequired,
248
+ onChangeFile: React.PropTypes.func.isRequired,
249
+ currentFile: React.PropTypes.string.isRequired
250
+ };
251
+
252
+ export default DragDropContext(HTML5Backend)(FileBrowser);
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import { DragSource, DropTarget } from 'react-dnd';
3
+
4
+ const fileSource = {
5
+ beginDrag(props) {
6
+ // Return the data describing the dragged item
7
+ return props.file
8
+ }
9
+ }
10
+
11
+ const fileTarget = {
12
+ hover(props, monitor) {
13
+ const draggedFile = monitor.getItem()
14
+
15
+ if (draggedFile !== props.file)
16
+ props.moveFile(draggedFile, props.file)
17
+ },
18
+
19
+ drop(props, monitor) {
20
+ props.saveManuscript()
21
+ }
22
+ }
23
+
24
+ @DropTarget('file', fileTarget, connect => ({
25
+ connectDropTarget: connect.dropTarget()
26
+ }))
27
+ @DragSource('file', fileSource, (connect, monitor) => ({
28
+ connectDragSource: connect.dragSource(),
29
+ isDragging: monitor.isDragging()
30
+ }))
31
+ export default class FileBrowserListItem {
32
+ constructor(props) {
33
+ this.onDelete = this.onDelete.bind(this);
34
+ this.onChange = this.onChange.bind(this);
35
+ }
36
+
37
+ onDelete(e) {
38
+ if (confirm("Are you sure you want to delete this file?"))
39
+ this.props.onDeleteFile(this.props.file)
40
+
41
+ e.stopPropagation();
42
+ e.preventDefault();
43
+ }
44
+
45
+ onChange(e) {
46
+ if (this.props.file.type === "image")
47
+ this.props.onPreviewImage(this.props.file)
48
+ else
49
+ this.props.onChangeFile(this.props.file)
50
+ }
51
+
52
+ getFileIconClass() {
53
+ switch (this.props.fileMode) {
54
+ case 'manuscript':
55
+ return 'fa-file-o';
56
+ case 'images':
57
+ return 'fa-file-image';
58
+ case 'code':
59
+ return 'fa-code';
60
+ }
61
+ }
62
+
63
+ render() {
64
+ let clazz = `files-list-item${this.props.isCurrent ? ' current' : ''}${this.props.file.parent ? ' child' : ''}${this.props.isDragging ? ' dragging' : ''}`
65
+
66
+ return this.props.connectDragSource(this.props.connectDropTarget(
67
+ <li className={clazz} onClick={this.onChange}>
68
+ <a>
69
+ <i className={`fa ${this.getFileIconClass()}`}></i> { this.props.file.filename }
70
+ </a>
71
+ <button onClick={this.onDelete}><i className="fa fa-times"></i></button>
72
+ </li>
73
+ ));
74
+ }
75
+ }
76
+
77
+ FileBrowserListItem.propTypes = {
78
+ file: React.PropTypes.object.isRequired,
79
+ fileMode: React.PropTypes.string.isRequired,
80
+ moveFile: React.PropTypes.func.isRequired,
81
+ saveManuscript: React.PropTypes.func.isRequired,
82
+ isCurrent: React.PropTypes.bool.isRequired,
83
+ onDeleteFile: React.PropTypes.func.isRequired,
84
+ onChangeFile: React.PropTypes.func.isRequired,
85
+ onPreviewImage: React.PropTypes.func.isRequired
86
+ }