/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var actions = require('lib/actions') var features = require('lib/features') var toggler = require('lib/toggler') var core = require('features/core') var widgets = require('features/ui-widgets') var overlay = require('lib/widget/overlay') var browse = require('lib/widget/browse') /*********************************************************************/ // url history... var URLHistoryActions = actions.Actions({ config: { 'url-history-push-up-on-open': false, // values: // -1 - no limit. // 0 - disabled // 1+ - length of history // // NOTE: this does not account for pinned items. 'url-history-length': 100, }, __url_history: null, // Format: // { // url: { // open: | , // check: | , // }, // ... // } // // NOTE: last opened url is last in object, i.e. the keys are in // reverse order... // NOTE: though functions are supported they are not recommended as // we can not stringify them to JSON... get url_history(){ return this.hasOwnProperty('__url_history') ? this.__url_history : undefined }, set url_history(value){ this.__url_history = value }, // NOTE: this is created on the fly as a convenience, editing this // will have no effect... get url_history_pinned(){ var url_history = this.url_history return Object.keys(url_history) .reduce(function(res, k){ if(url_history[k].pinned){ res[k] = url_history[k] } return res }, {}) }, clone: [function(full){ return function(res){ res.url_history = null if(full && this.url_history){ res.url_history = JSON.parse(JSON.stringify(this.url_history)) } } }], // NOTE: this updates .url_history in-place... setTopURLHistory: ['- History/', function(url){ var data = this.url_history[url] if(data == null){ return } delete this.url_history[url] this.url_history[url] = data }], // NOTE: this will overwrite .url_history object... // XXX should this be in-place or overwrite by default??? sortURLHistory: ['- History/', function(order, in_place){ var that = this var data = this.url_history var ordered = {} if(in_place){ order .unique() .reverse() .forEach(function(url){ that.setTopURLHistory(url) }) } else { order .concat( Object.keys(data) .reverse()) .unique() .reverse() .forEach(function(url){ url in data && (ordered[url] = data[url]) }) // sanity check... if(Object.keys(data).length != Object.keys(ordered).length){ console.error('Something went wrong with sort:', ordered) return } this.url_history = ordered } }], // NOTE: if clear is not true then this will update a history item // rather than fully rewriting it... // NOTE: this will not auto-remove pinned items if the length of // history is more than allowed... pushURLToHistory: ['- History/', function(url, open, check, clear){ url = url || this.location.path var l = this.config['url-history-length'] || -1 if(l == 0){ return } this.url_history = this.url_history || {} var item = !clear ? (this.url_history[url] || {}) : {} open = item.open = open || this.location.method check = item.check = check || this.location.check || 'checkPath' // remove the old value... if(url in this.url_history && this.config['url-history-push-up-on-open']){ delete this.url_history[url] } // push url to history... this.url_history[url] = item /*this.url_history[url] = { open: open, check: check, }*/ // update history length... if(l > 0){ do { var k = Object.keys(this.url_history) // we will not remove pinned items... .filter((function(e){ return !this.url_history[e].pinned }).bind(this)) // drop first url in order -- first added... this.dropURLFromHistory(k[0]) } while(k.length - 1 > l) } }], // NOTE: url can be an index, 0 being the last url added to history; // negative values are also supported. dropURLFromHistory: ['- History/', function(url){ this.url_history = this.url_history || {} url = typeof(url) == typeof(123) ? Object.keys(this.url_history).reverse().slice(url)[0] : url if(url){ delete this.url_history[url] } }], checkURLFromHistory: ['- History/', function(url){ this.url_history = this.url_history || {} url = typeof(url) == typeof(123) ? Object.keys(this.url_history).reverse().slice(url)[0] : url // if we have a check action then use it... if(url && this.url_history[url] && this.url_history[url].check){ var check = this.url_history[url].check if(check === true || check === false){ return check } else if(typeof(check) == typeof('str')){ return this[check](url) } else { return check(url) } // no way to check so we do not know... } else { return null } }], pinnedURLOrder: ['- History/', core.doc`Get/set history pin order Get pin order... .pinnnedURLOrder() -> order Set pin order... .pinnnedURLOrder(, ) -> this Set pin order to 'auto'... .pinnnedURLOrder(, 'auto') -> this Auto-ordered pins are sorted in the same order as .url_history NOTE: this will not reset the pin, use .toggleURLPinned(..) for that `, function(url, order){ var e = this.url_history[url] // get... if(order == null){ return e.pinned === true ? 'auto' : 'pinned' in e ? e.pinned : null // set... } else { e.pinned = order == 'auto' ? true : order } }], toggleURLPinned: ['History/', toggler.Toggler( function(){ return this.location.path }, function(url, action){ var e = this.url_history[url] // get state... if(action == null){ return (e && e.pinned != null) ? 'on' : 'off' // change state -> 'on'... } else if(action == 'on'){ e.pinned = true // change state -> 'off'... } else if(action == 'off'){ delete e.pinned } }, ['off', 'on'])], openURLFromHistory: ['- History/', function(url, open){ this.url_history = this.url_history || {} url = typeof(url) == typeof(123) ? Object.keys(this.url_history).reverse().slice(url)[0] : url if(url && !open && this.url_history[url] && this.url_history[url].open){ open = this.url_history[url].open } if(url && open){ if(open instanceof Function){ return open(url) } else { return this[open](url) } } }], clearURLHistory: ['History/', function(){ this.url_history = null }], }) var URLHistory = module.URLHistory = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'url-history', depends: [ 'location', ], suggested: [ 'ui-url-history', 'url-history-local-storage', 'url-history-fs-writer', ], actions: URLHistoryActions, }) //--------------------------------------------------------------------- // XXX should this be responsible for saving and loading of .location??? // ...on one hand it's part of the history, on the other it's part // of file loader... var URLHistoryLocalStorageActions = actions.Actions({ config: { 'url-history-local-storage-key': 'url-history', 'url-history-loaded-local-storage-key': 'url-history-loaded', 'url-history-load-current': true, }, __url_history: null, // load url history... get url_history(){ // get the attr value... if(this.hasOwnProperty('__url_history') && this.__url_history){ return this.__url_history } var key = this.config['url-history-local-storage-key'] if(key){ // get the storage value... // if not local __url_history and we are configured, load from storage... if(this.config && key){ var history = localStorage[key] if(history){ try{ this.__url_history = JSON.parse(history) } catch(e) { delete localStorage[key] } } } } return this.hasOwnProperty('__url_history') ? this.__url_history : null }, set url_history(value){ this.__url_history = value var key = this.config['url-history-local-storage-key'] if(key){ localStorage[key] = JSON.stringify(value) } }, // Disable localStorage in child... clone: [function(){ return function(res){ res.config['url-history-local-storage-key'] = null res.config['url-history-loaded-local-storage-key'] = null } }], storeURLHistory: ['History/', function(){ var history = this.config['url-history-local-storage-key'] if(history != null){ localStorage[history] = JSON.stringify(this.url_history) } this.storeLocation() }], storeLocation: ['History/', function(){ var loaded = this.config['url-history-loaded-local-storage-key'] if(loaded != null){ localStorage[loaded] = JSON.stringify(this.location || {}) } }], loadLastSavedBasePath: ['- History/', function(){ var loaded = this.config['url-history-loaded-local-storage-key'] if(loaded && localStorage[loaded]){ var l = JSON.parse(localStorage[loaded]) if(l.current != null && this.config['url-history-load-current']){ this.one('load', function(){ this.current = l.current }) } this.openURLFromHistory(l.path, l.method) } else { this.openURLFromHistory(0) } }] }) var URLHistoryLocalStorage = module.URLHistoryLocalStorage = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'url-history-local-storage', depends: [ 'ui', 'url-history', ], isApplicable: function(){ return typeof(localStorage) != 'undefined' && localStorage != null }, actions: URLHistoryLocalStorageActions, // NOTE: loading is done by the .url_history prop... handlers: [ ['ready', function(){ this.loadLastSavedBasePath() }], ['stop.pre', function(){ this.storeURLHistory() }], // save base_path... ['load', function(){ this.location && this.location.path && this.storeLocation() }], // save... ['pushURLToHistory dropURLFromHistory setTopURLHistory', function(){ this.storeURLHistory() }], // clear... ['clearURLHistory.pre', function(){ delete this.__url_history var history = this.config['url-history-local-storage-key'] if(history){ delete localStorage[history] } var loaded = this.config['url-history-loaded-local-storage-key'] if(loaded){ delete localStorage[loaded] } }], ], }) // XXX var URLHistoryFSWriter = module.URLHistoryFSWriter = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'url-history-fs-writer', depends: [ 'fs-writer', 'url-history-local-storage', ], config: { // XXX should we add a toggler action to toggle this? 'url-history-push-to-top-on-save': false, }, handlers: [ ['saveIndex', function(res){ var that = this res.then(function(l){ // push saved to top... if(that.config['url-history-push-to-top-on-save']){ that.pushURLToHistory(l.path, l.method) // update... } else { var e = that.url_history[l.path] if(e != null){ e.open = l.method that.storeURLHistory() } else { that.pushURLToHistory(l.path, l.method) } } }) }], ], }) //--------------------------------------------------------------------- var URLHistoryUIActions = actions.Actions({ config: { // If true pushing the pin item button will also focus the item // // NOTE: Both settings have their pluses and minuses: // enabled (true) // + will keep the item on screen // - will lose context // disabled (false) // + will keep context // - will lose the item from view if list is long 'url-history-focus-on-pin': false, }, // XXX add option to force full update on dialog.update() (???) listURLHistory: ['History|File/Location history...', widgets.makeUIDialog(function(){ var that = this var data var orig_pins var cur = this.location.path // NOTE: if doing intermediate/live saves this would need to // be set to false on state update... var state_saved = false var to_sort = [] var to_remove = [] // cached fs state... var fs_state = {} // NOTE: this would require the dialog to be updated if it's // not closed... var save = function(){ if(state_saved){ return } state_saved = true var pins = data.pins // remove items... to_remove.forEach(function(e){ that.dropURLFromHistory(e) // pins... var i = pins.indexOf(e) i >= 0 && pins.splice(i, 1) // urls to be sorted... i = to_sort.indexOf(e) i >= 0 && to_sort.splice(i, 1) }) to_remove = [] // sort history... that.sortURLHistory(to_sort, true) // toggle pins... pins .concat(orig_pins || []) .unique() .forEach(function(p){ pins.indexOf(p) < 0 && that.toggleURLPinned(p, 'off') orig_pins.indexOf(p) < 0 && that.toggleURLPinned(p, 'on') }) // sort pins... pins .forEach(function(p, i){ that.pinnedURLOrder(p, i) }) } var makeHistoryList = function(fs_state){ fs_state = fs_state || {} var history = Object.keys(that.url_history).reverse() var pinned_auto = [] var pinned_sorted = [] var list = history // NOTE: this might get a little slow for // very large sets... .map(function(p){ // pinned items... var pin = that.pinnedURLOrder(p) pin == 'auto' ? pinned_auto.push(p) // prepare for sort... : pin != null ? pinned_sorted.push([p, pin]) : null return p }) // sort pins... pinned_sorted = pinned_sorted .sort(function(a, b){ return a[1] - b[1] }) .map(function(e){ return e[0] }) return { urls: list, pins: pinned_sorted.concat(pinned_auto), } } var makeDisabledChecker = function(fs_state){ return function(url){ // see of we need a full refresh or use the last fs_state... return url in fs_state ? fs_state[url] : (fs_state[url] = !that.checkURLFromHistory(url)) } } var dialog = browse.makeLister(null, function(path, make){ // live update... // XXX add option to force full update??? data = data == null ? makeHistoryList(fs_state) : data orig_pins = orig_pins == null ? data.pins.slice() : orig_pins // special case: empty list... if(data.urls.length == 0){ make.Action('No history...', {disabled: true}) return } make.EditablePinnedList(data.urls, data.pins, { list_id: 'history', new_item: false, pins_sortable: true, isItemDisabled: makeDisabledChecker(fs_state), to_remove: to_remove, buttons: [ // open... ['', function(p){ dialog.browsePath(p) }], ['♦', 'TO_TOP'], 'PIN', 'REMOVE', ], }) make .done() // highlight the current item... .then(function(){ dialog .filter(`"${cur}"`) .addClass('highlighted') }) }, { cls: 'location-history', // NOTE: we are not using path: here because it will parse // the current element as a path, and we need it as-is... selected: cur, }) .open(function(evt, path){ dialog.close() that.openURLFromHistory(path) }) .on('close', save) .on('pin_button', function(evt, p, e){ dialog.select(that.config['url-history-focus-on-pin'] ? p : '!') }) .on('to_top_button', function(evt, p, e){ to_sort.splice(0, 0, p) }) // handle 'O' button to browse path... dialog.browsePath = function(p){ this.selected && that.browsePath(p || this.selected) .close(function(evt, reason){ reason != 'reject' && dialog.close(reason) }) } dialog.keyboard.handler('General', 'O', 'browsePath') return dialog })], }) var URLHistoryUI = module.URLHistoryUI = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'ui-url-history', depends: [ 'ui', 'url-history', ], actions: URLHistoryUIActions, }) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })