diff --git a/App.js b/App.js index 5c5353f..2b7721e 100644 --- a/App.js +++ b/App.js @@ -1,10 +1,10 @@ import Note from './Note.js'; -import {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes} from './handlers/App.js'; +import {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes, new_bin_handler} from './handlers/App.js'; function App(vnode_init){ const {state, dispatch} = vnode_init.attrs; - load_notes(state, dispatch); + //load_notes(state, dispatch); return { view: function(vnode){ const s = vnode.attrs.state; @@ -20,12 +20,12 @@ function App(vnode_init){ ]), m('.top-right', {key: 'top-right'}, [ m('.bin-id', {key: 'bin-id'}, s.bin.id), - m('button', {key: 'new-bin-button'}, 'New Bin...') + m('button', {key: 'new-bin-button', onclick: new_bin_handler.bind(null, s, dispatch)}, 'New Bin...') ]) ]), m('.main', {key: 'main'}, [ m('.notes', s.notes.map(note_state => - m(Note, {note_state, dispatch, key: note_state.note.id}) + m(Note, {state, note_state, dispatch, key: note_state.note.id}) )) ]) ]); diff --git a/Note.js b/Note.js index 6b8323e..a887b7b 100644 --- a/Note.js +++ b/Note.js @@ -8,7 +8,7 @@ function Note(vnode_init){ const {is_editing, note} = note_state; if(is_editing){ return m('.note', [ - m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note.text), + m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note_state.temp_text), m('.buttons', {key: 'editing-buttons'}, [ m('button', {key: 'cancel-button', onclick: cancel_handler.bind(null, note_state, dispatch) }, 'Cancel'), m('button', {key: 'save-button', onclick: save_handler.bind(null, note_state, dispatch) }, 'Save') diff --git a/api-stub.js b/api-stub.js index b704c18..334bf83 100644 --- a/api-stub.js +++ b/api-stub.js @@ -4,10 +4,15 @@ const api_stub = {}; // for mocking persistent state, for use by the fake endpoints, just as a real server endpoint would have DB access for state: const state = { - notes: [ - {id: nanoid(), text: 'Note one', modified: Date.now()}, - {id: nanoid(), text: 'Note two', modified: Date.now()}, - {id: nanoid(), text: 'Note three', modified: Date.now()} + bins: [ + { + id: nanoid(), + notes: [ + {id: nanoid(), text: 'Note one', modified: Date.now()}, + {id: nanoid(), text: 'Note two', modified: Date.now()}, + {id: nanoid(), text: 'Note three', modified: Date.now()} + ] + } ] }; @@ -16,11 +21,15 @@ const endpoints = { }; endpoints.post['/load-notes'] = function(resolve, reject, body){ - resolve( {status: 'ok', notes: state.notes} ); + let i = 0; + if(body.bin_id){ + i = state.bins.findIndex(b=>b.id===body.bin_id); + } + resolve( {status: 'ok', notes: state.bins[i].notes} ); }; endpoints.post['/search'] = function(resolve, reject, body){ - const notes = state.notes.filter(n => n.text.indexOf(body.search_term) !== -1 ) + const notes = state.bins.find(b=>b.id===body.bin_id).notes.filter(n => n.text.indexOf(body.search_term) !== -1 ) if(body.sorting==='new->old'){ notes.sort((a,b) => a.modified-b.modified); } @@ -32,12 +41,18 @@ endpoints.post['/search'] = function(resolve, reject, body){ endpoints.post['/save'] = function(resolve, reject, body){ const {note_id, text} = body; - const note = state.notes.find( n => n.id===note_id ); + let bin = state.bins.find(b=>b.id===body.bin_id); + if(!bin){ + bin = {id: body.bin_id, notes: []}; + state.bins.push(bin); + } + const note = bin.notes.find( n => n.id===note_id ); if(note){ note.text = text; + note.modified = Date.now(); } else{ - state.notes.push({id: note_id, text, modified: Date.now()}); + bin.notes.push({id: note_id, text, modified: Date.now()}); } resolve( {status: 'ok'} ); }; diff --git a/handlers/App.js b/handlers/App.js index 6633404..22d8b66 100644 --- a/handlers/App.js +++ b/handlers/App.js @@ -1,33 +1,39 @@ import nanoid from '../nanoid.min.js'; -import api from '../api.js'; -//import api from '../api-stub.js'; +//import api from '../api.js'; +import api from '../api-stub.js'; const load_notes = function(state, dispatch){ api.post('/load-notes', {bin_id: state.bin.id}) .then(res=>{ - dispatch({type: 'notes-loaded', notes: res.notes}); + dispatch('notes-loaded', res.notes); }); }; const runSearch = function(state, dispatch){ - api.post('/search', {search_term: state.search_term, sorting: state.sorting}) + api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin.id}) .then(res=>{ - dispatch({type:'update-search-results', notes: res.notes}); + dispatch('update-search-results', res.notes); }); }; const new_note_handler = function(state, dispatch){ - dispatch({type:'add-note', id: nanoid()}); + dispatch('add-note', {id: nanoid(), date: Date.now()}); }; const search_term_change_handler = function(state, dispatch, e){ if(e.code === 'Enter'){ runSearch(state, dispatch); } else{ - dispatch({type:'update-search-term', search_term: e.target.value}); + dispatch('update-search-term', e.target.value); } }; const sorting_change_handler = function(state, dispatch, e){ runSearch(state, dispatch); - dispatch({type:'update-sorting', sorting: e.target.value}); + dispatch('update-sorting', e.target.value); + }; +const new_bin_handler = function(state, dispatch){ + const id = nanoid(); + // TODO: consolidate: this will cause two redraws: + dispatch('new-bin', {id}); + dispatch('notes-loaded', []); }; -export {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes}; \ No newline at end of file +export {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes, new_bin_handler}; \ No newline at end of file diff --git a/handlers/Note.js b/handlers/Note.js index a48f899..e69de5d 100644 --- a/handlers/Note.js +++ b/handlers/Note.js @@ -1,21 +1,18 @@ -import api from '../api.js'; -//import api from '../api-stub.js'; +//import api from '../api.js'; +import api from '../api-stub.js'; const edit_handler = function(note_state, dispatch){ - dispatch({type: 'update-note-editing', note_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({type: 'update-note-editing', note_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){ - // TODO: consolidate: this will cause two redraws: - dispatch({type: 'update-note-text', note_id: note_state.note.id, text: note_state.temp_text}); - //note.text = temp_text; - dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: false}); - api.post('/save', {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/immer.min.js b/immer.min.js new file mode 100644 index 0000000..b2fe444 --- /dev/null +++ b/immer.min.js @@ -0,0 +1,2 @@ +!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).immer={})}(this,(function(n){function t(n){for(var t=arguments.length,r=Array(t>1?t-1:0),e=1;e3?t.t-4:t.t:Array.isArray(n)?1:v(n)?2:s(n)?3:0}function o(n,t){return 2===u(n)?n.has(t):Object.prototype.hasOwnProperty.call(n,t)}function f(n,t){return 2===u(n)?n.get(t):n[t]}function a(n,t,r){var e=u(n);2===e?n.set(t,r):3===e?(n.delete(t),n.add(r)):n[t]=r}function c(n,t){return n===t?0!==n||1/n==1/t:n!=n&&t!=t}function v(n){return X&&n instanceof Map}function s(n){return q&&n instanceof Set}function l(n){return n.i||n.u}function p(n){if(Array.isArray(n))return Array.prototype.slice.call(n);var t=U(n);delete t[L];for(var r=T(t),e=0;e1&&(n.set=n.add=n.clear=n.delete=d),Object.freeze(n),t&&i(n,(function(n,t){return h(t,!0)}),!0),n)}function d(){t(2)}function y(n){return null==n||"object"!=typeof n||Object.isFrozen(n)}function _(n){var r=V[n];return r||t(18,n),r}function b(n,t){V[n]||(V[n]=t)}function m(){return N}function j(n,t){t&&(_("Patches"),n.o=[],n.v=[],n.s=t)}function O(n){w(n),n.l.forEach(M),n.l=null}function w(n){n===N&&(N=n.p)}function S(n){return N={l:[],p:N,h:n,_:!0,m:0}}function M(n){var t=n[L];0===t.t||1===t.t?t.j():t.O=!0}function P(n,r){r.m=r.l.length;var i=r.l[0],u=void 0!==n&&n!==i;return r.h.S||_("ES5").M(r,n,u),u?(i[L].P&&(O(r),t(4)),e(n)&&(n=g(r,n),r.p||x(r,n)),r.o&&_("Patches").g(i[L],n,r.o,r.v)):n=g(r,i,[]),O(r),r.o&&r.s(r.o,r.v),n!==G?n:void 0}function g(n,t,r){if(y(t))return t;var e=t[L];if(!e)return i(t,(function(i,u){return A(n,e,t,i,u,r)}),!0),t;if(e.A!==n)return t;if(!e.P)return x(n,e.u,!0),e.u;if(!e.R){e.R=!0,e.A.m--;var u=4===e.t||5===e.t?e.i=p(e.k):e.i;i(3===e.t?new Set(u):u,(function(t,i){return A(n,e,u,t,i,r)})),x(n,u,!1),r&&n.o&&_("Patches").F(e,r,n.o,n.v)}return e.i}function A(n,t,i,u,f,c){if(r(f)){var v=g(n,f,c&&t&&3!==t.t&&!o(t.D,u)?c.concat(u):void 0);if(a(i,u,v),!r(v))return;n._=!1}if(e(f)&&!y(f)){if(!n.h.K&&n.m<1)return;g(n,f),t&&t.A.p||x(n,f)}}function x(n,t,r){void 0===r&&(r=!1),n.h.K&&n._&&h(t,r)}function z(n,t){var r=n[L];return(r?l(r):n)[t]}function E(n,t){if(t in n)for(var r=Object.getPrototypeOf(n);r;){var e=Object.getOwnPropertyDescriptor(r,t);if(e)return e;r=Object.getPrototypeOf(r)}}function R(n){n.P||(n.P=!0,n.p&&R(n.p))}function k(n){n.i||(n.i=p(n.u))}function F(n,t,r){var e=v(t)?_("MapSet").$(t,r):s(t)?_("MapSet").C(t,r):n.S?function(n,t){var r=Array.isArray(n),e={t:r?1:0,A:t?t.A:m(),P:!1,R:!1,D:{},p:t,u:n,k:null,i:null,j:null,I:!1},i=e,u=Y;r&&(i=[e],u=Z);var o=Proxy.revocable(i,u),f=o.revoke,a=o.proxy;return e.k=a,e.j=f,a}(t,r):_("ES5").J(t,r);return(r?r.A:m()).l.push(e),e}function D(n){return r(n)||t(22,n),function n(t){if(!e(t))return t;var r,o=t[L],c=u(t);if(o){if(!o.P&&(o.t<4||!_("ES5").N(o)))return o.u;o.R=!0,r=K(t,c),o.R=!1}else r=K(t,c);return i(r,(function(t,e){o&&f(o.u,t)===e||a(r,t,n(e))})),3===c?new Set(r):r}(n)}function K(n,t){switch(t){case 2:return new Map(n);case 3:return Array.from(n)}return p(n)}function $(){function n(n,t){var r=f[n];return r?r.enumerable=t:f[n]=r={configurable:!0,enumerable:t,get:function(){return Y.get(this[L],n)},set:function(t){Y.set(this[L],n,t)}},r}function t(n){for(var t=n.length-1;t>=0;t--){var r=n[t][L];if(!r.P)switch(r.t){case 5:u(r)&&R(r);break;case 4:e(r)&&R(r)}}}function e(n){for(var t=n.u,r=n.k,e=T(r),i=e.length-1;i>=0;i--){var u=e[i];if(u!==L){var f=t[u];if(void 0===f&&!o(t,u))return!0;var a=r[u],v=a&&a[L];if(v?v.u!==f:!c(a,f))return!0}}var s=!!t[L];return e.length!==T(t).length+(s?0:1)}function u(n){var t=n.k;if(t.length!==n.u.length)return!0;var r=Object.getOwnPropertyDescriptor(t,t.length-1);return!(!r||r.get)}var f={};b("ES5",{J:function(t,r){var e=Array.isArray(t),i=function(t,r){if(t){for(var e=Array(r.length),i=0;i1?e-1:0),f=1;f1?r-1:0),u=1;u=0;e--){var i=t[e];if(0===i.path.length&&"replace"===i.op){n=i.value;break}}var u=_("Patches").W;return r(n)?u(n,t):this.produce(n,(function(n){return u(n,t.slice(e+1))}))},n}(),tn=new nn,rn=tn.produce,en=tn.produceWithPatches.bind(tn),un=tn.setAutoFreeze.bind(tn),on=tn.setUseProxies.bind(tn),fn=tn.applyPatches.bind(tn),an=tn.createDraft.bind(tn),cn=tn.finishDraft.bind(tn);n.Immer=nn,n.applyPatches=fn,n.castDraft=function(n){return n},n.castImmutable=function(n){return n},n.createDraft=an,n.current=D,n.default=rn,n.enableAllPlugins=function(){$(),I(),C()},n.enableES5=$,n.enableMapSet=I,n.enablePatches=C,n.finishDraft=cn,n.freeze=h,n.immerable=H,n.isDraft=r,n.isDraftable=e,n.nothing=G,n.original=function(n){return r(n)||t(23,n),n[L].u},n.produce=rn,n.produceWithPatches=en,n.setAutoFreeze=un,n.setUseProxies=on,Object.defineProperty(n,"__esModule",{value:!0})})); +//# sourceMappingURL=immer.umd.production.min.js.map diff --git a/index.html b/index.html index 90a6b7e..20a687b 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,9 @@ - + diff --git a/index.js b/index.js index 09fe7a7..79f8250 100644 --- a/index.js +++ b/index.js @@ -1,77 +1,42 @@ import App from './App.js'; import nanoid from './nanoid.min.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; -function bin_reducer(old_state, new_state, action){ - new_state.bin = old_state.bin; - } - -function search_reducer(old_state, new_state, action){ - if(action.type === 'update-search-term'){ - new_state.search_term = action.search_term; - } - else if(action.type === 'update-search-results'){ - new_state.notes = action.notes.map(note=>({is_editing: false, note})); - new_state.search_term = old_state.search_term; - } - else{ - new_state.search_term = old_state.search_term; - } - } - -function notes_reducer(old_state, new_state, action){ - if(action.type === 'add-note'){ - new_state.notes = old_state.notes.concat([{is_editing: true, note: {id: action.id, text: '', modified: Date.now()}}]) - } - else if(action.type === 'notes-loaded'){ - new_state.notes = action.notes.map(note=>({is_editing: false, note})); - } - else if(action.type === 'update-note-text'){ - const i = old_state.notes.findIndex(note_state => note_state.note.id === action.note_id); - new_state.notes = old_state.notes.slice(); - new_state.notes[i] = {...new_state.notes[i], note: {...new_state.notes[i].note, text: action.text, modified: Date.now()}}; - } - else if(action.type === 'update-note-editing'){ - const i = old_state.notes.findIndex(note_state => note_state.note.id === action.note_id); - new_state.notes = old_state.notes.slice(); - new_state.notes[i] = {...new_state.notes[i], is_editing: action.is_editing}; - } - else{ - new_state.notes = old_state.notes; - } - } - -function sorting_reducer(old_state, new_state, action){ - if(action.type === 'update-sorting'){ - new_state.sorting = action.sorting; - } - else{ - new_state.sorting = old_state.sorting; - } - } - -function reducer(old_state, action){ - const new_state = {}; - bin_reducer(old_state, new_state, action); - notes_reducer(old_state, new_state, action); - search_reducer(old_state, new_state, action); - sorting_reducer(old_state, new_state, action); - return new_state; - } +/* 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); + }); + +const reducer = handleActions({ + 'new-bin': (s, bin) => { s.bin=bin; }, + '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, 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-sorting': (s, sorting) => { s.sorting=sorting; } + }, { + bin: {id: nanoid()}, + notes: [ + //{is_editing: false, temp_text: '', bin_id: '', note: {id: nanoid(), text: 'Note one', modified: 1}}, + ], + search_term: '', + sorting: 'new->old' + //search_result_notes: [] + }); // create Redux store, with Redux DevTools enabled: -const store = Redux.createStore(reducer, /* preloadedState, */ { - bin: {id: nanoid()}, - notes: [ - //{is_editing: false, note: {id: nanoid(), text: 'Note one', modified: 1}}, - ], - search_term: '', - sorting: 'new->old' - //search_result_notes: [] - }, +const store = Redux.createStore(reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); -const dispatch = function(...args){ store.dispatch(...args); }; +const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); }; store.subscribe(()=>{ m.render(root, m(App, {state: store.getState(), dispatch}));