Major overhaul for standalone operation.

This removes some of the `edbob` reliance, as well as borrowing some templates
and styling etc. from Dtail.
This commit is contained in:
Lance Edgar 2013-09-01 15:31:50 -07:00
parent b9f61e6a47
commit 2a50e704ef
59 changed files with 1969 additions and 39 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,10 @@
/**
* Copyright (c) 2009 Sergiy Kovalchuk (serg472@gmail.com)
*
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* Following code is based on Element.mask() implementation from ExtJS framework (http://extjs.com/)
*
*/
(function(a){a.fn.mask=function(c,b){a(this).each(function(){if(b!==undefined&&b>0){var d=a(this);d.data("_mask_timeout",setTimeout(function(){a.maskElement(d,c)},b))}else{a.maskElement(a(this),c)}})};a.fn.unmask=function(){a(this).each(function(){a.unmaskElement(a(this))})};a.fn.isMasked=function(){return this.hasClass("masked")};a.maskElement=function(d,c){if(d.data("_mask_timeout")!==undefined){clearTimeout(d.data("_mask_timeout"));d.removeData("_mask_timeout")}if(d.isMasked()){a.unmaskElement(d)}if(d.css("position")=="static"){d.addClass("masked-relative")}d.addClass("masked");var e=a('<div class="loadmask"></div>');if(navigator.userAgent.toLowerCase().indexOf("msie")>-1){e.height(d.height()+parseInt(d.css("padding-top"))+parseInt(d.css("padding-bottom")));e.width(d.width()+parseInt(d.css("padding-left"))+parseInt(d.css("padding-right")))}if(navigator.userAgent.toLowerCase().indexOf("msie 6")>-1){d.find("select").addClass("masked-hidden")}d.append(e);if(c!==undefined){var b=a('<div class="loadmask-msg" style="display:none;"></div>');b.append("<div>"+c+"</div>");d.append(b);b.css("top",Math.round(d.height()/2-(b.height()-parseInt(b.css("padding-top"))-parseInt(b.css("padding-bottom")))/2)+"px");b.css("left",Math.round(d.width()/2-(b.width()-parseInt(b.css("padding-left"))-parseInt(b.css("padding-right")))/2)+"px");b.show()}};a.unmaskElement=function(b){if(b.data("_mask_timeout")!==undefined){clearTimeout(b.data("_mask_timeout"));b.removeData("_mask_timeout")}b.find(".loadmask-msg,.loadmask").remove();b.removeClass("masked");b.removeClass("masked-relative");b.find("select").removeClass("masked-hidden")}})(jQuery);

View file

@ -0,0 +1,327 @@
/*
* jQuery UI Menubar @VERSION
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Menubar
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
* jquery.ui.position.js
* jquery.ui.menu.js
*/
(function( $ ) {
// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
// there has to be just one item that has tabindex
$.widget( "ui.menubar", {
version: "@VERSION",
options: {
autoExpand: false,
buttons: false,
items: "li",
menuElement: "ul",
menuIcon: false,
position: {
my: "left top",
at: "left bottom"
}
},
_create: function() {
var that = this;
this.menuItems = this.element.children( this.options.items );
this.items = this.menuItems.children( "button, a" );
this.menuItems
.addClass( "ui-menubar-item" )
.attr( "role", "presentation" );
// let only the first item receive focus
this.items.slice(1).attr( "tabIndex", -1 );
this.element
.addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
.attr( "role", "menubar" );
this._focusable( this.items );
this._hoverable( this.items );
this.items.siblings( this.options.menuElement )
.menu({
position: {
within: this.options.position.within
},
select: function( event, ui ) {
ui.item.parents( "ul.ui-menu:last" ).hide();
that._close();
// TODO what is this targetting? there's probably a better way to access it
$(event.target).prev().focus();
that._trigger( "select", event, ui );
},
menus: that.options.menuElement
})
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
})
// TODO use _on
.bind( "keydown.menubar", function( event ) {
var menu = $( this );
if ( menu.is( ":hidden" ) ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.LEFT:
that.previous( event );
event.preventDefault();
break;
case $.ui.keyCode.RIGHT:
that.next( event );
event.preventDefault();
break;
}
});
this.items.each(function() {
var input = $(this),
// TODO menu var is only used on two places, doesn't quite justify the .each
menu = input.next( that.options.menuElement );
// might be a non-menu button
if ( menu.length ) {
// TODO use _on
input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
// ignore triggered focus event
if ( event.type === "focus" && !event.originalEvent ) {
return;
}
event.preventDefault();
// TODO can we simplify or extractthis check? especially the last two expressions
// there's a similar active[0] == menu[0] check in _open
if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
that._close();
return;
}
if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
if( that.options.autoExpand ) {
clearTimeout( that.closeTimer );
}
that._open( event, menu );
}
})
// TODO use _on
.bind( "keydown", function( event ) {
switch ( event.keyCode ) {
case $.ui.keyCode.SPACE:
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
that._open( event, $( this ).next() );
event.preventDefault();
break;
case $.ui.keyCode.LEFT:
that.previous( event );
event.preventDefault();
break;
case $.ui.keyCode.RIGHT:
that.next( event );
event.preventDefault();
break;
}
})
.attr( "aria-haspopup", "true" );
// TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
if ( that.options.menuIcon ) {
input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
// TODO use _on
input.bind( "click.menubar mouseenter.menubar", function( event ) {
if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
that._close();
}
});
}
input
.addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
.attr( "role", "menuitem" )
.wrapInner( "<span class='ui-button-text'></span>" );
if ( that.options.buttons ) {
input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
}
});
that._on( {
keydown: function( event ) {
if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
var active = that.active;
that.active.blur();
that._close( event );
active.prev().focus();
}
},
focusin: function( event ) {
clearTimeout( that.closeTimer );
},
focusout: function( event ) {
that.closeTimer = setTimeout( function() {
that._close( event );
}, 150);
},
"mouseleave .ui-menubar-item": function( event ) {
if ( that.options.autoExpand ) {
that.closeTimer = setTimeout( function() {
that._close( event );
}, 150);
}
},
"mouseenter .ui-menubar-item": function( event ) {
clearTimeout( that.closeTimer );
}
});
// Keep track of open submenus
this.openSubmenus = 0;
},
_destroy : function() {
this.menuItems
.removeClass( "ui-menubar-item" )
.removeAttr( "role" );
this.element
.removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
.removeAttr( "role" )
.unbind( ".menubar" );
this.items
.unbind( ".menubar" )
.removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
.removeAttr( "role" )
.removeAttr( "aria-haspopup" )
// TODO unwrap?
.children( "span.ui-button-text" ).each(function( i, e ) {
var item = $( this );
item.parent().html( item.html() );
})
.end()
.children( ".ui-icon" ).remove();
this.element.find( ":ui-menu" )
.menu( "destroy" )
.show()
.removeAttr( "aria-hidden" )
.removeAttr( "aria-expanded" )
.removeAttr( "tabindex" )
.unbind( ".menubar" );
},
_close: function() {
if ( !this.active || !this.active.length ) {
return;
}
this.active
.menu( "collapseAll" )
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
});
this.active
.prev()
.removeClass( "ui-state-active" )
.removeAttr( "tabIndex" );
this.active = null;
this.open = false;
this.openSubmenus = 0;
},
_open: function( event, menu ) {
// on a single-button menubar, ignore reopening the same menu
if ( this.active && this.active[0] === menu[0] ) {
return;
}
// TODO refactor, almost the same as _close above, but don't remove tabIndex
if ( this.active ) {
this.active
.menu( "collapseAll" )
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
});
this.active
.prev()
.removeClass( "ui-state-active" );
}
// set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
var button = menu.prev().addClass( "ui-state-active" ).attr( "tabIndex", -1 );
this.active = menu
.show()
.position( $.extend({
of: button
}, this.options.position ) )
.removeAttr( "aria-hidden" )
.attr( "aria-expanded", "true" )
.menu("focus", event, menu.children( ".ui-menu-item" ).first() )
// TODO need a comment here why both events are triggered
.focus()
.focusin();
this.open = true;
},
next: function( event ) {
if ( this.open && this.active.data( "menu" ).active.has( ".ui-menu" ).length ) {
// Track number of open submenus and prevent moving to next menubar item
this.openSubmenus++;
return;
}
this.openSubmenus = 0;
this._move( "next", "first", event );
},
previous: function( event ) {
if ( this.open && this.openSubmenus ) {
// Track number of open submenus and prevent moving to previous menubar item
this.openSubmenus--;
return;
}
this.openSubmenus = 0;
this._move( "prev", "last", event );
},
_move: function( direction, filter, event ) {
var next,
wrapItem;
if ( this.open ) {
next = this.active.closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).first().children( ".ui-menu" ).eq( 0 );
wrapItem = this.menuItems[ filter ]().children( ".ui-menu" ).eq( 0 );
} else {
if ( event ) {
next = $( event.target ).closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).children( ".ui-menubar-link" ).eq( 0 );
wrapItem = this.menuItems[ filter ]().children( ".ui-menubar-link" ).eq( 0 );
} else {
next = wrapItem = this.menuItems.children( "a" ).eq( 0 );
}
}
if ( next.length ) {
if ( this.open ) {
this._open( event, next );
} else {
next.removeAttr( "tabIndex")[0].focus();
}
} else {
if ( this.open ) {
this._open( event, wrapItem );
} else {
wrapItem.removeAttr( "tabIndex")[0].focus();
}
}
}
});
}( jQuery ));

View file

@ -0,0 +1,260 @@
/************************************************************
*
* tailbone.js
*
************************************************************/
/*
* Initialize the disabled filters array. This is populated from within the
* /grids/search.mako template.
*/
var filters_to_disable = [];
/*
* Disables options within the "add filter" dropdown which correspond to those
* filters already being displayed. Called from /grids/search.mako template.
*/
function disable_filter_options() {
while (filters_to_disable.length) {
var filter = filters_to_disable.shift();
var option = $('#add-filter option[value="' + filter + '"]');
option.attr('disabled', 'disabled');
}
}
/*
* Convenience function to disable a form button.
*/
function disable_button(button, label) {
if (label) {
$(button).html(label + ", please wait...");
}
$(button).attr('disabled', 'disabled');
}
/*
* Load next / previous page of results to grid. This function is called on
* the click event from the pager links, via inline script code.
*/
function grid_navigate_page(link, url) {
var wrapper = $(link).parents('div.grid-wrapper');
var grid = wrapper.find('div.grid');
wrapper.mask("Loading...");
$.get(url, function(data) {
wrapper.unmask();
grid.replaceWith(data);
});
}
/*
* Fetch the UUID value associated with a table row.
*/
function get_uuid(obj) {
obj = $(obj);
if (obj.attr('uuid')) {
return obj.attr('uuid');
}
var tr = obj.parents('tr:first');
if (tr.attr('uuid')) {
return tr.attr('uuid');
}
return undefined;
}
/*
* get_dialog(id, callback)
*
* Returns a <DIV> element suitable for use as a jQuery dialog.
*
* ``id`` is used to construct a proper ID for the element and allows the
* dialog to be resused if possible.
*
* ``callback``, if specified, should be a callback function for the dialog.
* This function will be called whenever the dialog has been closed
* "successfully" (i.e. data submitted) by the user, and should accept a single
* ``data`` object which is the JSON response returned by the server.
*/
function get_dialog(id, callback) {
var dialog = $('#'+id+'-dialog');
if (! dialog.length) {
dialog = $('<div class="dialog" id="'+id+'-dialog"></div>');
}
if (callback) {
dialog.attr('callback', callback);
}
return dialog;
}
$(function() {
/*
* Initialize the menu bar.
*/
$('ul.menubar').menubar({
buttons: true,
menuIcon: true,
autoExpand: true
});
/*
* When filter labels are clicked, (un)check the associated checkbox.
*/
$('div.grid-wrapper div.filter label').on('click', function() {
var checkbox = $(this).prev('input[type="checkbox"]');
if (checkbox.prop('checked')) {
checkbox.prop('checked', false);
return false;
}
checkbox.prop('checked', true);
});
/*
* When a new filter is selected in the "add filter" dropdown, show it in
* the UI. This selects the filter's checkbox and puts focus to its input
* element. If all available filters have been displayed, the "add filter"
* dropdown will be hidden.
*/
$('#add-filter').on('change', function() {
var select = $(this);
var filters = select.parents('div.filters:first');
var filter = filters.find('#filter-' + select.val());
var checkbox = filter.find('input[type="checkbox"]:first');
var input = filter.find(':last-child');
checkbox.prop('checked', true);
filter.show();
input.select();
input.focus();
filters.find('input[type="submit"]').show();
filters.find('button[type="reset"]').show();
select.find('option:selected').attr('disabled', true);
select.val('add a filter');
if (select.find('option:enabled').length == 1) {
select.hide();
}
});
/*
* When user clicks the grid filters search button, perform the search in
* the background and reload the grid in-place.
*/
$('div.filters form').submit(function() {
var form = $(this);
var wrapper = form.parents('div.grid-wrapper');
var grid = wrapper.find('div.grid');
var data = form.serializeArray();
data.push({name: 'partial', value: true});
wrapper.mask("Loading...");
$.get(grid.attr('url'), data, function(data) {
wrapper.unmask();
grid.replaceWith(data);
});
return false;
});
/*
* When user clicks the grid filters reset button, manually clear all
* filter input elements, and submit a new search.
*/
$('div.filters form button[type="reset"]').click(function() {
var form = $(this).parents('form');
form.find('div.filter').each(function() {
$(this).find('div.value input').val('');
});
form.submit();
return false;
});
$('div.grid-wrapper').on('click', 'div.grid th.sortable a', function() {
var th = $(this).parent();
var wrapper = th.parents('div.grid-wrapper');
var grid = wrapper.find('div.grid');
var data = {
sort: th.attr('field'),
dir: (th.hasClass('sorted') && th.hasClass('asc')) ? 'desc' : 'asc',
page: 1,
partial: true
};
wrapper.mask("Loading...");
$.get(grid.attr('url'), data, function(data) {
wrapper.unmask();
grid.replaceWith(data);
});
return false;
});
$('#body').on('mouseenter', 'div.grid.hoverable table tbody tr', function() {
$(this).addClass('hovering');
});
$('#body').on('mouseleave', 'div.grid.hoverable table tbody tr', function() {
$(this).removeClass('hovering');
});
$('div.grid-wrapper').on('click', 'div.grid table tbody td.view', function() {
var url = $(this).attr('url');
if (url) {
location.href = url;
}
});
$('div.grid-wrapper').on('click', 'div.grid table tbody td.edit', function() {
var url = $(this).attr('url');
if (url) {
location.href = url;
}
});
$('div.grid-wrapper').on('click', 'div.grid table tbody td.delete', function() {
var url = $(this).attr('url');
if (url) {
if (confirm("Do you really wish to delete this object?")) {
location.href = url;
}
}
});
$('div.grid-wrapper').on('change', 'div.grid div.pager select#grid-page-count', function() {
var select = $(this);
var wrapper = select.parents('div.grid-wrapper');
var grid = wrapper.find('div.grid');
var data = {
per_page: select.val(),
partial: true
};
wrapper.mask("Loading...");
$.get(grid.attr('url'), data, function(data) {
wrapper.unmask();
grid.replaceWith(data);
});
});
/*
* Add "check all" functionality to tables with checkboxes.
*/
$('body').on('click', 'div.grid table thead th.checkbox input[type="checkbox"]', function() {
var table = $(this).parents('table:first');
var checked = $(this).prop('checked');
table.find('tbody tr').each(function() {
$(this).find('td.checkbox input[type="checkbox"]').prop('checked', checked);
});
});
$('body').on('click', 'div.dialog button.close', function() {
var dialog = $(this).parents('div.dialog:first');
dialog.dialog('close');
});
});