2018-03-26 01:41:29 +08:00
import treeService from './tree.js' ;
2018-04-09 10:38:52 +08:00
import treeUtils from './tree_utils.js' ;
2018-03-26 01:41:29 +08:00
import noteTypeService from './note_type.js' ;
import protectedSessionService from './protected_session.js' ;
2018-03-26 09:16:57 +08:00
import protectedSessionHolder from './protected_session_holder.js' ;
2018-03-25 23:09:17 +08:00
import utils from './utils.js' ;
import server from './server.js' ;
2018-03-26 09:16:57 +08:00
import messagingService from "./messaging.js" ;
2018-03-26 09:29:35 +08:00
import infoService from "./info.js" ;
2018-07-28 23:59:55 +08:00
import linkService from "./link.js" ;
2018-03-26 11:25:17 +08:00
import treeCache from "./tree_cache.js" ;
import NoteFull from "../entities/note_full.js" ;
2018-03-27 12:22:02 +08:00
import noteDetailCode from './note_detail_code.js' ;
import noteDetailText from './note_detail_text.js' ;
2018-03-28 10:11:06 +08:00
import noteDetailFile from './note_detail_file.js' ;
2018-03-28 09:36:01 +08:00
import noteDetailSearch from './note_detail_search.js' ;
import noteDetailRender from './note_detail_render.js' ;
2018-07-29 22:06:13 +08:00
import bundleService from "./bundle.js" ;
2018-08-07 04:29:03 +08:00
import noteAutocompleteService from "./note_autocomplete.js" ;
2018-03-25 23:09:17 +08:00
const $noteTitle = $ ( "#note-title" ) ;
2018-03-27 12:22:02 +08:00
const $noteDetailComponents = $ ( ".note-detail-component" ) ;
2018-03-25 23:09:17 +08:00
const $protectButton = $ ( "#protect-button" ) ;
const $unprotectButton = $ ( "#unprotect-button" ) ;
const $noteDetailWrapper = $ ( "#note-detail-wrapper" ) ;
const $noteIdDisplay = $ ( "#note-id-display" ) ;
2018-08-06 15:41:01 +08:00
const $attributeList = $ ( "#attribute-list" ) ;
const $attributeListInner = $ ( "#attribute-list-inner" ) ;
2018-04-09 10:38:52 +08:00
const $childrenOverview = $ ( "#children-overview" ) ;
2018-07-29 22:06:13 +08:00
const $scriptArea = $ ( "#note-detail-script-area" ) ;
2018-08-06 20:43:42 +08:00
const $promotedAttributesContainer = $ ( "#note-detail-promoted-attributes" ) ;
2018-03-25 23:09:17 +08:00
let currentNote = null ;
let noteChangeDisabled = false ;
let isNoteChanged = false ;
2018-03-28 09:46:38 +08:00
const components = {
'code' : noteDetailCode ,
'text' : noteDetailText ,
2018-03-28 10:11:06 +08:00
'file' : noteDetailFile ,
2018-03-28 09:46:38 +08:00
'search' : noteDetailSearch ,
'render' : noteDetailRender
} ;
function getComponent ( type ) {
if ( components [ type ] ) {
return components [ type ] ;
}
else {
infoService . throwError ( "Unrecognized type: " + type ) ;
}
}
2018-03-25 23:09:17 +08:00
function getCurrentNote ( ) {
return currentNote ;
}
function getCurrentNoteId ( ) {
2018-03-26 11:25:17 +08:00
return currentNote ? currentNote . noteId : null ;
2018-03-25 23:09:17 +08:00
}
2018-03-27 12:22:02 +08:00
function getCurrentNoteType ( ) {
const currentNote = getCurrentNote ( ) ;
return currentNote ? currentNote . type : null ;
}
2018-03-25 23:09:17 +08:00
function noteChanged ( ) {
if ( noteChangeDisabled ) {
return ;
}
2018-03-24 12:54:50 +08:00
2018-03-25 23:09:17 +08:00
isNoteChanged = true ;
}
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
async function reload ( ) {
// no saving here
2017-11-05 05:54:27 +08:00
2018-04-09 10:38:52 +08:00
await loadNoteDetail ( getCurrentNoteId ( ) ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
async function switchToNote ( noteId ) {
if ( getCurrentNoteId ( ) !== noteId ) {
await saveNoteIfChanged ( ) ;
2017-11-05 05:54:27 +08:00
2018-04-09 10:38:52 +08:00
await loadNoteDetail ( noteId ) ;
2017-11-05 05:54:27 +08:00
}
2018-03-25 23:09:17 +08:00
}
2017-11-05 05:54:27 +08:00
2018-04-08 20:21:49 +08:00
async function saveNote ( ) {
2018-03-25 23:09:17 +08:00
const note = getCurrentNote ( ) ;
2017-11-05 05:54:27 +08:00
2018-03-28 09:36:01 +08:00
note . title = $noteTitle . val ( ) ;
2018-03-28 09:46:38 +08:00
note . content = getComponent ( note . type ) . getContent ( ) ;
2017-11-05 05:54:27 +08:00
2018-03-28 09:36:01 +08:00
treeService . setNoteTitle ( note . noteId , note . title ) ;
2017-11-05 05:54:27 +08:00
2018-04-08 20:21:49 +08:00
await server . put ( 'notes/' + note . noteId , note . dto ) ;
2017-11-15 11:50:56 +08:00
2018-03-25 23:09:17 +08:00
isNoteChanged = false ;
2017-11-05 05:54:27 +08:00
2018-04-08 20:21:49 +08:00
if ( note . isProtected ) {
protectedSessionHolder . touchProtectedSession ( ) ;
}
2018-03-26 09:29:35 +08:00
infoService . showMessage ( "Saved!" ) ;
2018-03-25 23:09:17 +08:00
}
2018-01-27 08:54:27 +08:00
2018-04-08 20:21:49 +08:00
async function saveNoteIfChanged ( ) {
if ( ! isNoteChanged ) {
return ;
}
await saveNote ( ) ;
}
2018-03-25 23:09:17 +08:00
function setNoteBackgroundIfProtected ( note ) {
2018-08-17 21:21:59 +08:00
const isProtected = note . isProtected ;
2018-03-08 09:19:53 +08:00
2018-08-17 15:32:07 +08:00
$noteDetailWrapper . toggleClass ( "protected" , isProtected ) ;
2018-06-02 23:47:16 +08:00
$protectButton . toggleClass ( "active" , isProtected ) ;
$unprotectButton . toggleClass ( "active" , ! isProtected ) ;
2018-08-17 21:21:59 +08:00
$unprotectButton . prop ( "disabled" , ! protectedSessionHolder . isProtectedSessionAvailable ( ) ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
let isNewNoteCreated = false ;
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
function newNoteCreated ( ) {
isNewNoteCreated = true ;
}
2017-11-05 05:54:27 +08:00
2018-03-27 12:22:02 +08:00
async function handleProtectedSession ( ) {
await protectedSessionService . ensureProtectedSession ( currentNote . isProtected , false ) ;
if ( currentNote . isProtected ) {
protectedSessionHolder . touchProtectedSession ( ) ;
}
// this might be important if we focused on protected note when not in protected note and we got a dialog
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protectedSessionService . ensureDialogIsClosed ( ) ;
}
2018-04-09 10:38:52 +08:00
async function loadNoteDetail ( noteId ) {
2018-03-25 23:09:17 +08:00
currentNote = await loadNote ( noteId ) ;
2018-02-13 12:53:00 +08:00
2018-03-25 23:09:17 +08:00
if ( isNewNoteCreated ) {
isNewNoteCreated = false ;
2018-02-25 03:42:52 +08:00
2018-03-25 23:09:17 +08:00
$noteTitle . focus ( ) . select ( ) ;
}
2018-03-11 22:49:22 +08:00
2018-03-25 23:09:17 +08:00
$noteIdDisplay . html ( noteId ) ;
2018-03-11 22:49:22 +08:00
2018-06-02 23:47:16 +08:00
setNoteBackgroundIfProtected ( currentNote ) ;
2018-03-25 23:09:17 +08:00
$noteDetailWrapper . show ( ) ;
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
noteChangeDisabled = true ;
2017-11-05 05:54:27 +08:00
2018-03-27 12:22:02 +08:00
try {
$noteTitle . val ( currentNote . title ) ;
2017-12-25 22:46:11 +08:00
2018-03-27 12:22:02 +08:00
noteTypeService . setNoteType ( currentNote . type ) ;
noteTypeService . setNoteMime ( currentNote . mime ) ;
2017-11-05 05:54:27 +08:00
2018-03-27 12:22:02 +08:00
$noteDetailComponents . hide ( ) ;
2017-11-15 11:50:56 +08:00
2018-08-17 21:21:59 +08:00
await handleProtectedSession ( ) ;
2018-03-28 09:46:38 +08:00
await getComponent ( currentNote . type ) . show ( ) ;
2018-03-27 11:48:45 +08:00
}
2018-03-27 12:22:02 +08:00
finally {
noteChangeDisabled = false ;
2018-03-25 23:09:17 +08:00
}
2018-01-22 12:36:09 +08:00
2018-03-25 23:09:17 +08:00
treeService . setBranchBackgroundBasedOnProtectedStatus ( noteId ) ;
2018-01-24 12:41:22 +08:00
2018-03-25 23:09:17 +08:00
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper . scrollTop ( 0 ) ;
2018-01-24 12:41:22 +08:00
2018-07-30 02:51:28 +08:00
$scriptArea . html ( '' ) ;
2018-07-30 00:39:10 +08:00
await bundleService . executeRelationBundles ( getCurrentNote ( ) , 'runOnNoteView' ) ;
2018-08-06 14:59:26 +08:00
2018-08-07 18:48:11 +08:00
const attributes = await loadAttributes ( ) ;
const hideChildrenOverview = attributes . some ( attr => attr . type === 'label' && attr . name === 'hideChildrenOverview' ) ;
await showChildrenOverview ( hideChildrenOverview ) ;
2018-04-09 10:38:52 +08:00
}
2018-04-11 11:15:41 +08:00
async function showChildrenOverview ( hideChildrenOverview ) {
if ( hideChildrenOverview ) {
$childrenOverview . hide ( ) ;
return ;
}
2018-04-09 10:38:52 +08:00
const note = getCurrentNote ( ) ;
$childrenOverview . empty ( ) ;
const notePath = treeService . getCurrentNotePath ( ) ;
for ( const childBranch of await note . getChildBranches ( ) ) {
const link = $ ( '<a>' , {
href : 'javascript:' ,
text : await treeUtils . getNoteTitle ( childBranch . noteId , childBranch . parentNoteId )
2018-08-15 16:14:14 +08:00
} ) . attr ( 'data-action' , 'note' ) . attr ( 'data-note-path' , notePath + '/' + childBranch . noteId ) ;
2018-04-09 10:38:52 +08:00
const childEl = $ ( '<div class="child-overview">' ) . html ( link ) ;
$childrenOverview . append ( childEl ) ;
}
2018-04-11 11:15:41 +08:00
$childrenOverview . show ( ) ;
2018-03-25 23:09:17 +08:00
}
2018-03-07 13:17:18 +08:00
2018-08-06 14:59:26 +08:00
async function loadAttributes ( ) {
2018-08-06 20:43:42 +08:00
$promotedAttributesContainer . empty ( ) ;
2018-08-07 17:38:00 +08:00
$attributeList . hide ( ) ;
2018-08-06 14:59:26 +08:00
const noteId = getCurrentNoteId ( ) ;
const attributes = await server . get ( 'notes/' + noteId + '/attributes' ) ;
const promoted = attributes . filter ( attr => ( attr . type === 'label-definition' || attr . type === 'relation-definition' ) && attr . value . isPromoted ) ;
2018-08-14 19:50:04 +08:00
let idx = 1 ;
2018-08-06 14:59:26 +08:00
2018-08-06 23:24:35 +08:00
async function createRow ( definitionAttr , valueAttr ) {
const definition = definitionAttr . value ;
2018-08-13 15:07:21 +08:00
const inputId = "promoted-input-" + idx ;
2018-08-06 23:24:35 +08:00
const $tr = $ ( "<tr>" ) ;
const $labelCell = $ ( "<th>" ) . append ( valueAttr . name ) ;
const $input = $ ( "<input>" )
. prop ( "id" , inputId )
2018-08-14 19:50:04 +08:00
. prop ( "tabindex" , definitionAttr . position )
2018-08-07 17:38:00 +08:00
. prop ( "attribute-id" , valueAttr . isOwned ? valueAttr . attributeId : '' ) // if not owned, we'll force creation of a new attribute instead of updating the inherited one
2018-08-06 23:24:35 +08:00
. prop ( "attribute-type" , valueAttr . type )
. prop ( "attribute-name" , valueAttr . name )
. prop ( "value" , valueAttr . value )
. addClass ( "form-control" )
. addClass ( "promoted-attribute-input" ) ;
2018-08-13 15:07:21 +08:00
idx ++ ;
2018-08-07 04:29:03 +08:00
const $inputCell = $ ( "<td>" ) . append ( $ ( "<div>" ) . addClass ( "input-group" ) . append ( $input ) ) ;
2018-08-06 23:24:35 +08:00
const $actionCell = $ ( "<td>" ) ;
const $multiplicityCell = $ ( "<td>" ) ;
$tr
. append ( $labelCell )
. append ( $inputCell )
. append ( $actionCell )
. append ( $multiplicityCell ) ;
if ( valueAttr . type === 'label' ) {
if ( definition . labelType === 'text' ) {
$input . prop ( "type" , "text" ) ;
2018-08-07 04:52:49 +08:00
2018-08-13 21:58:37 +08:00
// no need to await for this, can be done asynchronously
server . get ( 'attributes/values/' + encodeURIComponent ( valueAttr . name ) ) . then ( attributeValues => {
if ( attributeValues . length === 0 ) {
return ;
}
$input . autocomplete ( {
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source : attributeValues . map ( attribute => {
return {
attribute : attribute ,
value : attribute
}
} ) ,
minLength : 0
} ) ;
$input . focus ( ( ) => $input . autocomplete ( "search" , "" ) ) ;
2018-08-07 04:52:49 +08:00
} ) ;
2018-08-06 23:24:35 +08:00
}
else if ( definition . labelType === 'number' ) {
$input . prop ( "type" , "number" ) ;
}
else if ( definition . labelType === 'boolean' ) {
$input . prop ( "type" , "checkbox" ) ;
if ( valueAttr . value === "true" ) {
$input . prop ( "checked" , "checked" ) ;
}
}
else if ( definition . labelType === 'date' ) {
$input . prop ( "type" , "text" ) ;
$input . datepicker ( {
changeMonth : true ,
changeYear : true ,
dateFormat : "yy-mm-dd"
} ) ;
const $todayButton = $ ( "<button>" ) . addClass ( "btn btn-small" ) . text ( "Today" ) . click ( ( ) => {
$input . val ( utils . formatDateISO ( new Date ( ) ) ) ;
$input . trigger ( "change" ) ;
} ) ;
$actionCell . append ( $todayButton ) ;
}
else {
messagingService . logError ( "Unknown labelType=" + definitionAttr . labelType ) ;
}
}
2018-08-07 04:29:03 +08:00
else if ( valueAttr . type === 'relation' ) {
if ( valueAttr . value ) {
$input . val ( ( await treeUtils . getNoteTitle ( valueAttr . value ) + " (" + valueAttr . value + ")" ) ) ;
}
2018-08-14 19:50:04 +08:00
// no need to wait for this
noteAutocompleteService . initNoteAutocomplete ( $input ) ;
2018-08-07 04:29:03 +08:00
}
else {
messagingService . logError ( "Unknown attribute type=" + valueAttr . type ) ;
return ;
}
2018-08-06 23:24:35 +08:00
if ( definition . multiplicityType === "multivalue" ) {
2018-08-06 23:53:13 +08:00
const addButton = $ ( "<span>" )
. addClass ( "glyphicon glyphicon-plus pointer" )
. prop ( "title" , "Add new attribute" )
. click ( async ( ) => {
2018-08-06 23:24:35 +08:00
const $new = await createRow ( definitionAttr , {
attributeId : "" ,
type : valueAttr . type ,
name : definitionAttr . name ,
value : ""
} ) ;
$tr . after ( $new ) ;
2018-08-14 19:50:04 +08:00
$new . find ( 'input' ) . focus ( ) ;
2018-08-06 23:24:35 +08:00
} ) ;
2018-08-06 23:53:13 +08:00
const removeButton = $ ( "<span>" )
. addClass ( "glyphicon glyphicon-trash pointer" )
. prop ( "title" , "Remove this attribute" )
. click ( async ( ) => {
2018-08-06 23:24:35 +08:00
if ( valueAttr . attributeId ) {
await server . remove ( "notes/" + noteId + "/attributes/" + valueAttr . attributeId ) ;
}
$tr . remove ( ) ;
} ) ;
2018-08-06 23:53:13 +08:00
$multiplicityCell . append ( addButton ) . append ( " " ) . append ( removeButton ) ;
2018-08-06 23:24:35 +08:00
}
2018-08-13 21:58:37 +08:00
2018-08-06 23:24:35 +08:00
return $tr ;
}
2018-08-06 14:59:26 +08:00
if ( promoted . length > 0 ) {
2018-08-14 19:50:04 +08:00
const $tbody = $ ( "<tbody>" ) ;
2018-08-06 20:43:42 +08:00
for ( const definitionAttr of promoted ) {
2018-08-06 21:23:22 +08:00
const definitionType = definitionAttr . type ;
const valueType = definitionType . substr ( 0 , definitionType . length - 11 ) ;
2018-08-06 23:24:35 +08:00
let valueAttrs = attributes . filter ( el => el . name === definitionAttr . name && el . type === valueType ) ;
2018-08-06 20:43:42 +08:00
if ( valueAttrs . length === 0 ) {
valueAttrs . push ( {
attributeId : "" ,
2018-08-06 21:23:22 +08:00
type : valueType ,
2018-08-06 20:43:42 +08:00
name : definitionAttr . name ,
value : ""
} ) ;
}
2018-08-06 23:24:35 +08:00
if ( definitionAttr . value . multiplicityType === 'singlevalue' ) {
valueAttrs = valueAttrs . slice ( 0 , 1 ) ;
}
2018-08-06 20:43:42 +08:00
for ( const valueAttr of valueAttrs ) {
2018-08-06 23:24:35 +08:00
const $tr = await createRow ( definitionAttr , valueAttr ) ;
2018-08-06 21:58:59 +08:00
2018-08-14 19:50:04 +08:00
$tbody . append ( $tr ) ;
2018-08-06 14:59:26 +08:00
}
}
2018-08-14 19:50:04 +08:00
// we replace the whole content in one step so there can't be any race conditions
// (previously we saw promoted attributes doubling)
$promotedAttributesContainer . empty ( ) . append ( $tbody ) ;
2018-08-06 14:59:26 +08:00
}
2018-08-06 15:41:01 +08:00
else {
$attributeListInner . html ( '' ) ;
2018-08-06 21:23:22 +08:00
if ( attributes . length > 0 ) {
2018-08-06 15:41:01 +08:00
for ( const attribute of attributes ) {
if ( attribute . type === 'label' ) {
$attributeListInner . append ( utils . formatLabel ( attribute ) + " " ) ;
}
else if ( attribute . type === 'relation' ) {
2018-08-06 17:30:37 +08:00
$attributeListInner . append ( attribute . name + "=" ) ;
2018-08-06 15:41:01 +08:00
$attributeListInner . append ( await linkService . createNoteLink ( attribute . value ) ) ;
$attributeListInner . append ( " " ) ;
}
else if ( attribute . type === 'label-definition' || attribute . type === 'relation-definition' ) {
$attributeListInner . append ( attribute . name + " definition " ) ;
}
else {
messagingService . logError ( "Unknown attr type: " + attribute . type ) ;
}
}
$attributeList . show ( ) ;
}
}
2018-07-28 23:59:55 +08:00
2018-08-07 18:48:11 +08:00
return attributes ;
2018-07-28 23:59:55 +08:00
}
2018-03-25 23:09:17 +08:00
async function loadNote ( noteId ) {
2018-03-26 11:25:17 +08:00
const row = await server . get ( 'notes/' + noteId ) ;
return new NoteFull ( treeCache , row ) ;
2018-03-25 23:09:17 +08:00
}
2018-02-05 09:23:30 +08:00
2018-03-25 23:09:17 +08:00
function focus ( ) {
const note = getCurrentNote ( ) ;
2018-02-05 09:23:30 +08:00
2018-03-28 09:46:38 +08:00
getComponent ( note . type ) . focus ( ) ;
2018-03-25 23:09:17 +08:00
}
2018-01-23 11:14:03 +08:00
2018-08-01 15:26:02 +08:00
messagingService . subscribeToSyncMessages ( syncData => {
2018-03-26 09:16:57 +08:00
if ( syncData . some ( sync => sync . entityName === 'notes' && sync . entityId === getCurrentNoteId ( ) ) ) {
2018-03-26 09:29:35 +08:00
infoService . showMessage ( 'Reloading note because of background changes' ) ;
2018-03-26 09:16:57 +08:00
reload ( ) ;
}
} ) ;
2018-08-06 20:43:42 +08:00
$promotedAttributesContainer . on ( 'change' , '.promoted-attribute-input' , async event => {
const $attr = $ ( event . target ) ;
2018-08-06 21:23:22 +08:00
let value ;
if ( $attr . prop ( "type" ) === "checkbox" ) {
value = $attr . is ( ':checked' ) ? "true" : "false" ;
}
2018-08-07 04:29:03 +08:00
else if ( $attr . prop ( "attribute-type" ) === "relation" ) {
if ( $attr . val ( ) ) {
value = treeUtils . getNoteIdFromNotePath ( linkService . getNotePathFromLabel ( $attr . val ( ) ) ) ;
}
}
2018-08-06 21:23:22 +08:00
else {
value = $attr . val ( ) ;
}
2018-08-06 23:24:35 +08:00
const result = await server . put ( "notes/" + getCurrentNoteId ( ) + "/attribute" , {
2018-08-06 20:43:42 +08:00
attributeId : $attr . prop ( "attribute-id" ) ,
type : $attr . prop ( "attribute-type" ) ,
name : $attr . prop ( "attribute-name" ) ,
2018-08-06 21:23:22 +08:00
value : value
2018-08-06 20:43:42 +08:00
} ) ;
2018-08-06 23:24:35 +08:00
$attr . prop ( "attribute-id" , result . attributeId ) ;
2018-08-06 20:43:42 +08:00
infoService . showMessage ( "Attribute has been saved." ) ;
} ) ;
2018-03-25 23:09:17 +08:00
$ ( document ) . ready ( ( ) => {
$noteTitle . on ( 'input' , ( ) => {
noteChanged ( ) ;
2017-11-30 10:13:12 +08:00
2018-03-25 23:09:17 +08:00
const title = $noteTitle . val ( ) ;
2017-11-05 05:54:27 +08:00
2018-03-25 23:09:17 +08:00
treeService . setNoteTitle ( getCurrentNoteId ( ) , title ) ;
2017-11-05 05:54:27 +08:00
} ) ;
2018-03-27 12:22:02 +08:00
noteDetailText . focus ( ) ;
2018-03-25 23:09:17 +08:00
} ) ;
2018-03-27 10:29:14 +08:00
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
// this sends the request asynchronously and doesn't wait for result
2018-03-28 10:27:46 +08:00
$ ( window ) . on ( 'beforeunload' , ( ) => { saveNoteIfChanged ( ) ; } ) ; // don't convert to short form, handler doesn't like returned promise
2018-03-27 10:29:14 +08:00
2018-03-25 23:09:17 +08:00
setInterval ( saveNoteIfChanged , 5000 ) ;
export default {
reload ,
switchToNote ,
setNoteBackgroundIfProtected ,
loadNote ,
getCurrentNote ,
getCurrentNoteType ,
getCurrentNoteId ,
newNoteCreated ,
focus ,
2018-08-06 14:59:26 +08:00
loadAttributes ,
2018-04-08 20:21:49 +08:00
saveNote ,
2018-03-27 12:22:02 +08:00
saveNoteIfChanged ,
noteChanged
2018-03-25 23:09:17 +08:00
} ;