@standardnotes/markdown-basic 1.6.1
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.
- package/.babelrc +4 -0
- package/.editorconfig +9 -0
- package/.eslintignore +5 -0
- package/.eslintrc +49 -0
- package/.jshintignore +1 -0
- package/.jshintrc +10 -0
- package/.travis.yml +7 -0
- package/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +138 -0
- package/app/components/Home.js +312 -0
- package/app/index.html +7 -0
- package/app/main.js +23 -0
- package/app/stylesheets/main.scss +242 -0
- package/dist/dist.css +3 -0
- package/dist/dist.css.map +1 -0
- package/dist/dist.js +2 -0
- package/dist/dist.js.LICENSE.txt +32 -0
- package/dist/index.html +7 -0
- package/ext.json.sample +10 -0
- package/favicon.ico +0 -0
- package/package.json +64 -0
- package/webpack.config.js +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Markdown Basic
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://github.com/sn-extensions/markdown-basic/blob/master/LICENSE)
|
|
6
|
+
[](https://github.com/sn-extensions/markdown-basic/issues/)
|
|
7
|
+
[](https://standardnotes.org/slack)
|
|
8
|
+
[](https://github.com/sn-extensions/markdown-basic)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
## Introduction
|
|
13
|
+
|
|
14
|
+
Markdown Basic is a [custom editor](https://standardnotes.org/help/77/what-are-editors) for [Standard Notes](https://standardnotes.org), a free, open-source, and [end-to-end encrypted](https://standardnotes.org/knowledge/2/what-is-end-to-end-encryption) notes app.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Markdown via Markdown-It
|
|
19
|
+
- Syntax Highlighting via Highlight.js
|
|
20
|
+
- Optional split pane view
|
|
21
|
+
- Task Lists
|
|
22
|
+
- Tables
|
|
23
|
+
- Footnotes
|
|
24
|
+
- Inline external images
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
1. Register for an account at Standard Notes using the [Desktop App](https://standardnotes.org/download) or [Web app](https://app.standardnotes.org). Remember to use a strong and memorable password.
|
|
29
|
+
2. Sign up for [Standard Notes Extended](https://dashboard.standardnotes.org/member). Then, follow the instructions [here](https://standardnotes.org/help/29/how-do-i-install-extensions-once-i-ve-signed-up-for-extended) or continue.
|
|
30
|
+
3. Click **Extensions** in the lower left corner.
|
|
31
|
+
4. Under **Repository**, find **Markdown Basic**.
|
|
32
|
+
5. Click **Install**.
|
|
33
|
+
6. Close the **Extensions** pop-up.
|
|
34
|
+
7. At the top of your note, click **Editor**, then click **Markdown Basic**.
|
|
35
|
+
8. Click **Continue**, and you are done!
|
|
36
|
+
|
|
37
|
+
After you have installed the editor on the web or desktop app, it will automatically sync to your [mobile app](https://standardnotes.org/download) after you log in.
|
|
38
|
+
|
|
39
|
+
## Style Guide
|
|
40
|
+
|
|
41
|
+
| Result | Markdown |
|
|
42
|
+
| :----------------- | :------------------------------------------- |
|
|
43
|
+
| **Bold** | \*\*text\*\* or \_\_text\_\_ |
|
|
44
|
+
| _Emphasize_ | \*text\* or \_text\_ |
|
|
45
|
+
| ~~Strike-through~~ | \~\~text\~\~ |
|
|
46
|
+
| Link | [text]\(http://) |
|
|
47
|
+
| Image | ![text]\(http://) |
|
|
48
|
+
| `Inline Code` | \`code\` |
|
|
49
|
+
| Code Block | \`\`\`language <br></br>code <br></br>\`\`\` |
|
|
50
|
+
| Unordered List | \* item <br></br> - item <br></br> + item |
|
|
51
|
+
| Ordered List | 1. item |
|
|
52
|
+
| Task List | `- [ ] Task` or `- [x] Task` |
|
|
53
|
+
| Blockquote | \> quote |
|
|
54
|
+
| H1 | # Heading |
|
|
55
|
+
| H2 | ## Heading |
|
|
56
|
+
| H3 | ### Heading |
|
|
57
|
+
| H4 | #### Heading |
|
|
58
|
+
| Section Breaks | `---` or `***` |
|
|
59
|
+
|
|
60
|
+
## Tables
|
|
61
|
+
|
|
62
|
+
Colons can be used to align columns.
|
|
63
|
+
Copy this into your editor to see what it renders:
|
|
64
|
+
|
|
65
|
+
```md
|
|
66
|
+
| Tables | Are | Cool |
|
|
67
|
+
| ------------------ | :-----------: | ------: |
|
|
68
|
+
| col 2 is | centered | \$149 |
|
|
69
|
+
| col 3 is | right-aligned | \$4.17 |
|
|
70
|
+
| privacy is | neat | \$2.48 |
|
|
71
|
+
| rows don't need to | be pretty | what? |
|
|
72
|
+
| the last line is | unnecessary | really? |
|
|
73
|
+
| one more | row | Yay! 😆 |
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Footnotes
|
|
77
|
+
|
|
78
|
+
The Markdown Basic editor supports footnotes. The footnote links do not work properly on mobile. Copy this into your note to see how they're used:
|
|
79
|
+
|
|
80
|
+
```md
|
|
81
|
+
You can create footnote references that are short[^1] or long.[^2]
|
|
82
|
+
You can also create them inline.^[which may be easier,
|
|
83
|
+
since you don't need to pick an identifier and move down to type the note]
|
|
84
|
+
The footnotes are automatically numbered at the bottom of your note,
|
|
85
|
+
but you'll need to manually number your superscripts.
|
|
86
|
+
Make sure to count your variable[^variable] footnotes.[^5]
|
|
87
|
+
|
|
88
|
+
[^1]: Here's a footnote.
|
|
89
|
+
[^2]: Here’s a footnote with multiple blocks.
|
|
90
|
+
|
|
91
|
+
Subsequent paragraphs are indented to show that they belong to the previous footnote.
|
|
92
|
+
|
|
93
|
+
{ eight spaces for some code }
|
|
94
|
+
|
|
95
|
+
The whole paragraph can be indented, or just the first
|
|
96
|
+
line. In this way, multi-paragraph footnotes work like
|
|
97
|
+
multi-paragraph list items.
|
|
98
|
+
|
|
99
|
+
This paragraph won’t be part of the footnote, because it
|
|
100
|
+
isn’t indented.
|
|
101
|
+
|
|
102
|
+
[^variable]: The variable footnote is the fourth footnote.
|
|
103
|
+
[^5]: This is the fifth footnote.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Not yet available:
|
|
107
|
+
|
|
108
|
+
- KaTeX
|
|
109
|
+
- Printing
|
|
110
|
+
- Custom Font Families
|
|
111
|
+
- Custom Font Sizes
|
|
112
|
+
- Superscript
|
|
113
|
+
- Subscript
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[GNU Affero General Public License v3.0](https://github.com/sn-extensions/markdown-basic/blob/master/LICENSE)
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
The instructions for local setup can be found [here](https://docs.standardnotes.org/extensions/local-setup). All commands are performed in the root directory:
|
|
122
|
+
|
|
123
|
+
1. Fork the [repository](https://github.com/sn-extensions/markdown-basic) on GitHub
|
|
124
|
+
2. [Clone](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) your fork of the repository
|
|
125
|
+
3. Type `cd markdown-basic`
|
|
126
|
+
4. Run `yarn` to locally install the packages in `package.json`
|
|
127
|
+
5. Create `ext.json` as shown [here](https://docs.standardnotes.org/extensions/local-setup) with `url: "http://localhost:8004/dist/index.html"`. Optionally, create your `ext.json` as a copy of `ext.json.sample`.
|
|
128
|
+
6. Install `http-server` using `yarn global add http-server` or `npm install -g http-server`
|
|
129
|
+
7. Start the server at `http://localhost:8004` using `http-server . --cors -p 8004`
|
|
130
|
+
8. Import the extension into the [web](https://app.standardnotes.org) or [desktop](https://standardnotes.org/download) app with `http://localhost:8004/ext.json`.
|
|
131
|
+
9. To build the editor, open another command window and run `yarn build` or `npm run build`. For live builds, use `yarn watch` or `npm run watch`. You can also run `yarn start` or `npm run start` and open the editor at `http://localhost:8080`.
|
|
132
|
+
|
|
133
|
+
## Further Resources
|
|
134
|
+
|
|
135
|
+
- [GitHub](https://github.com/sn-extensions/markdown-basic/)
|
|
136
|
+
- [Issues and Feature Requests](https://github.com/sn-extensions/markdown-basic/issues)
|
|
137
|
+
- [Standard Notes Slack](https://standardnotes.org/slack) (for connecting with the Standard Notes Community)
|
|
138
|
+
- [Standard Notes Help Files](https://standardnotes.org/help) (for issues related to Standard Notes but unrelated to this editor)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ComponentRelay from '@standardnotes/component-relay';
|
|
3
|
+
const MarkdownIt = require('markdown-it');
|
|
4
|
+
|
|
5
|
+
const EditMode = 0;
|
|
6
|
+
const SplitMode = 1;
|
|
7
|
+
const PreviewMode = 2;
|
|
8
|
+
|
|
9
|
+
export default class Home extends React.Component {
|
|
10
|
+
|
|
11
|
+
constructor(props) {
|
|
12
|
+
super(props);
|
|
13
|
+
|
|
14
|
+
this.modes = [
|
|
15
|
+
{ mode: EditMode, label: 'Edit', css: 'edit' },
|
|
16
|
+
{ mode: SplitMode, label: 'Split', css: 'split' },
|
|
17
|
+
{ mode: PreviewMode, label: 'Preview', css: 'preview' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
this.state = { mode: this.modes[0] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
componentDidMount() {
|
|
24
|
+
this.simpleMarkdown = document.getElementById('simple-markdown');
|
|
25
|
+
this.editor = document.getElementById('editor');
|
|
26
|
+
this.preview = document.getElementById('preview');
|
|
27
|
+
|
|
28
|
+
this.configureMarkdown();
|
|
29
|
+
this.connectToBridge();
|
|
30
|
+
this.updatePreviewText();
|
|
31
|
+
this.addChangeListener();
|
|
32
|
+
|
|
33
|
+
this.configureResizer();
|
|
34
|
+
this.addTabHandler();
|
|
35
|
+
|
|
36
|
+
this.scrollTriggers = {};
|
|
37
|
+
this.scrollHandlers = [
|
|
38
|
+
{ el: this.editor, handler: this.scrollHandler(this.editor, this.preview) },
|
|
39
|
+
{ el: this.preview, handler: this.scrollHandler(this.preview, this.editor) }
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
UNSAFE_componentWillUpdate(nextProps, nextState) {
|
|
44
|
+
let prevMode = this.state.mode.mode;
|
|
45
|
+
let nextMode = nextState.mode.mode;
|
|
46
|
+
|
|
47
|
+
// If we changed to Split mode we add the scroll listeners
|
|
48
|
+
if (prevMode !== nextMode) {
|
|
49
|
+
if (nextMode === SplitMode) {
|
|
50
|
+
this.addScrollListeners();
|
|
51
|
+
} else {
|
|
52
|
+
this.removeScrollListeners();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setModeFromModeValue(value) {
|
|
58
|
+
for (let mode of this.modes) {
|
|
59
|
+
if (mode.mode == value) {
|
|
60
|
+
this.setState({ mode });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
changeMode(mode) {
|
|
67
|
+
this.setState({ mode });
|
|
68
|
+
if (!this.note) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.note.clientData = { mode: mode.mode };
|
|
72
|
+
this.componentRelay.saveItem(this.note);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
configureMarkdown() {
|
|
76
|
+
const markdownitOptions = {
|
|
77
|
+
// automatically render raw links as anchors.
|
|
78
|
+
linkify: true,
|
|
79
|
+
// Convert '\n' in paragraphs into <br>
|
|
80
|
+
breaks: true
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.markdown = MarkdownIt(markdownitOptions)
|
|
84
|
+
.use(require('markdown-it-footnote'))
|
|
85
|
+
.use(require('markdown-it-task-lists'))
|
|
86
|
+
.use(require('markdown-it-highlightjs'));
|
|
87
|
+
|
|
88
|
+
// Remember old renderer, if overriden, or proxy to default renderer
|
|
89
|
+
const defaultRender = this.markdown.renderer.rules.link_open || ((tokens, idx, options, env, self) => {
|
|
90
|
+
return self.renderToken(tokens, idx, options);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.markdown.renderer.rules.link_open = ((tokens, idx, options, env, self) => {
|
|
94
|
+
// If you are sure other plugins can't add `target` - drop check below
|
|
95
|
+
const aIndex = tokens[idx].attrIndex('target');
|
|
96
|
+
|
|
97
|
+
if (aIndex < 0) {
|
|
98
|
+
tokens[idx].attrPush(['target', '_blank']); // add new attribute
|
|
99
|
+
} else {
|
|
100
|
+
tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// pass token to default renderer.
|
|
104
|
+
return defaultRender(tokens, idx, options, env, self);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
connectToBridge() {
|
|
109
|
+
this.componentRelay = new ComponentRelay({
|
|
110
|
+
targetWindow: window,
|
|
111
|
+
onReady: () => {
|
|
112
|
+
const { platform } = this.componentRelay;
|
|
113
|
+
this.setState({ platform });
|
|
114
|
+
},
|
|
115
|
+
handleRequestForContentHeight: () => {
|
|
116
|
+
return undefined
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.componentRelay.streamContextItem((note) => {
|
|
121
|
+
this.note = note;
|
|
122
|
+
|
|
123
|
+
if (note.clientData) {
|
|
124
|
+
const mode = note.clientData.mode ?? EditMode;
|
|
125
|
+
this.setModeFromModeValue(mode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Only update UI on non-metadata updates.
|
|
129
|
+
if (note.isMetadataUpdate) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.editor.value = note.content.text;
|
|
134
|
+
this.preview.innerHTML = this.markdown.render(note.content.text);
|
|
135
|
+
|
|
136
|
+
document.getElementById('editor').setAttribute(
|
|
137
|
+
'spellcheck',
|
|
138
|
+
JSON.stringify(note.content.spellcheck)
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
truncateString(string, limit = 80) {
|
|
144
|
+
if (!string) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (string.length <= limit) {
|
|
148
|
+
return string;
|
|
149
|
+
} else {
|
|
150
|
+
return string.substring(0, limit) + '...';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updatePreviewText() {
|
|
155
|
+
const text = this.editor.value || '';
|
|
156
|
+
this.preview.innerHTML = this.markdown.render(text);
|
|
157
|
+
return text;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
addChangeListener() {
|
|
161
|
+
document.getElementById('editor').addEventListener('input', () => {
|
|
162
|
+
if (this.note) {
|
|
163
|
+
// Be sure to capture this object as a variable, as this.note may be reassigned in `streamContextItem`, so by the time
|
|
164
|
+
// you modify it in the presave block, it may not be the same object anymore, so the presave values will not be applied to
|
|
165
|
+
// the right object, and it will save incorrectly.
|
|
166
|
+
let note = this.note;
|
|
167
|
+
|
|
168
|
+
this.componentRelay.saveItemWithPresave(note, () => {
|
|
169
|
+
note.content.text = this.updatePreviewText();
|
|
170
|
+
note.content.preview_plain = this.truncateString(this.preview.textContent || this.preview.innerText);
|
|
171
|
+
note.content.preview_html = null;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addScrollListeners() {
|
|
178
|
+
this.scrollHandlers.forEach(({ el, handler }) => el.addEventListener('scroll', handler));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
removeScrollListeners() {
|
|
182
|
+
this.scrollHandlers.forEach(({ el, handler }) => el.removeEventListener('scroll', handler));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
scrollHandler = (source, destination) => {
|
|
186
|
+
let frameRequested;
|
|
187
|
+
|
|
188
|
+
return (event) => {
|
|
189
|
+
// Avoid the cascading effect by not handling the event if it was triggered initially by this element
|
|
190
|
+
if (this.scrollTriggers[source] === true) {
|
|
191
|
+
this.scrollTriggers[source] = false;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.scrollTriggers[source] = true;
|
|
195
|
+
|
|
196
|
+
// Only request the animation frame once until it gets processed
|
|
197
|
+
if (frameRequested) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
frameRequested = true;
|
|
201
|
+
|
|
202
|
+
window.requestAnimationFrame(() => {
|
|
203
|
+
let target = event.target;
|
|
204
|
+
let height = target.scrollHeight - target.clientHeight;
|
|
205
|
+
let ratio = parseFloat(target.scrollTop) / height;
|
|
206
|
+
let move = (destination.scrollHeight - destination.clientHeight) * ratio;
|
|
207
|
+
destination.scrollTop = move;
|
|
208
|
+
|
|
209
|
+
frameRequested = false;
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
removeSelection() {
|
|
215
|
+
if (window.getSelection) {
|
|
216
|
+
window.getSelection().removeAllRanges();
|
|
217
|
+
} else if (document.selection) {
|
|
218
|
+
document.selection.empty();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
configureResizer() {
|
|
223
|
+
let pressed = false;
|
|
224
|
+
const columnResizer = document.getElementById('column-resizer');
|
|
225
|
+
const resizerWidth = columnResizer.offsetWidth;
|
|
226
|
+
const safetyOffset = 15;
|
|
227
|
+
|
|
228
|
+
columnResizer.addEventListener('mousedown', () => {
|
|
229
|
+
pressed = true;
|
|
230
|
+
columnResizer.classList.add('dragging');
|
|
231
|
+
this.editor.classList.add('no-selection');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
document.addEventListener('mousemove', (event) => {
|
|
235
|
+
if (!pressed) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let x = event.clientX;
|
|
240
|
+
if (x < resizerWidth / 2 + safetyOffset) {
|
|
241
|
+
x = resizerWidth / 2 + safetyOffset;
|
|
242
|
+
} else if (x > this.simpleMarkdown.offsetWidth - resizerWidth - safetyOffset) {
|
|
243
|
+
x = this.simpleMarkdown.offsetWidth - resizerWidth - safetyOffset;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const colLeft = x - resizerWidth / 2;
|
|
247
|
+
columnResizer.style.left = colLeft + 'px';
|
|
248
|
+
this.editor.style.width = (colLeft - safetyOffset) + 'px';
|
|
249
|
+
|
|
250
|
+
this.removeSelection();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
document.addEventListener('mouseup', () => {
|
|
254
|
+
if (pressed) {
|
|
255
|
+
pressed = false;
|
|
256
|
+
columnResizer.classList.remove('dragging');
|
|
257
|
+
this.editor.classList.remove('no-selection');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
addTabHandler() {
|
|
263
|
+
// Tab handler
|
|
264
|
+
this.editor.addEventListener('keydown', (event) => {
|
|
265
|
+
if (!event.shiftKey && event.which == 9) {
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
|
|
268
|
+
// Using document.execCommand gives us undo support
|
|
269
|
+
if (!document.execCommand('insertText', false, '\t')) {
|
|
270
|
+
// document.execCommand works great on Chrome/Safari but not Firefox
|
|
271
|
+
const start = this.selectionStart;
|
|
272
|
+
const end = this.selectionEnd;
|
|
273
|
+
const spaces = ' ';
|
|
274
|
+
|
|
275
|
+
// Insert 4 spaces
|
|
276
|
+
this.value = this.value.substring(0, start)
|
|
277
|
+
+ spaces + this.value.substring(end);
|
|
278
|
+
|
|
279
|
+
// Place cursor 4 spaces away from where
|
|
280
|
+
// the tab key was pressed
|
|
281
|
+
this.selectionStart = this.selectionEnd = start + 4;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
render() {
|
|
288
|
+
return (
|
|
289
|
+
<div id="simple-markdown" className={`sn-component ${this.state.platform}`}>
|
|
290
|
+
<div id="header">
|
|
291
|
+
<div className="segmented-buttons-container sk-segmented-buttons">
|
|
292
|
+
<div className="buttons">
|
|
293
|
+
{this.modes.map(mode =>
|
|
294
|
+
<div key={mode} onClick={() => this.changeMode(mode)} className={`sk-button button ${this.state.mode == mode ? 'selected info' : 'sk-secondary-contrast'}`}>
|
|
295
|
+
<div className="sk-label">
|
|
296
|
+
{mode.label}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div id="editor-container" className={this.state.mode.css}>
|
|
305
|
+
<textarea dir="auto" id="editor" className={this.state.mode.css}></textarea>
|
|
306
|
+
<div id="column-resizer" className={this.state.mode.css}></div>
|
|
307
|
+
<div id="preview" className={this.state.mode.css}></div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
package/app/index.html
ADDED
package/app/main.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import Home from './components/Home';
|
|
4
|
+
|
|
5
|
+
export default class App extends React.Component {
|
|
6
|
+
constructor(props) {
|
|
7
|
+
super(props);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
render() {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
13
|
+
<Home />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ReactDOM.render(
|
|
21
|
+
<App />,
|
|
22
|
+
document.body.appendChild(document.createElement('div'))
|
|
23
|
+
);
|