/********************************************************************** * * * **********************************************************************/ //var DEBUG = DEBUG != null ? DEBUG : true /*********************************************************************/ // XXX NOTE: the widget itself does not need a title, that's the job for // a container widget (dialog, field, ...) // ...it can be implemented trivially via an attribute and a :before // CSS class... var BrowserClassPrototype = { // construct the dom... make: function(options){ var browser = $('
') .addClass('browse') // make thie widget focusable... // NOTE: tabindex 0 means automatic tab indexing and -1 means // focusable bot not tabable... //.attr('tabindex', -1) .attr('tabindex', 0) // focus the widget if something inside is clicked... .click(function(){ $(this).focus() }) if(options.path == null || options.show_path){ browser .append($('
') .addClass('v-block path')) } browser .append($('
') .addClass('v-block list')) return browser }, } // XXX Q: should we make a base list dialog and build this on that or // simplify this to implement a list (removing the path and disbling // traversal)?? // XXX need a search/filter field... // XXX need base events: // - open // - update // - select (???) // XXX add "current selection" to the path... var BrowserPrototype = { dom: null, options: { //path: null, //show_path: null, }, // XXX this should prevent event handler deligation... keyboard: { '.browse':{ Up: 'prev', Backspace: 'Up', Down: 'next', Left: 'pop', Right: 'push', Enter: 'action', Esc: 'close', }, }, // base api... // NOTE: to avoid duplicating and syncing data, the actual path is // stored in DOM... // XXX does the path includes the currently selected element? get path(){ var skip = false return this.dom.find('.path .dir:not(.cur)') .map(function(i, e){ return $(e).text() }) .toArray() }, set path(value){ // XXX normalize path... return this.update(value) }, // update path... // - build the path // - build the element list // // XXX trigger an "update" event... // XXX current path click shoud make it editable and start a live // search/filter... update: function(path){ path = path || this.path var browser = this.dom var that = this var p = browser.find('.path').empty() var l = browser.find('.list').empty() var c = [] // fill the path field... path.forEach(function(e){ c.push(e) var cur = c.slice() p.append($('
') .addClass('dir') .click(function(){ that .update(cur.slice(0, -1)) .select('"'+cur.pop()+'"') }) .text(e)) }) // add current selction indicator... p.append($('
') .addClass('dir cur') // XXX start search/filter... // - on click / letter set content editable // - triger filterig on modified // - disable nav in favor of editing // - enter/blur to exit edit mode // - esc to cancel and reset // XXX add a filter mode... .click(function(){ //that.update(path.concat($(this).text())) $(this) .text('') .attr('contenteditable', true) .keyup(function(){ that.filter($(this).text()) }) })) // fill the children list... this.list(path) .forEach(function(e){ l.append($('
') .click(function(){ that.update(that.path.concat([$(this).text()])) }) .text(e)) }) return this }, // XXX should have two non_matched modes: // - hide - hide non-matching content // - shadow - shadow non-matching content // XXX pattern modes: // - lazy match // abc -> *abc* -> ^.*abc.*$ // ab cd -> *ab*cd* -> ^.*ab.*cd.*$ // - glob // - regex // XXX sort: // - as-is // - best match filter: function(pattern, mode, non_matched, sort){ var that = this var browser = this.dom // show all... if(pattern == null || pattern.trim() == '*'){ this.update() // basic filter... } else { var l = browser.find('.list>div') l.each(function(i, e){ e = $(e) var t = e.text() var i = t.search(pattern) if(i < 0){ e.remove() } else { e.html(t.replace(pattern, pattern.bold())) } }) } return this }, // internal actions... // Select a list element... // // Select first/last child // .select('first') // .select('last') // -> elem // // Select previous/lext child // .select('prev') // .select('next') // -> elem // // Deselect // .select('none') // -> elem // // Get selected element if it exists, null otherwise... // .select('!') // -> elem // -> $() // // Select element by sequence number // .select() // -> elem // // Select element by its text... // .select('""') // -> elem // // .select() // -> elem // // This will return a jQuery object. // // // XXX revise return values... // XXX Q: should this trigger a "select" event??? select: function(elem){ var browser = this.dom var elems = browser.find('.list div') if(elems.length == 0){ return $() } elem = elem || this.select('!') // if none selected get the first... elem = elem.length == 0 ? 'first' : elem // first/last... if(elem == 'first' || elem == 'last'){ return this.select(elems[elem]()) // prev/next... } else if(elem == 'prev' || elem == 'next'){ var to = this.select('!', browser)[elem]('.list div') if(to.length == 0){ return this.select(elem == 'prev' ? 'last' : 'first', browser) } this.select('none') return this.select(to) // deselect... } else if(elem == 'none'){ browser.find('.path .dir.cur').empty() return elems .filter('.selected') .removeClass('selected') // strict... } else if(elem == '!'){ return elems.filter('.selected') // number... } else if(typeof(elem) == typeof(123)){ return this.select($(elems[elem])) // string... } else if(typeof(elem) == typeof('str') && /^'.*'$|^".*"$/.test(elem.trim())){ elem = elem.trim().slice(1, -1) return this.select(browser.find('.list div') .filter(function(i, e){ return $(e).text() == elem })) // element... } else { this.select('none') browser.find('.path .dir.cur').text(elem.text()) return elem.addClass('selected') } }, 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 }, next: function(elem){ if(elem != null){ this.select(elem) } this.select('next') return this }, prev: function(elem){ if(elem != null){ this.select(elem) } this.select('prev') 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.push(elem.text()) var res = this.open(path) return res }, // extension methods... open: function(path){ var m = this.options.list return m ? m.call(this, path) : path }, list: function(path){ var m = this.options.list return m ? m.call(this, path) : [] }, isTraversable: null, // XXX need to get a container.... // XXX prepare/merge options... // XXX setup instance events... __init__: function(parent, options){ // XXX merge options... // XXX this.options = options // build the dom... var dom = this.dom = this.constructor.make(options) // add keyboard handler... dom.keydown( keyboard.makeKeyboardHandler( this.keyboard, // XXX function(k){ window.DEBUG && console.log(k) }, 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 : */