# 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 ![](https://i.imgur.com/6KXHC4a.png) ## How to call editor function ![](https://i.imgur.com/2DnA5PK.png) ## How to get editor function callback ![](https://i.imgur.com/CGh6QzK.png) ## How to listen content changes ![](https://i.imgur.com/SCle4ir.png) ## 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 ![](https://i.imgur.com/PQV4eEP.png) * 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圖片) ![](https://i.imgur.com/MkPdJCv.png) ## Auto save http://spec.qnap.com.tw/issues/13869 * For offline save note automatically instead of click save button.