')
.addClass('dir')
.click(function(){
that
.update(cur.slice(0, -1))
.select('"'+cur.pop()+'"')
})
.text(e))
})
// add current selction indicator...
p.append($('
')
.addClass('dir cur')
.click(function(){
that.startFilter()
//that.update(path.concat($(this).text()))
// XXX HACK: prevents the field from bluring when clicked...
// ...need to find a better way...
that._hold_blur = true
setTimeout(function(){ delete that._hold_blur }, 20)
})
// XXX for some reason this gets triggered when clicking ano
// is not triggered when entering via '/'
.on('blur', function(){
// XXX HACK: prevents the field from bluring when clicked...
// ...need to find a better way...
if(!that._hold_blur){
that.stopFilter()
}
//that.stopFilter()
})
.keyup(function(){
that.filter($(this).text())
}))
// fill the children list...
this.list(path)
.forEach(function(e){
l.append($('
')
.click(function(){
// handle clicks ONLY when not disabled...
if(!$(this).hasClass('disabled')){
that.update(that.path.concat([$(this).text()]))
}
})
.text(e))
})
return this
},
// internal actions...
// XXX pattern modes:
// - lazy match
// abc -> *abc* -> ^.*abc.*$
// ab cd -> *ab*cd* -> ^.*ab.*cd.*$
// - glob
// - regex
// XXX sort:
// - as-is
// - best match
// XXX add deep-mode filtering...
// if '/' is in the pattern then we list down and combine paths...
// XXX might be good to use the same mechanism for this and .select(..)
filter: function(pattern){
var that = this
var browser = this.dom
// show all...
if(pattern == null || pattern.trim() == '*'){
browser.find('.filtered-out')
.removeClass('filtered-out')
// clear the highlighing...
browser.find('.list b')
.replaceWith(function() { return this.innerHTML })
// basic filter...
} else {
var l = browser.find('.list>div:not(disabled)')
l.each(function(i, e){
e = $(e)
var t = e.text()
var i = t.search(pattern)
if(i < 0){
e
.addClass('filtered-out')
.removeClass('selected')
} else {
e.removeClass('filtered-out')
.html(t.replace(pattern, pattern.bold()))
}
})
}
return this
},
// XXX make this a toggler... (???)
startFilter: function(){
var range = document.createRange()
var selection = window.getSelection()
var that = this
var e = this.dom.find('.path .dir.cur')
.text('')
.attr('contenteditable', true)
.focus()
// place the cursor...
range.setStart(e[0], 0)
range.collapse(true)
// XXX
selection.removeAllRanges()
selection.addRange(range)
return this
},
stopFilter: function(){
this.filter('*')
this.dom.find('.path .dir.cur')
.text('')
.removeAttr('contenteditable')
this
.focus()
return this
},
get filtering(){
return this.dom.find('.path .dir.cur[contenteditable]').length > 0
},
toggleFilterMode: function(){
this.dom.toggleClass('show-filtered-out')
return this
},
// Select a list element...
//
// Get selected element if it exists, otherwise select and return
// the first...
// .select()
// -> elem
//
// Get selected element if it exists, null otherwise...
// .select('!')
// -> elem
// -> $()
//
// Select first/last child
// .select('first')
// .select('last')
// -> elem
//
// Select previous/next child
// .select('prev')
// .select('next')
// -> elem
//
// Deselect
// .select('none')
// -> $()
//
// Select element by sequence number
// NOTE: negative numbers count from the tail.
// NOTE: overflowing selects the first/last element.
// .select()
// -> elem
//
// Select element by its text...
// NOTE: if text matches one of the reserved commands above use
// quotes to escape it...
// .select('')
// .select("''")
// .select('""')
// -> elem
//
// Select element via a regular expression...
// .select()
// -> elem
// -> $()
//
// Select jQuery object...
// .select()
// -> elem
// -> $()
//
// This will return a jQuery object.
//
// NOTE: if multiple matches occur this will select the first.
// NOTE: 'none' will always return an empty jQuery object, to get
// the selection state before deselecting use .select('!')
//
//
// XXX Q: should this trigger a "select" event???
// XXX the scroll handling might be a bit inaccurate...
select: function(elem, filtering){
var pattern = '.list div:not(.disabled):not(.filtered-out)'
var browser = this.dom
var elems = browser.find(pattern)
filtering = filtering == null ? this.filtering : filtering
if(elems.length == 0){
return $()
}
// empty list/string selects none...
elem = elem != null && elem.length == 0 ? 'none' : elem
// 0 or no args (null) selects first...
elem = elem == 0 || elem == null ? 'first' : elem
// first/last...
if(elem == 'first' || elem == 'last'){
return this.select(elems[elem](), filtering)
// prev/next...
} else if(elem == 'prev' || elem == 'next'){
var to = this.select('!', filtering)[elem + 'All'](pattern).first()
if(to.length == 0){
return this.select(elem == 'prev' ? 'last' : 'first', filtering)
}
this.select('none', filtering)
return this.select(to, filtering)
// deselect...
} else if(elem == 'none'){
if(!filtering){
browser.find('.path .dir.cur').empty()
}
elems
.filter('.selected')
.removeClass('selected')
return $()
// strict...
} else if(elem == '!'){
return elems.filter('.selected')
// number...
// NOTE: on overflow this will get the first/last element...
} else if(typeof(elem) == typeof(123)){
return this.select($(elems.slice(elem)[0] || elems.slice(-1)[0] ), filtering)
// string...
} else if(typeof(elem) == typeof('str')){
if(/^'.*'$|^".*"$/.test(elem.trim())){
elem = elem.trim().slice(1, -1)
}
return this.select(browser.find(pattern)
.filter(function(i, e){
return $(e).text() == elem
}), filtering)
// regexp...
} else if(elem.constructor === RegExp){
return this.select(browser.find(pattern)
.filter(function(i, e){
return elem.test($(e).text())
}), filtering)
// element...
} else {
elem = $(elem).first()
if(elem.length == 0){
this.select(null, filtering)
} else {
this.select('none', filtering)
if(!filtering){
browser.find('.path .dir.cur').text(elem.text())
}
// handle scroll position...
var p = elem.scrollParent()
var S = p.scrollTop()
var H = p.height()
var h = elem.height()
var t = elem.offset().top - p.offset().top
var D = 3 * h
// too low...
if(t+h+D > H){
p.scrollTop(S + (t+h+D) - H)
// too high...
} else if(t < D){
p.scrollTop(S + t - D)
}
return elem.addClass('selected')
}
}
},
// Select next element...
next: function(elem){
if(elem != null){
this.select(elem)
}
this.select('next')
return this
},
// Select previous element...
prev: function(elem){
if(elem != null){
this.select(elem)
}
this.select('prev')
return this
},
// Push an element to path / go down one level...
push: function(elem){
var browser = this.dom
var elem = this.select(elem || '!')
// nothing selected, select first and exit...
if(elem.length == 0){
this.select()
return this
}
var path = this.path
path.push(elem.text())
// if not traversable call the action...
if(this.isTraversable != null
&& (this.isTraversable !== false
|| ! this.isTraversable(path))){
return this.action(path)
}
this.path = path
this.select()
return this
},
// Pop an element off the path / go up one level...
pop: function(){
var browser = this.dom
var path = this.path
var dir = path.pop()
this.update(path)
this.select('"'+dir+'"')
return this
},
focus: function(){
this.dom.focus()
return this
},
// XXX think about the API...
// XXX trigger an "open" event...
action: function(){
var elem = this.select('!')
// nothing selected, select first and exit...
if(elem.length == 0){
this.select()
return this
}
var path = this.path
path.push(elem.text())
var res = this.open(path)
return res
},
// extension methods...
// XXX this is wrong...
// ...need to actually open something...
open: function(path){
path = path || this.path
var m = this.options.list
return m ? m.call(this, path) : path
},
list: function(path){
path = path || this.path
var m = this.options.list
return m ? m.call(this, path) : []
},
isTraversable: null,
// XXX need to get a container....
// XXX setup instance events...
__init__: function(parent, options){
options = options || {}
// merge options...
var opts = Object.create(this.options)
Object.keys(options).forEach(function(n){ opts[n] = options[n] })
options = this.options = opts
// build the dom...
var dom = this.dom = this.constructor.make(options)
// add keyboard handler...
dom.keydown(
keyboard.makeKeyboardHandler(
this.keyboard,
options.logKeys,
this))
// attach to parent...
if(parent != null){
parent.append(dom)
}
// load the initial state...
this.update(this.path)
},
}
/*
var Browser =
//module.Browser =
object.makeConstructor('Browser',
BrowserClassPrototype,
BrowserPrototype)
*/
/**********************************************************************
* vim:set ts=4 sw=4 : */