diff --git a/nginx/public_html/App.js b/nginx/public_html/App.js index 801cbd1..0e6b8a0 100644 --- a/nginx/public_html/App.js +++ b/nginx/public_html/App.js @@ -1,6 +1,7 @@ import Note from './Note.js'; import { new_note_handler, + new_note_by_dblclick_handler, search_term_change_handler, sorting_change_handler, load_notes, @@ -38,7 +39,7 @@ function App(vnode_init){ (s.login.showing ? (s.login.is_logged_in ? m('.login-info', {key:'login-info'}, [ - m('.text', 'Logged in as '+s.login.user.username), + m('.text', 'Logged in as '+s.db[s.login.user_id].model.username), m('button', {onclick: o(logout_request_handler)}, 'Logout') ]) : m('.login-form', {key:'login-form'}, [ @@ -52,7 +53,7 @@ function App(vnode_init){ m('.buttons', {key:'buttons'}, [ (s.is_editing_bin_name ? m('input.bin-name-textbox', {key: 'bin-name-textbox', value: s.temp_bin_name, onchange: o(bin_name_change_handler)}) - : m('.bin-name', {key: 'bin-name'}, s.bin.name) + : m('.bin-name', {key: 'bin-name'}, s.db[s.bin_id].model.name) ), m('button', {key: 'new-bin-button', onclick: o(new_bin_handler)}, 'New Bin...'), (s.is_editing_bin_name @@ -60,14 +61,14 @@ function App(vnode_init){ : m('button', {key:'rename-bin-button', onclick: o(bin_name_editing_toggle_button_handler)}, 'Rename') ) ]), - m('ul.bin-list', {key:'bin-list'}, s.login.bins.map(b=> - m('li.bin', {key:b.id, onclick:o1(choose_bin_handler, b.id)}, b.name) + m('ul.bin-list', {key:'bin-list'}, s.login.bin_ids.map(bin_id=> + m('li.bin', {key:bin_id, onclick:o1(choose_bin_handler, bin_id)}, s.db[bin_id].model.name) )) ]) ]), m('.main', {key: 'main'}, [ - m('.notes', s.notes.map(note_state => - m(Note, {state:s, note_state, dispatch, key: note_state.note.id}) + m('.notes', {ondblclick: o(new_note_by_dblclick_handler)}, s.notes.map(note_state => + m(Note, {state:s, note_state, dispatch, key: note_state.note_id}) )) ]) ]); diff --git a/nginx/public_html/Note.js b/nginx/public_html/Note.js index 80ab83b..7e9e779 100644 --- a/nginx/public_html/Note.js +++ b/nginx/public_html/Note.js @@ -4,8 +4,9 @@ function Note(vnode_init){ const {dispatch} = vnode_init.attrs; return { view: function(vnode){ - const {note_state} = vnode.attrs; - const {is_editing, is_focused, note} = note_state; + const {state, note_state} = vnode.attrs; + const {is_editing, is_focused, note_id} = note_state; + const note = state.db[note_id].model; if(is_editing){ return m('.note', [ m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch), oncreate: is_focused?({dom})=>{ dom.focus(); delete note_state.is_focused; }:null }, note_state.temp_text), diff --git a/nginx/public_html/handlers/App.js b/nginx/public_html/handlers/App.js index bede62e..3f53a9b 100644 --- a/nginx/public_html/handlers/App.js +++ b/nginx/public_html/handlers/App.js @@ -2,13 +2,13 @@ import nanoid from '../nanoid.min.js'; import api from '../api.js'; const load_notes = function(state, dispatch){ - api.post('/load-notes', {bin_id: state.bin.id}) + api.post('/load-notes', {bin_id: state.bin_id}) .then(res=>{ dispatch('notes-loaded', res.notes); }); }; const runSearch = function(state, dispatch){ - api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin.id}) + api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin_id}) .then(res=>{ dispatch('update-search-results', res.notes); }); @@ -16,6 +16,10 @@ const runSearch = function(state, dispatch){ const new_note_handler = function(state, dispatch){ dispatch('add-note', {id: nanoid(), date: Date.now()}); }; +const new_note_by_dblclick_handler = function(state, dispatch, e){ + e.preventDefault(); // for nothing gets highlighted + dispatch('add-note', {id: nanoid(), date: Date.now()}); + }; const search_term_change_handler = function(state, dispatch, e){ if(e.code === 'Enter'){ runSearch(state, dispatch); @@ -51,8 +55,8 @@ const login_request_handler = function(state, dispatch, e){ else{ dispatch('login-failed'); } - }) - .catch(e=>{ dispatch('login-failed'); }); + }, + e=>{ dispatch('login-failed'); }); }; const logout_request_handler = function(state, dispatch, e){ api.post('/logout', {}); @@ -68,7 +72,7 @@ const bin_name_change_handler = function(state, dispatch, e){ dispatch('update-bin-name', e.target.value); }; const bin_name_commit_handler = function(state, dispatch){ - api.post('/bin-rename', {bin_id:state.bin.id, name:state.temp_bin_name}); + api.post('/bin-rename', {bin_id:state.bin_id, name:state.temp_bin_name}); dispatch('commit-bin-name'); }; const choose_bin_handler = function(state, dispatch, bin_id){ @@ -85,6 +89,7 @@ const choose_bin_handler = function(state, dispatch, bin_id){ }; export {new_note_handler, + new_note_by_dblclick_handler, search_term_change_handler, sorting_change_handler, load_notes, diff --git a/nginx/public_html/handlers/Note.js b/nginx/public_html/handlers/Note.js index 8056046..8db6e58 100644 --- a/nginx/public_html/handlers/Note.js +++ b/nginx/public_html/handlers/Note.js @@ -1,17 +1,17 @@ import api from '../api.js'; const edit_handler = function(note_state, dispatch){ - dispatch('update-note-editing', {id: note_state.note.id, is_editing: true}); + dispatch('update-note-editing', {id: note_state.note_id, is_editing: true}); }; const cancel_handler = function(note_state, dispatch){ - dispatch('update-note-editing', {id: note_state.note.id, is_editing: false}); + dispatch('update-note-editing', {id: note_state.note_id, is_editing: false}); }; const text_change_handler = function(note_state, dispatch, e){ note_state.temp_text = e.target.value; }; const save_handler = function(note_state, dispatch){ - dispatch('save-note-edit', {id: note_state.note.id, text: note_state.temp_text}); - api.post('/save', {bin_id: note_state.bin_id, note_id: note_state.note.id, text: note_state.temp_text}) + dispatch('save-note-edit', {id: note_state.note_id, text: note_state.temp_text}); + api.post('/save', {bin_id: note_state.bin_id, note_id: note_state.note_id, text: note_state.temp_text}) }; export { edit_handler, cancel_handler, text_change_handler, save_handler }; \ No newline at end of file diff --git a/nginx/public_html/handlers/index.js b/nginx/public_html/handlers/index.js index d001838..7b814ed 100644 --- a/nginx/public_html/handlers/index.js +++ b/nginx/public_html/handlers/index.js @@ -15,7 +15,7 @@ const hash_change_handler = function(state, dispatch, e){ // get bin id from URL let bin_id = window.location.hash.substring(1); // extract the leading '#' if(bin_id === ''){ - const old_bin_id = state.bin.id; + const old_bin_id = state.bin_id; window.history.replaceState(null,'', '#'+old_bin_id); } else{ diff --git a/nginx/public_html/index.js b/nginx/public_html/index.js index 6bcd45a..e41c4f3 100644 --- a/nginx/public_html/index.js +++ b/nginx/public_html/index.js @@ -24,45 +24,100 @@ if(bin_id === ''){ } // the actual loading from server is done later, after store and dispatch are defined + + +/* Integrates a database row from the server into the local store: */ +const integrate = function(s, model_object){ + const id = model_object.id; + s.db[id] = s.db[id] || {ref_count: 0, model: null}; + s.db[id].model = model_object; + }; +const i = integrate; + +/* integrates a list of model objects, as if 'integrate' were called on each element. */ +const integrateAll = function(s, model_objects){ + model_objects.forEach(mo=>{ + integrate(s, mo); + }); + }; + +/* Register a reference-by-id: */ +const ref = function(s, o, key, new_model_id){ + const old_model_id = o[key]; + s.db[new_model_id].ref_count++; + if(old_model_id && s.db[old_model_id]){ + s.db[old_model_id].ref_count--; + if(s.db[old_model_id].ref_count === 0){ + delete s.db[old_model_id]; + } + } + // this needs to come after the above 'if' statement: + o[key] = new_model_id; + }; + +/* Register references in-bulk: */ +/* o[key] refers to an array-of-ids */ +const refAll = function(s, o, key, new_model_ids){ + const old_model_ids = o[key]; + // ref the new refs: + o[key] = new_model_ids; + new_model_ids.forEach(new_model_id=>{ + s.db[new_model_id].ref_count++; + }); + // de-ref the existing refs, deleting no-longer-referred-to models: + old_model_ids.forEach(old_model_id=>{ + s.db[old_model_id].ref_count--; + // if this is the last reference, delete it from the store: + if(s.db[old_model_id].ref_count === 0){ + delete s.db[old_model_id]; + } + }); + }; + +/* TODO: routine to de-ref and ref in bulk when the id is in an object which is in an array (as opposed to refAll, where the id itself is in the array) + This routine would need to be called for state.notes, which contains refs of the form: `state.notes[n].note_id` + */ + function addToBinListIfLoggedIn(state){ const s = state; // if user is logged in: if(s.login.is_logged_in){ // if bin is not already in the list: - if(s.login.bins.filter(b=>b.id===s.bin.id).length===0){ - s.login.bins.push({id: s.bin.id, name: s.bin.name}); + if(s.login.bin_ids.filter(bin_id=>bin_id===s.bin_id).length===0){ + i(s, {id: s.bin_id, name: s.db[s.bin_id].model.name}); + s.login.bin_ids.push(s.bin_id); } } } const reducer = handleActions({ - 'new-bin': (s, bin) => { s.bin=bin; s.notes=[]; s.temp_bin_name=bin.id; }, - 'bin-requested': (s, bin_id) => { s.bin={id:bin_id}; s.notes = []; }, - 'bin-loaded': (s, bin) => { s.bin=bin; s.temp_bin_name=bin.name; }, + 'new-bin': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.notes=[]; s.temp_bin_name=bin.id; }, + 'bin-requested': (s, bin_id) => { i(s,{id:bin_id}); ref(s,s,'bin_id',bin_id); s.notes = []; }, + 'bin-loaded': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.temp_bin_name=bin.name; }, 'update-search-term': (s, search_term) => { s.search_term=search_term; }, - 'update-search-results': (s, _notes) => { s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin.id, note:n})); }, - 'add-note': (s, {id, date}) => { s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin.id, is_focused:true, note: {id: id, text: '', modified: date}}); }, - 'notes-loaded': (s, _notes) => { s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin.id, note:n})); }, - 'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below - 'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.is_editing=is_editing; note_s.temp_text=note_s.note.text; }, - 'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; note_s.temp_text=text; note_s.is_editing=false; }, + 'update-search-results': (s, _notes) => { integrateAll(s,s.notes,'note_id',_notes); s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin_id, note_id:n.id})); }, + 'add-note': (s, {id, date}) => { i(s, {id: id, text: '', modified: date}); s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin_id, is_focused:true, note_id: id}); }, + 'notes-loaded': (s, _notes) => { integrateAll(s,_notes); s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin_id, note_id:n.id})); }, + 'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); s.db[note_s.note_id].model.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below + 'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note_id===id); note_s.is_editing=is_editing; note_s.temp_text=s.db[note_s.note_id].model.text; }, + 'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); s.db[note_s.note_id].model.text=text; note_s.temp_text=text; note_s.is_editing=false; }, 'update-sorting': (s, sorting) => { s.sorting=sorting; }, 'update-username': (s, username) => { s.login.username=username; }, 'update-password': (s, password) => { s.login.password=password; }, 'login-requested': (s) => { s.login.showing=false; }, - 'login-succeeded': (s, {user, session_id}) => { s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; s.login.user=user; s.login.session_id=session_id; }, + 'login-succeeded': (s, {user, session_id}) => { i(s, user); s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; ref(s, s.login, 'user_id', user.id); s.login.session_id=session_id; }, 'login-failed': (s) => { s.login.showing=true; s.login.password=''; }, - 'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; s.login.bins=[]; }, - 'user-bin-list-loaded': (s, bins) => { s.login.bins=bins; }, + 'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; refAll(s, s.login, 'bin_ids', []); }, + 'user-bin-list-loaded': (s, bins) => { s.login.bins=bins; integrateAll(s, bins); refAll(s, s.login, 'bin_ids', bins.map(b=>b.id)); }, 'update-bin-name-editing': (s, is_editing) => { s.is_editing_bin_name=is_editing; }, 'update-bin-name': (s, name) => { s.temp_bin_name=name; }, - 'commit-bin-name': (s) => { s.bin.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); } + 'commit-bin-name': (s) => { s.db[s.bin_id].model.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); } }, { - bin: {id: bin_id, name: bin_id, user_id: ''}, + bin_id: bin_id, is_editing_bin_name: false, temp_bin_name: bin_id, notes: [ - //{is_editing: false, temp_text: '', bin_id: '', note: {id: nanoid(), text: 'Note one', modified: 1}}, + //{is_editing: false, temp_text: '', bin_id: '', note_id: ''}, ], search_term: '', sorting: 'new->old', @@ -71,12 +126,18 @@ const reducer = handleActions({ username: '', // value of textbox password: '', // value of textbox is_logged_in: false, - user: null /*{ + user_id: '' /*{ id: '', username: '' }*/, session_id: '', - bins: [] + bin_ids: [] + }, + db: { + [bin_id]: { + ref_count: 1, + model: {id: bin_id, name: bin_id, user_id: ''} + } } //search_result_notes: [] });