ImageGrid/ui (gen4)/experiments/browse-dialog.html

803 lines
16 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html>
<style>
.container {
display: inline-block;
position: absolute;
top: 100px;
left: 100px;
box-shadow: rgba(0,0,0,0.5) 0.1em 0.1em 0.4em;
/* make the container expand only to a certain size, then scroll */
/* XXX need to:
- auto-scroll vertically
- use custom scroll bars
- shorten path to fit width
i.e. manage width manually when at max-width...
*/
max-height: 60vh;
max-width: 60vw;
height: auto;
width: auto;
overflow-y: auto;
overflow-x: hidden;
}
.browse {
display: inline-block;
min-width: 300px;
width: initial;
background: gray;
color: rgba(255,255,255,0.8);
padding: 5px;
font-family: sans-serif;
}
/*
.browse:not(:focus) {
opacity: 0.8;
}
*/
.browse .v-block {
width: 100%;
height: auto;
box-sizing: border-box;
border-top: 1px solid rgba(255,255,255, 0.3);
}
.browse .v-block:first-of-type {
border-top: none;
}
.browse .v-block:empty {
display: none;
}
.browse .title {
font-weight: bold;
color: rgba(255,255,255,0.9);
padding: 5px;
padding-left: 10px;
padding-right: 10px;
}
.browse .path {
padding: 5px;
padding-left: 10px;
padding-right: 10px;
white-space: nowrap;
}
.browse .path:empty {
display: block;
}
.browse .path:before {
content: "/";
}
.browse .path .dir {
display: inline-block;
}
.browse .path .dir:after {
content: "/";
}
/* XXX need to make this resizable up but only to a certain size (~80vh) */
/*
.browse .list {
max-height: 50vh;
}
.browse .list:empty {
display: block;
}
*/
.browse .list div {
padding: 5px;
padding-left: 10px;
padding-right: 10px;
}
.browse:focus .list div.selected,
.browse .path .dir:hover,
.browse .list div:hover {
color: white;
background: rgba(0,0,0, 0.05);
}
.browse:focus .list div.selected {
background: rgba(0,0,0, 0.1);
box-shadow: rgba(0,0,0,0.2) 0.1em 0.1em 0.2em;
}
.browse .list div.selected {
background: rgba(0,0,0, 0.08);
}
</style>
<script src="../ext-lib/jquery.js"></script>
<script src="../ext-lib/jquery-ui.js"></script>
<script src="../lib/jli.js"></script>
<script src="../ext-lib/require.js"></script>
<script>
var TREE = {
dir_a: {},
dir_b: {
file1: 'this is a file',
file2: 'this is a file',
file3: 'this is a file',
},
dir_c: {
file1: 'this is a file',
dir_b: {
file1: 'this is a file',
},
dir_c: {},
dir_d: {},
dir_e: {},
dir_f: {},
dir_g: {},
dir_h: {},
dir_i: {},
dir_j: {},
dir_k: {},
dir_l: {},
dir_m: {},
dir_o: {},
dir_p: {},
dir_q: {},
dir_r: {},
dir_s: {},
dir_t: {},
dir_u: {},
},
file: 'this is a file',
}
// add some recursion for testing...
TREE.dir_d = TREE.dir_c.dir_b
TREE.dir_a.tree = TREE
TREE.dir_c.tree = TREE
TREE.dir_c.dir_b.tree = TREE
function skipFiles(e, v){
return typeof(v) != typeof('str')
}
function make(title){
var browser = $('<div>')
.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()
})
.append($('<div>')
.addClass('v-block title')
.text(title))
.append($('<div>')
.addClass('v-block path'))
.append($('<div>')
.addClass('v-block list'))
.append($('<div>')
.addClass('v-block info'))
.append($('<div>')
.addClass('v-block actions'))
return browser
}
// low level update...
function update(browser, path, list){
browser = browser || $('.browse')
var p = browser.find('.path').empty()
var l = browser.find('.list').empty()
path.forEach(function(e){
p.append($('<div>')
.addClass('dir')
.click(popDir)
.text(e))
})
list.forEach(function(e){
l.append($('<div>')
.click(pushDir)
.text(e))
})
return browser
}
function showPath(browser, path, tree, skip){
// XXX remove for pruduction...
skip = skip || skipFiles
browser = browser || $('.browse')
path = path.constructor !== Array ? path.split(/[\\\/]+/g) : path
path = path.filter(function(e){ return e != '' })
var dir = tree
path.forEach(function(d){
dir = dir[d]
})
// XXX do error checking...
dir = skip != null
// skip files....
? Object.keys(dir).filter(function(e){ return skip(e, dir[e]) })
: Object.keys(dir)
update(browser, path, dir)
}
// XXX this will skip incuding the current dir...
// ...this might work and might not work...
function getPath(browser, to){
var skip = false
return browser.find('.path .dir')
.filter(function(i, e){
if(e === to){
skip = true
}
return !skip
})
.map(function(i, e){ return $(e).text() })
.toArray()
}
function pushDir(){
var dir = $(this).text()
var browser = $(this).parents('.browse')
var path = getPath(browser)
path.push(dir)
showPath(browser, path, TREE)
}
function popDir(){
var dir = $(this).text()
var browser = $(this).parents('.browse')
var path = getPath(browser, this)
showPath(browser, path, TREE)
select('"'+dir+'"')
}
// base control actions...
// XXX add keywords:
// 'first'
// 'prev'
// 'next'
// 'last'
// 'none' -- deselect
// '!' -- return selected element if it exists...
// <number> -- select by sequence number...
// <elem> -- select a specific element...
function select(elem, browser){
browser = browser || $('.browse')
var elems = browser.find('.list div')
if(elems.length == 0){
return $()
}
elem = elem || select('!')
// if none selected get the first...
elem = elem.length == 0 ? 'first' : elem
// first/last...
if(elem == 'first' || elem == 'last'){
return select(elems[elem](), browser)
// prev/next...
} else if(elem == 'prev' || elem == 'next'){
var to = select('!', browser)[elem]('.list div')
if(to.length == 0){
return select(elem == 'prev' ? 'last' : 'first', browser)
}
select('none')
return select(to, browser)
// deselect...
} else if(elem == 'none'){
return elems
.filter('.selected')
.removeClass('selected')
// strict...
} else if(elem == '!'){
return elems.filter('.selected')
// number...
} else if(typeof(elem) == typeof(123)){
return select($(elems[elem]), browser)
// string...
} else if(typeof(elem) == typeof('str') && /^'.*'$|^".*"$/.test(elem.trim())){
elem = elem.trim().slice(1, -1)
return select(browser.find('.list div').filter(function(i, e){
return $(e).text() == elem
}))
// element...
} else {
select('none', browser)
return elem.addClass('selected')
}
}
function push(browser){
browser = browser || $('.browse')
var elem = select('!', browser)
if(elem.length == 0){
return select()
}
var path = getPath(browser)
path.push(elem.text())
var res = showPath(browser, path, TREE)
select(null, browser)
return res
}
function pop(browser){
browser = browser || $('.browse')
var path = getPath(browser)
var dir = path.pop()
var res = showPath(browser, path, TREE)
select('"'+dir+'"')
return res
}
function next(browser){
return select('next', browser)
}
function prev(browser){
return select('prev', browser)
}
// default action...
function action(browser){
browser = browser || $('.browse')
var cur = select('!', browser).text()
alert('/' + getPath(browser).concat(cur == '' ? [cur] : [cur, '']).join('/'))
return false
}
var KB = {
'*':{
F5: function(){ window.location.reload() },
},
//'.browse:focus':{
'.browse':{
Up: prev,
Backspace: 'Up',
Down: next,
Left: pop,
Right: push,
Enter: action,
},
}
//---
// 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 = $('<div>')
.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($('<div>')
.addClass('v-block path'))
}
browser
.append($('<div>')
.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:
// - opne
// - update
// - select (???)
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')
.map(function(i, e){ return $(e).text() })
.toArray()
},
set path(value){
// XXX normalize path...
return this.update(path)
},
// update path...
// XXX trigger an "update" event...
update: function(path){
var browser = this.dom
var p = browser.find('.path').empty()
var l = browser.find('.list').empty()
// fill the path field...
path.forEach(function(e){
p.append($('<div>')
.addClass('dir')
.click(popDir)
.text(e))
})
// fill the children list...
this.list(path)
.forEach(function(e){
l.append($('<div>')
.click(pushDir)
.text(e))
})
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(<number>)
// -> elem
//
// Select element by its text...
// .select('"<text>"')
// -> elem
//
// .select(<elem>)
// -> 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'){
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')
return elem.addClass('selected')
}
},
// XXX check if we need to do the ,action when the element id not traversable...
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.push(elem.text())
// if not traversable call the action...
if(this.isTraversable && ! this.isTraversable(path)){
return this.action(path)
}
this
.update(path)
.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
},
// 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(){ },
list: function(){ },
isTraversable: null,
// XXX need to get a container....
// XXX prepare/merge options...
__init__: function(parent, options){
// XXX merge options...
// XXX
// 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)
}
},
}
/*
var Browser =
//module.Browser =
object.makeConstructor('Browser',
BrowserClassPrototype,
BrowserPrototype)
*/
//---
requirejs(['../lib/keyboard', '../object'], function(k, o){
keyboard = k
object = o
// setup base keyboard for devel, in case something breaks...
//$(document)
$('.browse')
.keydown(
keyboard.makeKeyboardHandler(
KB,
function(k){ window.DEBUG && console.log(k) }))
})
$(function(){
var browser = make()
$('.container')
.empty()
.append(browser)
showPath(browser, '/', TREE)
$('.container').draggable({
cancel: ".path .dir, .list div"
})
browser.focus()
})
</script>
<body>
<button onclick="pop()">&lt;</button>
<button onclick="push()">&gt;</button>
<button onclick="prev()">^</button>
<button onclick="next()">v</button>
<div class="container">
<div class="browse">
<!-- title, optional -->
<div class="v-block title">
[title]
</div>
<!-- the actual list -->
<div class="v-block path">
<div class="dir">
[dir]
</div>
<div class="dir">
[dir]
</div>
<div class="dir">
[dir]
</div>
</div>
<!-- the actual list -->
<div class="v-block list">
<div>
[dir]
</div>
<div>
[dir]
</div>
<div>
[dir]
</div>
</div>
<!-- info, optional -->
<div class="v-block info">
[info]
</div>
<!-- buttons, optional -->
<div class="v-block actions">
[actions]
</div>
</div>
</div>
</body>
</html>
<!-- vim:set ts=4 sw=4 : -->