# Editor document
###### tags: `Qnotes3`
The current development version of Qnotes 3 is v1.2.0, css is used for v.20180606, and editor is v.20180614
https://drive.google.com/drive/u/1/folders/0B4o_i2HiAtNmbGRxNTVScU1Wcnc
## Architecture

## How to call editor function

## How to get editor function callback

## How to listen content changes

## Bind editor
```
container = document.getElementById("example");
editor = new NSEditor(
container, // document.getElementById("example");
contentString,
+version,
clientID, // NotesStation.generateClientID();
{
serverUrl: serverUrl, // http://xxx.xx.xx.xxx:xxxx
}
)
```
## Unbind editor
`editor.detach(this.editorEventListeners);
editor = null;`
## Handling paste content contains images
```
document.getElementsByClassName("ProseMirror")[0].addEventListener('paste', function(e) {
var testElement = document.createElement("div");
var data = e.clipboardData.getData('text/html');
if (data === "") return
// Disable paste event
e.preventDefault();
e.stopPropagation();
testElement.innerHTML = data;
// get image from html
var imgList = testElement.querySelectorAll('img');
}, true);
```
## When you received: EDITOR_STEPS_HANDLE
```
function checkHandle(content) {
var isApply = content.isApply;
if (isApply) {
updateEditor(content);
} else {
// Retry to update the note content
}
}
```
## When you received: EDITOR_UPDATE(receive apply response)
```
function updateEditor(content) {
editor.receiveUpdate(content.steps, content.clientIDs);
}
```
## Get file/image/link src when click
```
editor.on('click', function(type, attrs, marks, pm, pos, e) {
// type: image, file, text, check_list_item(checkbox)
});
```
## Listen cursor position change
```
editor.on("selectionChange", function() {
});
```
## Click on the editor event
```
document.getElementById("example").addEventListener('click', event);
```
## Calculate curser position
```
var sel = document.getSelection();
var container = sel.getRangeAt(0).startContainer;
if (container.nodeType === 3) container = container.parentNode;
var rect = container.getBoundingClientRect();
var scrollTop = window.pageYOffset;
var scroll = +rect.top + +container.offsetHeight + scrollTop;
```
## Turn on/off editing
```
document.getElementsByClassName("ProseMirror")[0].contentEditable = true/false;
document.getElementsByClassName("noteTitle")[0].disabled = true/false
```
## Get note info
* Get not title
```
document.getElementsByClassName("titleInput")[0].value
```
* Get note content
```
editor.content
```
* Get edit toolbar status
```
editor.menuStatus;
```
* Get color
```
editor.selectionColor
format:
{"background": "#000000"/"transparent", "text": "#000000"/"transparent"}
```
* Text style
```
var mark = editor.getMark("strong");
var mark = editor.getMark("em");
var mark = editor.getMark("u");
var mark = editor.getMark("del");
editor.execCommand("toggleMark", mark);
```
* List
```
var node = editor.getNode("bullet_list");
var node = editor.getNode("ordered_list");
var node = editor.getNode("check_list");
editor.execCommand("wrapIn", node);
```
* Paragraph align
```
editor.execCommand("setParagraphAlign", 'left');
editor.execCommand("setParagraphAlign", 'center');
editor.execCommand("setParagraphAlign", 'right');
```
* Indent
```
editor.execCommand("lift");
```
* Undo & Redo
```
editor.execCommand("undo");
editor.execCommand("redo");
```
* Get hyperlink information
```
editor.execCommand('addOrEditHyperLink', callback);
Callback has 2 formats:
{type: 'add', callback: {title: title, href: href}, title}
{type: 'edit', title, href, editFunc: {title: title, href: href}, removeFunc, callback}
```
* Insert image
```
editor.execCommand("insertImage", {
"src": src,
"alt": alt,
"title": title
});
```
* Insert file
```
editor.execCommand("insertFile", {
src: src,
title: title,
size: size,
type: type
});
```
* Text & Background color
```
var mark = editor.getMark("color"); // text color
var mark = editor.getMark("bg"); // background color
editor.execCommand("applyMark", mark, {color: "#000000"}); // set color
editor.execCommand("removeMark", mark); // reomve color
```
* Load editor js
```
var script = document.createElement("script");
script.type = "text/javascript";
script.src = jsSrc;
script.onload = function () {
// callback
};
document.getElementsByTagName("head")[0].appendChild(script);
```
* Load editor css
```
var link = document.createElement("link")
link.rel = "stylesheet";
link.href = src;
link.onload = function () {
// callback
};
document.getElementsByTagName("head")[0].appendChild(link);
```
# Socket
## Socket event
* note: This event is triggered by all messages related to the note
| Action | Description |
| -------- | -------- |
| EDITOR_JOIN_NOTE_ROOM | Every time opening the notes, you need send this message |
| EDITOR_SET_EDITOR | You will received this message after you send EDITOR_JOIN_NOTE_ROOM, and you need to initial Editor |
| EDITOR_UPDATE_COVER | If someone uploaded a image you will receive this message and need to change the cover of notes
| EDITOR_ERROR | if an unknow error is occurred |
| EDITOR_STEPS_HANDLE | You will received this message after you send EDITOR_UPDATE, and you need to check your request is apply or not |
| EDITOR_UPDATE | If someone change note content you will receive the message and need to update editor |
| EDITOR_CHANGE_MODE | You will receive this message if the notes owner remove this share with you or this notes, or permission is incorrect |
## Merge/Conflict Rule (app version ~v1.1)
* 在離線的情況下做任何編輯,筆記的版本都不會有任何變動
* 離線編輯時,所有新增檔案與圖片路徑皆為裝置路徑(/getFilesDir()/share/...)
* 同步時需檢查伺服器筆記的版本
| 伺服器版本大於離線筆記版本 | 離線編輯 | 動作 |
| -------- | -------- | -------- |
| O | O | 新增衝突筆記,並且復原離線筆記至伺服器版本 |
| O | X | 復原離線筆記至伺服器版本 |
| X | O | 上傳並覆蓋伺服器筆記 |
| X | X | 不做任何動作 |
## Merge/Conflict Rule (app version v1.2~)
http://spec.qnap.com.tw/issues/14485
* Database CREATE_NOTE_TABLE changes
```
CREATE_NOTE_TABLE = "CREATE TABLE " + TABLE_NOTE + " (" +
COLUMN_NOTE_STEPS + " TEXT, " + // Steps of note change
COLUMN_NOTE_CONTENT_CHANGE + " TEXT, " + // Content of note change(used in conflict notes)
COLUMN_NOTE_CONTENT + " TEXT, " + // Original unchanged note
```
* Sync without conflict
** Rebase (API: )
```
POST /ns/api/v2/updateSteps
```
| Variable | Description |
| -------- | -------- |
| ${noteId} | identifier for each note(default uniqid()) |
| ${content} | Original unchanged note |
| ${version} | the version of note_content |
| ${steps} | Steps of note change |
| ${clientId} | identifier for user |
* Sync conflict
* Continue synchronizing (default) API:
Same as Sync without conflict Rebase API
* Save as duplicated note (current solution)
Same as the previous version
## Files Rule
* Cache file
Server path and device path conversion
```
Server file path: /{note id}/image/5950c894f253a642.jpg
Device file path: getFilesDir()/share/{note id}/5950c894f253a642.jpg
```
* Upload file
| Source | Action |
| ------ | ------ |
| Device file path(getFilesDir()/share/...) | Upload file and get new server file path from response, replace device file path to server file path |
| Server file path(/noteID/image/image.jpg) | Create conflict note: Upload server file path and get new server file path from response, replace old server file path to new server file path (Upload note: Don't do any action) |
## Qnotes3 File Provider
**Specify the archive path and turn on the camera**
* FileManager.java:
```
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()) + ".jpg");
Uri contentUri = FileProvider.getUriForFile(activity,
"com.qnap.mobile.qnotes3.fileprovider", file);
new Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
```
* res/xml/file_path.xml:
```
<external-path name="photo" path="DCIM/" />
```
**Use content uri let other app can open private file**
* FileManager.java:
```
Uri contentUri = FileProvider.getUriForFile(context, "com.qnap.mobile.qnotes3.fileprovider", file);
new Intent(android.content.Intent.ACTION_VIEW)
.setDataAndType(contentUri, mimeType)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
```
* res/xml/file_path.xml:
```
<files-path name="shared_files" path="share/"/>
```
## Waiting page for first time login
* Spec
```
https://docs.google.com/document/d/1fauhtUlcBfsSKJaQfIOSAcn4bxYjQaZPQS7ZDx36Rxc/edit
```
* Command:
```
GET /ns/api/v2/system/status
```
* Return value:
| Tag name | Type | Description |
| ------------ | ------ | ------------------------------------------------------------------------------------- |
| progress | int | value: 0~100 |
| serverStatus | string | 1. success(Status Code: 200) 2. waiting(Status Code: 512) 3. failed(Status Code: 513) |
| status | int | value: 200/521/513 |
## Over the Air editor update
如果APP預設編輯器版本比伺服器版本低時(編輯器版本等於QPKG版本)才透過下載的方式更新
* Spec
```
https://docs.google.com/document/d/13DH1fh9Mh2ssvU1vKOlxR_ln0HqNjWYBPiD1XJuWWeI/edit
```
* Command:
Because api does not provide download method, it can only be archived as a file
```
GET /ns/dist/editor/editor.js
GET /ns/dist/editor/editor.css
```
* Download to:
```
getFilesDir()/editor/editor.js
getFilesDir()/editor/css/editor.css
```
* Bind JS & CSS in to editor:
NoteEditorFragment.java:
```
String jsPath = "file:///android_asset/editor.js";
File jsFile = new File(context.getFilesDir(), "editor/editor.js");
if (jsFile.exists()) {
jsPath = "file://" + jsFile.getPath();
}
String cssPath = "file:///android_asset/css/editor.css";
File cssFile = new File(context.getFilesDir(), "editor/css/editor.css");
if (cssFile.exists()) {
cssPath = "file://" + cssFile.getPath();
}
mEditor.loadEditorFile(jsPath, cssPath);
```
## Share web contents and device images to Qnotes3
在3.0.27版,Station提供一個javascript方法,裝html轉editor json fotmat,但是這個方法經過測試發現有些問題,之後改由一個api處理
* HTML format to Qnotes3 editor format

* Get intent, action and MIME type
```
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
// Handle text being sent
} else {
// Handle single file being sent
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
// Handle multiple files being sent
}
```
* How to get html from url
```
mWebView.addJavascriptInterface(new MyJavaScriptInterface(), "HTMLOUT");
mWebView.setWebViewClient(new WebViewClient(){
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon){
isRedirected = false;
}
@Override
public void onPageFinished(WebView view, String url){
if (!isRedirected) {
if (view.getUrl().equals("about:blank") || view.getTitle().equals("about:blank")) {
// Text Only
} else {
if (urlToRender.equals(url)) {
isRedirected = true;
webTitle = view.getTitle();
mWebView.loadUrl("javascript:window.HTMLOUT.processHTML('<body>'+document.getElementsByTagName('body')[0].innerHTML+'</body>');");
}
}
}
}
});
mWebView.loadUrl(url);
protected class MyJavaScriptInterface {
@JavascriptInterface
public void processHTML(final String html) {
GetShareDataActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (webTitle.equals("") || html.equals("<body></body>")) {
// Unable to get html
} else {
// Successfully achieved html
}
}
});
}
}
```
* Upload image
首先透過parser取得所有圖片,透過Station提供的API(不需下載後上傳,不用自行處理base64圖片)

## Auto save
http://spec.qnap.com.tw/issues/13869
* For offline save note automatically instead of click save button.