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.
pastebin/nginx/public_html/index.js

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.bin_ids, s.login.bin_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}))