You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
7.5 KiB
JavaScript
166 lines
7.5 KiB
JavaScript
import api from './api.js';
|
|
import App from './App.js';
|
|
import nanoid from './nanoid.min.js';
|
|
import {initial_load_bin_handler, hash_change_handler} from './handlers/index.js';
|
|
import {bin, note} from './models.js';
|
|
const produce = immer.produce;
|
|
immer.setAutoFreeze(false); // needed for high-frequency updated values, like onkeyup->note.temp_text; only once 'save' is called will it produce a new immutable state tree
|
|
|
|
var root = document.body;
|
|
|
|
/* Ruthlessly taken from [https://gist.github.com/kitze/fb65f527803a93fb2803ce79a792fff8]: */
|
|
const handleActions = (actionsMap, defaultState) =>
|
|
(state=defaultState, {type, payload}) =>
|
|
produce(state, draft => {
|
|
const action = actionsMap[type];
|
|
action && action(draft, payload);
|
|
});
|
|
|
|
// get bin id from URL, and load notes from that bin; if a bin id isn't specified in the URL, create a new one and update the URL
|
|
let bin_id = window.location.hash.substring(1); // extract the leading '#'
|
|
if(bin_id === ''){
|
|
bin_id = nanoid();
|
|
window.history.replaceState(null,'', '#'+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.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});
|
|
// causes problems because ref_count isn't properly updated:
|
|
//s.login.bin_ids.push(s.bin_id);
|
|
ref(s, s.login.bind_ids, s.login.bind_ids.length, s.bin_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
const reducer = handleActions({
|
|
'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) => { 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}) => { 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=''; refAll(s, s.login, 'bin_ids', []); },
|
|
'user-bin-list-loaded': (s, 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.db[s.bin_id].model.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); }
|
|
}, {
|
|
bin_id: bin_id,
|
|
is_editing_bin_name: false,
|
|
temp_bin_name: bin_id,
|
|
notes: [
|
|
//{is_editing: false, temp_text: '', bin_id: '', note_id: ''},
|
|
],
|
|
search_term: '',
|
|
sorting: 'new->old',
|
|
login: {
|
|
showing: true,
|
|
username: '', // value of textbox
|
|
password: '', // value of textbox
|
|
is_logged_in: false,
|
|
user_id: '' /*{
|
|
id: '',
|
|
username: ''
|
|
}*/,
|
|
session_id: '',
|
|
bin_ids: []
|
|
},
|
|
db: {
|
|
[bin_id]: {
|
|
ref_count: 1,
|
|
model: {id: bin_id, name: bin_id, user_id: ''}
|
|
}
|
|
}
|
|
//search_result_notes: []
|
|
});
|
|
|
|
// create Redux store, with Redux DevTools enabled:
|
|
const store = Redux.createStore(reducer, /* preloadedState, */
|
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
|
|
const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); };
|
|
|
|
store.subscribe(()=>{
|
|
const state = store.getState();
|
|
m.render(root, m(App, {state, dispatch}));
|
|
});
|
|
|
|
api.setStore(store);
|
|
|
|
window.addEventListener("hashchange", (e)=>hash_change_handler(store.getState(), dispatch, e), false);
|
|
|
|
let state = store.getState();
|
|
initial_load_bin_handler(state, dispatch, bin_id);
|
|
|
|
// we don't want Mithril auto-redraw system in place, since Redux will manually re-render when necessary with store.subscribe():
|
|
// m.mount(root, App);
|
|
m.render(root, m(App, {state, dispatch})) |