@standardnotes/bold-editor 1.6.3
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 +11 -0
- package/.eslintignore +3 -0
- package/.eslintrc +29 -0
- package/CHANGELOG.md +82 -0
- package/LICENSE +661 -0
- package/README.md +98 -0
- package/app/App.js +18 -0
- package/app/components/Editor.js +415 -0
- package/app/main.js +8 -0
- package/app/stylesheets/main.scss +272 -0
- package/dist/dist.css +1 -0
- package/dist/dist.min.js +2 -0
- package/dist/dist.min.js.LICENSE.txt +42 -0
- package/dist/filesafe-js/EncryptionWorker.js +2 -0
- package/dist/filesafe-js/EncryptionWorker.js.LICENSE.txt +5 -0
- package/dist/index.html +1 -0
- package/dist/vendor.css +6 -0
- package/dist/vendor.js +1 -0
- package/editor.index.ejs +14 -0
- package/editor_bar.png +0 -0
- package/ext.json.sample +9 -0
- package/package.json +54 -0
- package/redactor/plugins/alignment/alignment.js +55 -0
- package/redactor/plugins/alignment/alignment.min.js +1 -0
- package/redactor/plugins/counter/counter.js +76 -0
- package/redactor/plugins/counter/counter.min.js +1 -0
- package/redactor/plugins/filesafe/filesafe.js +70 -0
- package/redactor/plugins/filesafe/filesafe.min.js +70 -0
- package/redactor/plugins/fontcolor/fontcolor.js +184 -0
- package/redactor/plugins/fontcolor/fontcolor.min.js +1 -0
- package/redactor/plugins/fontfamily/fontfamily.js +59 -0
- package/redactor/plugins/fontfamily/fontfamily.min.js +1 -0
- package/redactor/plugins/fontsize/fontsize.js +58 -0
- package/redactor/plugins/fontsize/fontsize.min.js +1 -0
- package/redactor/plugins/imagemanager/imagemanager.js +82 -0
- package/redactor/plugins/imagemanager/imagemanager.min.js +1 -0
- package/redactor/plugins/inlinestyle/inlinestyle.css +34 -0
- package/redactor/plugins/inlinestyle/inlinestyle.js +62 -0
- package/redactor/plugins/inlinestyle/inlinestyle.min.css +1 -0
- package/redactor/plugins/inlinestyle/inlinestyle.min.js +1 -0
- package/redactor/plugins/specialchars/specialchars.js +78 -0
- package/redactor/plugins/specialchars/specialchars.min.js +1 -0
- package/redactor/plugins/table/table.js +477 -0
- package/redactor/plugins/table/table.min.js +1 -0
- package/redactor/plugins/textdirection/textdirection.js +44 -0
- package/redactor/plugins/textdirection/textdirection.min.js +1 -0
- package/redactor/plugins/textexpander/textexpander.js +64 -0
- package/redactor/plugins/textexpander/textexpander.min.js +1 -0
- package/redactor/plugins/variable/variable.css +23 -0
- package/redactor/plugins/variable/variable.js +222 -0
- package/redactor/plugins/variable/variable.min.css +1 -0
- package/redactor/plugins/variable/variable.min.js +1 -0
- package/redactor/src/redactor.min.css +1 -0
- package/redactor/src/redactor.min.js +1 -0
- package/webpack.config.js +82 -0
- package/webpack.dev.js +20 -0
- package/webpack.prod.js +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Bold Editor
|
|
2
|
+
|
|
3
|
+
The Bold Editor is a Standard Notes derived editor that offers text formatting and FileSafe integration.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Local Installation
|
|
8
|
+
|
|
9
|
+
Get a full working copy of the editor (with FileSafe) for development.
|
|
10
|
+
|
|
11
|
+
1. Clone the [bold-editor](https://github.com/standardnotes/bold-editor) and [filesafe-embed](https://github.com/standardnotes/filesafe-embed) repositories from GitHub.
|
|
12
|
+
|
|
13
|
+
2. Ensure that either the Standard Notes desktop app is available for use or the web app is accessible. Use both locally or with an Extended account (or the extension will not load).
|
|
14
|
+
|
|
15
|
+
3. In the `bold-editor` folder, edit the `package.json` file under `devDependencies` to use the local `filesafe-embed`:
|
|
16
|
+
```json
|
|
17
|
+
"filesafe-embed": "~/folder_with_both_repositories/filesafe-embed",
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
4. Run `npm i` in both the `bold-editor` and `filesafe-embed` folders to install the required dependencies.
|
|
21
|
+
- If there are errors, delete the `package-lock.json` file and `node_modules` folder. Then run `npm i` again. ([source](https://stackoverflow.com/questions/48298361/npm-install-failed-at-the-node-sass4-5-0-postinstall-script))
|
|
22
|
+
|
|
23
|
+
5. Edit `app/index.html` for use locally:
|
|
24
|
+
- comment out lines under 'Production'
|
|
25
|
+
- uncomment lines under 'Development'
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!-- Development -->
|
|
29
|
+
<script type="text/javascript" src="redactor.min.js"></script>
|
|
30
|
+
<script type="text/javascript" src="app.min.js"></script>
|
|
31
|
+
|
|
32
|
+
<!-- Production -->
|
|
33
|
+
<!--<script type="text/javascript" src="dist.min.js"></script>-->
|
|
34
|
+
```
|
|
35
|
+
6. Run `npm run build` to build the files.
|
|
36
|
+
6. Run `npm i -g http-server` to install a simple local server to host the extension.
|
|
37
|
+
7. Choose between webpack Watch Mode and webpack-dev-server for development and follow the corresponding instructions.
|
|
38
|
+
|
|
39
|
+
## Development with webpack Watch Mode (recommended)
|
|
40
|
+
|
|
41
|
+
Start by following the instructions here: https://docs.standardnotes.org/extensions/local-setup. Included in the repository is an `ext.json.sample` file that can be used in the setup.
|
|
42
|
+
|
|
43
|
+
This will setup a local server from which the bold-editor can be imported via the desktop app or the web app. You should be able to use the bold-editor now.
|
|
44
|
+
|
|
45
|
+
However, this will not allow for easy development because the app will not automatically build to the dist folder. We will use [webpack](https://webpack.js.org/guides/development/#using-watch-mode) for this.
|
|
46
|
+
|
|
47
|
+
1. Use `npm run watch` to automatically build files.
|
|
48
|
+
- There should be an existing console open that is running `http-server`
|
|
49
|
+
- Open a new console for `npm run watch`
|
|
50
|
+
|
|
51
|
+
2. Disable the cache on the desktop app/web app.
|
|
52
|
+
- We want to ensure that the latest build is always loaded when the app is refreshed
|
|
53
|
+
- Open devtools (`Ctrl+Shift+i`) and go to `Network`
|
|
54
|
+
- Check `Disable cache`
|
|
55
|
+
- On some systems, devtools must be kept open for this to work
|
|
56
|
+
|
|
57
|
+
3. Make some changes to `Editor.js`, reload the desktop or web app, and your changes will show up.
|
|
58
|
+
|
|
59
|
+
## Development with webpack-dev-server
|
|
60
|
+
|
|
61
|
+
*Note that this method only actively builds `app.min.js`.*
|
|
62
|
+
|
|
63
|
+
The steps are similar to the webpack Watch Mode, differences are listed below:
|
|
64
|
+
|
|
65
|
+
- The `ext.json` file belongs in the `dist` folder
|
|
66
|
+
- Update the url to `http://localhost:8080`
|
|
67
|
+
- `npm run start` to use the webpack-dev-server.
|
|
68
|
+
|
|
69
|
+
Disable the cache as in the webpack Watch Mode. Reload may be required to see changes in action.
|
|
70
|
+
|
|
71
|
+
## Production
|
|
72
|
+
|
|
73
|
+
In production environments, check that the `index.html` file is configured as follows:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<!-- Development -->
|
|
77
|
+
<!-- <script type="text/javascript" src="redactor.min.js"></script>
|
|
78
|
+
<script type="text/javascript" src="app.min.js"></script> -->
|
|
79
|
+
|
|
80
|
+
<!-- Production -->
|
|
81
|
+
<script type="text/javascript" src="dist.min.js"></script>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`dist.min.js` is built via `grunt`.
|
|
85
|
+
|
|
86
|
+
The CSS is also built with grunt, so webpack-dev-server will not be able to reload it. You must run `npm run build` anytime you change the CSS.
|
|
87
|
+
|
|
88
|
+
## Support
|
|
89
|
+
|
|
90
|
+
Please open a new issue and the Standard Notes team will take a look as soon as we can. For more information on editors, refer to the following link:
|
|
91
|
+
|
|
92
|
+
- Standard Notes Help: [What are editors?](https://standardnotes.org/help/77/what-are-editors)
|
|
93
|
+
|
|
94
|
+
Known issue: ordered lists, unordered lists, and tables seem to ignore any font preference you apply to it.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[GNU AGPL v3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
package/app/App.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Editor from '@Components/Editor';
|
|
3
|
+
|
|
4
|
+
export default class App extends React.Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
render() {
|
|
10
|
+
return (
|
|
11
|
+
<div id="editor-container">
|
|
12
|
+
<div key="editor" id="editor">
|
|
13
|
+
<Editor />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import FilesafeEmbed from 'filesafe-embed';
|
|
3
|
+
import EditorKit from '@standardnotes/editor-kit';
|
|
4
|
+
import DOMPurify from 'dompurify';
|
|
5
|
+
import { SKAlert } from 'sn-stylekit';
|
|
6
|
+
|
|
7
|
+
// Not used directly here, but required to be imported so that it is included
|
|
8
|
+
// in dist file.
|
|
9
|
+
// Note that filesafe-embed also imports filesafe-js, but conditionally, so
|
|
10
|
+
// it's not included in it's own dist files.
|
|
11
|
+
// eslint-disable-next-line no-unused-vars
|
|
12
|
+
import Filesafe from 'filesafe-js';
|
|
13
|
+
|
|
14
|
+
export default class Editor extends React.Component {
|
|
15
|
+
|
|
16
|
+
constructor(props) {
|
|
17
|
+
super(props);
|
|
18
|
+
this.state = {};
|
|
19
|
+
this.alert = null;
|
|
20
|
+
this.renderNote = false;
|
|
21
|
+
this.isNoteLocked = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidMount() {
|
|
25
|
+
this.configureEditorKit();
|
|
26
|
+
this.configureEditor();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
configureEditorKit() {
|
|
30
|
+
// EditorKit is a wrapper on top of the component manager to make it
|
|
31
|
+
// easier to build editors. As such, it very general and does not know
|
|
32
|
+
// how the functions are implemented, just that they are needed. It is
|
|
33
|
+
// up to the Bold Editor wrapper to implement these important functions.
|
|
34
|
+
const delegate = {
|
|
35
|
+
insertRawText: (rawText) => {
|
|
36
|
+
this.redactor.insertion.insertHtml(rawText);
|
|
37
|
+
},
|
|
38
|
+
handleRequestForContentHeight: () => {
|
|
39
|
+
return undefined
|
|
40
|
+
},
|
|
41
|
+
preprocessElement: (element) => {
|
|
42
|
+
// Convert inserting element to format Redactor wants.
|
|
43
|
+
// This will wrap img elements, for example, in a figure element.
|
|
44
|
+
// We also want to persist attributes from the inserting element.
|
|
45
|
+
const cleaned = this.redactor.cleaner.input(element.outerHTML);
|
|
46
|
+
const newElement = $R.dom(cleaned).nodes[0];
|
|
47
|
+
|
|
48
|
+
for (const attribute of element.attributes) {
|
|
49
|
+
newElement.setAttribute(attribute.nodeName, attribute.nodeValue);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return newElement;
|
|
53
|
+
},
|
|
54
|
+
insertElement: (element, inVicinityOfElement, insertionType) => {
|
|
55
|
+
// When inserting elements via dom manipulation, it doesnt update the
|
|
56
|
+
// source code view. So when you insert this element, open the code
|
|
57
|
+
// view, and close it, the element will be gone. The only way it works
|
|
58
|
+
// is if we use the proper redactor.insertion API, but I haven't found
|
|
59
|
+
// a good way to use that API for inserting text at a given position.
|
|
60
|
+
// There is 'insertToOffset', but where offset is the index of the
|
|
61
|
+
// plaintext, but I haven't found a way to map the adjacentTo element
|
|
62
|
+
// to a plaintext offset. So for now this bug will persist.
|
|
63
|
+
|
|
64
|
+
// insertionType can be either 'afterend' or 'child'
|
|
65
|
+
|
|
66
|
+
if (inVicinityOfElement) {
|
|
67
|
+
if (insertionType == 'afterend') {
|
|
68
|
+
inVicinityOfElement.insertAdjacentElement('afterend', element);
|
|
69
|
+
} else if (insertionType == 'child') {
|
|
70
|
+
// inVicinityOfElement.appendChild(element) doesn't work for some
|
|
71
|
+
// reason when inserting videos.
|
|
72
|
+
inVicinityOfElement.after(element);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
this.redactor.insertion.insertHtml(element.outerHTML);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
getElementsBySelector: (selector) => {
|
|
79
|
+
return this.redactor.editor.getElement().find(selector).nodes;
|
|
80
|
+
},
|
|
81
|
+
getCurrentLineText: () => {
|
|
82
|
+
// Returns the text content of the node where the cursor currently is.
|
|
83
|
+
// Typically a paragraph if no formatter, otherwise the closest
|
|
84
|
+
// formatted element, or null if there is no text content.
|
|
85
|
+
const node = this.redactor.selection.getCurrent();
|
|
86
|
+
return node.textContent;
|
|
87
|
+
},
|
|
88
|
+
getPreviousLineText: () => {
|
|
89
|
+
// Returns the text content of the previous node, unless there is no
|
|
90
|
+
// previous node, in which case it returns the falsy value.
|
|
91
|
+
const currentElement = this.redactor.selection.getElement();
|
|
92
|
+
const previousSibling = currentElement.previousSibling;
|
|
93
|
+
return previousSibling && previousSibling.textContent;
|
|
94
|
+
},
|
|
95
|
+
replaceText: ({ regex, replacement, previousLine }) => {
|
|
96
|
+
const marker = this.redactor.marker.insert('start');
|
|
97
|
+
let node;
|
|
98
|
+
if (previousLine) {
|
|
99
|
+
node = this.redactor.selection.getElement().previousSibling;
|
|
100
|
+
} else {
|
|
101
|
+
node = marker.previousSibling;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If we're searching the previous line, previousSibling may sometimes
|
|
105
|
+
// be null.
|
|
106
|
+
if (!node) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let nodeText = node.textContent;
|
|
111
|
+
// Remove our match from this element by replacing with empty string.
|
|
112
|
+
// We'll add in our actual replacement as a new element
|
|
113
|
+
nodeText = nodeText.replace(/ /, ' ');
|
|
114
|
+
nodeText = nodeText.replace(regex, '').replace(/\s$/, '').trim();
|
|
115
|
+
if (nodeText.length == 0) {
|
|
116
|
+
node.remove();
|
|
117
|
+
} else {
|
|
118
|
+
node.textContent = nodeText;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.redactor.insertion.insertHtml(replacement, 'start');
|
|
122
|
+
this.redactor.selection.restoreMarkers();
|
|
123
|
+
},
|
|
124
|
+
clearUndoHistory: () => {
|
|
125
|
+
// Called when switching notes to prevent history mixup.
|
|
126
|
+
$R('#editor', 'module.buffer.clear');
|
|
127
|
+
},
|
|
128
|
+
onNoteValueChange: async (note) => {
|
|
129
|
+
this.renderNote = await this.shouldRenderNote(note);
|
|
130
|
+
this.isNoteLocked = this.getNoteLockState(note);
|
|
131
|
+
|
|
132
|
+
document.getElementById('editor').setAttribute(
|
|
133
|
+
'spellcheck',
|
|
134
|
+
JSON.stringify(note.content.spellcheck)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
this.scrollToTop();
|
|
138
|
+
},
|
|
139
|
+
setEditorRawText: (rawText) => {
|
|
140
|
+
// Disabling read-only mode so that we can use source.setCode
|
|
141
|
+
this.disableReadOnly();
|
|
142
|
+
|
|
143
|
+
if (!this.renderNote) {
|
|
144
|
+
$R('#editor', 'source.setCode', '');
|
|
145
|
+
this.enableReadOnly();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Called when the Bold Editor is loaded, when switching to a Bold
|
|
150
|
+
// Editor note, or when uploading files, maybe in more places too.
|
|
151
|
+
const cleaned = this.redactor.cleaner.input(rawText);
|
|
152
|
+
$R('#editor', 'source.setCode', cleaned);
|
|
153
|
+
|
|
154
|
+
if (this.isNoteLocked) {
|
|
155
|
+
this.enableReadOnly();
|
|
156
|
+
} else {
|
|
157
|
+
this.disableReadOnly();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
this.editorKit = new EditorKit(delegate, {
|
|
163
|
+
mode: 'html',
|
|
164
|
+
supportsFileSafe: true,
|
|
165
|
+
// Redactor has its own debouncing, so we'll set ours to 0
|
|
166
|
+
coallesedSavingDelay: 0
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async configureEditor() {
|
|
171
|
+
// We need to set this as a window variable so that the filesafe plugin
|
|
172
|
+
// can interact with this object passing it as an opt for some reason
|
|
173
|
+
// strips any functions off the objects.
|
|
174
|
+
const filesafeInstance = await this.editorKit.getFileSafe();
|
|
175
|
+
window.filesafe_params = {
|
|
176
|
+
embed: FilesafeEmbed,
|
|
177
|
+
client: filesafeInstance
|
|
178
|
+
};
|
|
179
|
+
this.redactor = $R('#editor', {
|
|
180
|
+
styles: true,
|
|
181
|
+
toolbarFixed: true, // sticky toolbar
|
|
182
|
+
tabAsSpaces: 2, // currently tab only works if you use spaces.
|
|
183
|
+
tabKey: true, // explicitly set tabkey for editor use, not for focus.
|
|
184
|
+
linkSize: 20000, // redactor default is 30, which truncates the link.
|
|
185
|
+
buttonsAdd: ['filesafe'],
|
|
186
|
+
buttons: [
|
|
187
|
+
'bold', 'italic', 'underline', 'deleted', 'format', 'fontsize',
|
|
188
|
+
'fontfamily', 'fontcolor', 'filesafe', 'link', 'lists', 'alignment',
|
|
189
|
+
'line', 'redo', 'undo', 'indent', 'outdent', 'textdirection', 'html'
|
|
190
|
+
],
|
|
191
|
+
plugins: [
|
|
192
|
+
'filesafe', 'fontsize', 'fontfamily', 'fontcolor', 'alignment',
|
|
193
|
+
'table', 'inlinestyle', 'textdirection'
|
|
194
|
+
],
|
|
195
|
+
fontfamily: [
|
|
196
|
+
'Arial', 'Helvetica', 'Georgia', 'Times New Roman', 'Trebuchet MS',
|
|
197
|
+
'Monospace'
|
|
198
|
+
],
|
|
199
|
+
callbacks: {
|
|
200
|
+
changed: (html) => {
|
|
201
|
+
if (this.isNoteLocked || this.redactor.isReadOnly() || !this.renderNote) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// I think it's already cleaned so we don't need to do this.
|
|
205
|
+
// let cleaned = this.redactor.cleaner.output(html);
|
|
206
|
+
this.editorKit.onEditorValueChanged(html);
|
|
207
|
+
},
|
|
208
|
+
pasted: (_nodes) => {
|
|
209
|
+
this.editorKit.onEditorPaste();
|
|
210
|
+
},
|
|
211
|
+
image: {
|
|
212
|
+
resized: (image) => {
|
|
213
|
+
// Underlying html will change, triggering save event.
|
|
214
|
+
// New img dimensions need to be copied over to figure element.
|
|
215
|
+
const img = image.nodes[0];
|
|
216
|
+
const fig = img.parentNode;
|
|
217
|
+
fig.setAttribute('width', img.getAttribute('width'));
|
|
218
|
+
fig.setAttribute('height', img.getAttribute('height'));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
imageEditable: false,
|
|
223
|
+
imageCaption: false,
|
|
224
|
+
imageLink: false,
|
|
225
|
+
imageResizable: true, // requires image to be wrapped in a figure.
|
|
226
|
+
imageUpload: (formData, files, _event) => {
|
|
227
|
+
// Called when images are pasted from the clipboard too.
|
|
228
|
+
this.onEditorFilesDrop(files);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
this.redactor.editor.getElement().on('keyup.textsearcher', (event) => {
|
|
233
|
+
const key = event.which;
|
|
234
|
+
this.editorKit.onEditorKeyUp({
|
|
235
|
+
key,
|
|
236
|
+
isSpace: key == this.redactor.keycodes.SPACE,
|
|
237
|
+
isEnter: key == this.redactor.keycodes.ENTER
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// "Set the focus to the editor layer to the end of the content."
|
|
242
|
+
// Doesn't work because setEditorRawText is called when loading a note and
|
|
243
|
+
// it doesn't save the caret location, so focuses to beginning.
|
|
244
|
+
if (!this.redactor.editor.isEmpty()) {
|
|
245
|
+
this.redactor.editor.endFocus();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onEditorFilesDrop(files) {
|
|
250
|
+
if (!this.editorKit.canUseFileSafe()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!this.editorKit.canUploadFiles()) {
|
|
255
|
+
// Open filesafe modal
|
|
256
|
+
this.redactor.plugin.filesafe.open();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
// Observers in EditorKitInternal.js will handle successful upload
|
|
262
|
+
this.editorKit.uploadJSFileObject(file).then((descriptor) => {
|
|
263
|
+
if (!descriptor || !descriptor.uuid) {
|
|
264
|
+
// alert("File failed to upload. Please try again");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Checks if HTML is safe to render.
|
|
272
|
+
*/
|
|
273
|
+
checkIfUnsafeContent(renderedHtml) {
|
|
274
|
+
const sanitizedHtml = DOMPurify.sanitize(renderedHtml, {
|
|
275
|
+
/**
|
|
276
|
+
* We don't need script or style tags.
|
|
277
|
+
*/
|
|
278
|
+
FORBID_TAGS: ['script', 'style'],
|
|
279
|
+
/**
|
|
280
|
+
* XSS payloads can be injected via these attributes.
|
|
281
|
+
*/
|
|
282
|
+
FORBID_ATTR: [
|
|
283
|
+
'onerror',
|
|
284
|
+
'onload',
|
|
285
|
+
'onunload',
|
|
286
|
+
'onclick',
|
|
287
|
+
'ondblclick',
|
|
288
|
+
'onmousedown',
|
|
289
|
+
'onmouseup',
|
|
290
|
+
'onmouseover',
|
|
291
|
+
'onmousemove',
|
|
292
|
+
'onmouseout',
|
|
293
|
+
'onfocus',
|
|
294
|
+
'onblur',
|
|
295
|
+
'onkeypress',
|
|
296
|
+
'onkeydown',
|
|
297
|
+
'onkeyup',
|
|
298
|
+
'onsubmit',
|
|
299
|
+
'onreset',
|
|
300
|
+
'onselect',
|
|
301
|
+
'onchange'
|
|
302
|
+
]
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create documents from both the sanitized string and the rendered string.
|
|
307
|
+
* This will allow us to compare them, and if they are not equal
|
|
308
|
+
* (i.e: do not contain the same properties, attributes, inner text, etc)
|
|
309
|
+
* it means something was stripped.
|
|
310
|
+
*/
|
|
311
|
+
const renderedDom = new DOMParser().parseFromString(renderedHtml, 'text/html');
|
|
312
|
+
const sanitizedDom = new DOMParser().parseFromString(sanitizedHtml, 'text/html');
|
|
313
|
+
return !renderedDom.isEqualNode(sanitizedDom);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async showUnsafeContentAlert() {
|
|
317
|
+
const text = 'We’ve detected that this note contains a script or code snippet which may be unsafe to execute. ' +
|
|
318
|
+
'Scripts executed in the editor have the ability to impersonate as the editor to Standard Notes. ' +
|
|
319
|
+
'Press Continue to mark this script as safe and proceed, or Cancel to avoid rendering this note.';
|
|
320
|
+
|
|
321
|
+
return new Promise((resolve) => {
|
|
322
|
+
this.alert = new SKAlert({
|
|
323
|
+
title: null,
|
|
324
|
+
text,
|
|
325
|
+
buttons: [
|
|
326
|
+
{
|
|
327
|
+
text: 'Cancel',
|
|
328
|
+
style: 'neutral',
|
|
329
|
+
action: function() {
|
|
330
|
+
resolve(false);
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
text: 'Continue',
|
|
335
|
+
style: 'danger',
|
|
336
|
+
action: function() {
|
|
337
|
+
resolve(true);
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
]
|
|
341
|
+
});
|
|
342
|
+
this.alert.present();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setTrustUnsafeContent(note) {
|
|
347
|
+
this.editorKit.saveItemWithPresave(note, () => {
|
|
348
|
+
note.clientData = {
|
|
349
|
+
...note.clientData,
|
|
350
|
+
trustUnsafeContent: true
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
enableReadOnly() {
|
|
356
|
+
if (this.redactor.isReadOnly()) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
$R('#editor', 'enableReadOnly');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
disableReadOnly() {
|
|
363
|
+
if (!this.redactor.isReadOnly()) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
$R('#editor', 'disableReadOnly');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
scrollToTop() {
|
|
370
|
+
window.scroll(0, 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async shouldRenderNote(noteItem) {
|
|
374
|
+
this.dismissUnsafeContentAlerts();
|
|
375
|
+
|
|
376
|
+
const isUnsafeContent = this.checkIfUnsafeContent(noteItem.content.text);
|
|
377
|
+
const trustUnsafeContent = noteItem.clientData['trustUnsafeContent'] ?? false;
|
|
378
|
+
|
|
379
|
+
if (!isUnsafeContent) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (isUnsafeContent && trustUnsafeContent) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = await this.showUnsafeContentAlert();
|
|
388
|
+
if (result) {
|
|
389
|
+
this.setTrustUnsafeContent(noteItem);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
dismissUnsafeContentAlerts() {
|
|
396
|
+
try {
|
|
397
|
+
if (this.alert) {
|
|
398
|
+
this.alert.dismiss();
|
|
399
|
+
}
|
|
400
|
+
this.alert = null;
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.warn('Trying to dismiss an alert that does not exist anymore.');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getNoteLockState(note) {
|
|
407
|
+
return note.content.appData['org.standardnotes.sn']['locked'] ?? false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
render() {
|
|
411
|
+
return (
|
|
412
|
+
<div key="editor" className={'sn-component'} />
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|