diff options
author | luxagraf <sng@luxagraf.net> | 2010-10-23 19:46:20 -0400 |
---|---|---|
committer | luxagraf <sng@luxagraf.net> | 2010-10-23 19:46:20 -0400 |
commit | c59a2a69fb38b92b6c45bcf7431d2b1a3c5dce3c (patch) | |
tree | 2fc44ca867839d5e591e21467b6e4526f7a9f080 /lib/grappelli/media/js | |
parent | ed77da873e675f02f12cbab9be27f342f825444b (diff) |
added grappelli, filebrowser, chunks and tagging to lcal repo
Diffstat (limited to 'lib/grappelli/media/js')
31 files changed, 3348 insertions, 0 deletions
diff --git a/lib/grappelli/media/js/GRAPPELLI_INFO.TXT b/lib/grappelli/media/js/GRAPPELLI_INFO.TXT new file mode 100644 index 0000000..c26d607 --- /dev/null +++ b/lib/grappelli/media/js/GRAPPELLI_INFO.TXT @@ -0,0 +1,57 @@ +JavaScript infos about grappelli: + +grappelli adds much additional features to the django.contrib.admin. to accomplish this the js loaded in grappelli is pretty different to django admin's. + +### +loading order of grappelli: +### + + * jquery-1.4.2.min.js + * jquery-ui-1.8.custom.min.js (with datepicker) + * grappelli.init.js + * grappelli.timepicker.js + * grappelli.RelatedObjectLookups.js + * grappelli.js + * (optional) grappelli.change_list.js + + +### +django.admin js removed: +### + +grappelli has its own adminmedia folder. some of django's js files are missing (intentionally) + +actions.js, actions.min.js +jquery.js, jquery.min.js +collapse.js, collapse.min.js +calendar.js +jquery.init.js + +### +django.admin js updated: +### + +datetimeshortcuts.js + - outcommented initialization of js for date fields (use jquery-ui datepicker @see grappelli.js) +RelatedObjectLookups.js + Popups: + - trigger focus/click event of element to open cancel/save footer in changelist + - open popups with 500x920 instead of 500x800 + +### +js added by grappelli: +### + + grappelli.change_list.js + - all js magic for change_list + + grappelli.RelatedObjectLookups.js + - customization and initialization + + grappelli.js + - grappelli wide features like collapsables + - hacks like editing dom which is rendered from python code (not customizable via templates) + + media/jquery/* + - "own" jquery (use newer version than original) + - jquery-ui stuff like datepicker
\ No newline at end of file diff --git a/lib/grappelli/media/js/LICENSE-JQUERY.txt b/lib/grappelli/media/js/LICENSE-JQUERY.txt new file mode 100644 index 0000000..a4c5bd7 --- /dev/null +++ b/lib/grappelli/media/js/LICENSE-JQUERY.txt @@ -0,0 +1,20 @@ +Copyright (c) 2010 John Resig, http://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/lib/grappelli/media/js/SelectBox.js b/lib/grappelli/media/js/SelectBox.js new file mode 100644 index 0000000..f28c861 --- /dev/null +++ b/lib/grappelli/media/js/SelectBox.js @@ -0,0 +1,111 @@ +var SelectBox = { + cache: new Object(), + init: function(id) { + var box = document.getElementById(id); + var node; + SelectBox.cache[id] = new Array(); + var cache = SelectBox.cache[id]; + for (var i = 0; (node = box.options[i]); i++) { + cache.push({value: node.value, text: node.text, displayed: 1}); + } + }, + redisplay: function(id) { + // Repopulate HTML select box from cache + var box = document.getElementById(id); + box.options.length = 0; // clear all options + for (var i = 0, j = SelectBox.cache[id].length; i < j; i++) { + var node = SelectBox.cache[id][i]; + if (node.displayed) { + box.options[box.options.length] = new Option(node.text, node.value, false, false); + } + } + }, + filter: function(id, text) { + // Redisplay the HTML select box, displaying only the choices containing ALL + // the words in text. (It's an AND search.) + var tokens = text.toLowerCase().split(/\s+/); + var node, token; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + node.displayed = 1; + for (var j = 0; (token = tokens[j]); j++) { + if (node.text.toLowerCase().indexOf(token) == -1) { + node.displayed = 0; + } + } + } + SelectBox.redisplay(id); + }, + delete_from_cache: function(id, value) { + var node, delete_index = null; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + if (node.value == value) { + delete_index = i; + break; + } + } + var j = SelectBox.cache[id].length - 1; + for (var i = delete_index; i < j; i++) { + SelectBox.cache[id][i] = SelectBox.cache[id][i+1]; + } + SelectBox.cache[id].length--; + }, + add_to_cache: function(id, option) { + SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); + }, + cache_contains: function(id, value) { + // Check if an item is contained in the cache + var node; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + if (node.value == value) { + return true; + } + } + return false; + }, + move: function(from, to) { + var from_box = document.getElementById(from); + var to_box = document.getElementById(to); + var option; + for (var i = 0; (option = from_box.options[i]); i++) { + if (option.selected && SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + move_all: function(from, to) { + var from_box = document.getElementById(from); + var to_box = document.getElementById(to); + var option; + for (var i = 0; (option = from_box.options[i]); i++) { + if (SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + sort: function(id) { + SelectBox.cache[id].sort( function(a, b) { + a = a.text.toLowerCase(); + b = b.text.toLowerCase(); + try { + if (a > b) return 1; + if (a < b) return -1; + } + catch (e) { + // silently fail on IE 'unknown' exception + } + return 0; + } ); + }, + select_all: function(id) { + var box = document.getElementById(id); + for (var i = 0; i < box.options.length; i++) { + box.options[i].selected = 'selected'; + } + } +} diff --git a/lib/grappelli/media/js/SelectFilter2.js b/lib/grappelli/media/js/SelectFilter2.js new file mode 100644 index 0000000..9b50cb9 --- /dev/null +++ b/lib/grappelli/media/js/SelectFilter2.js @@ -0,0 +1,117 @@ +/* +SelectFilter2 - Turns a multiple-select box into a filter interface. + +Different than SelectFilter because this is coupled to the admin framework. + +Requires core.js, SelectBox.js and addevent.js. +*/ + +function findForm(node) { + // returns the node of the form containing the given node + if (node.tagName.toLowerCase() != 'form') { + return findForm(node.parentNode); + } + return node; +} + +var SelectFilter = { + init: function(field_id, field_name, is_stacked, admin_media_prefix) { + if (field_id.match(/__prefix__/)){ + // Don't intialize on empty forms. + return; + } + var from_box = document.getElementById(field_id); + from_box.id += '_from'; // change its ID + from_box.className = 'filtered'; + + // Remove <p class="info">, because it just gets in the way. + var ps = from_box.parentNode.getElementsByTagName('p'); + for (var i=0; i<ps.length; i++) { + from_box.parentNode.removeChild(ps[i]); + } + + // <div class="selector"> or <div class="selector stacked"> + var selector_div = quickElement('div', from_box.parentNode); + selector_div.className = is_stacked ? 'selector stacked' : 'selector'; + + // <div class="selector-available"> + var selector_available = quickElement('div', selector_div, ''); + selector_available.className = 'selector-available'; + quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name])); + var filter_p = quickElement('p', selector_available, ''); + filter_p.className = 'selector-filter'; + quickElement('img', filter_p, '', 'src', admin_media_prefix + 'img/admin/selector-search.gif'); + filter_p.appendChild(document.createTextNode(' ')); + var filter_input = quickElement('input', filter_p, '', 'type', 'text'); + filter_input.id = field_id + '_input'; + selector_available.appendChild(from_box); + var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()'); + choose_all.className = 'selector-chooseall'; + + // <ul class="selector-chooser"> + var selector_chooser = quickElement('ul', selector_div, ''); + selector_chooser.className = 'selector-chooser'; + var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()'); + add_link.className = 'selector-add'; + var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()'); + remove_link.className = 'selector-remove'; + + // <div class="selector-chosen"> + var selector_chosen = quickElement('div', selector_div, ''); + selector_chosen.className = 'selector-chosen'; + quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name])); + var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click ')); + selector_filter.className = 'selector-filter'; + quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? 'img/admin/selector_stacked-add.gif':'img/admin/selector-add.gif'), 'alt', 'Add'); + var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name')); + to_box.className = 'filtered'; + var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()'); + clear_all.className = 'selector-clearall'; + + from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); + + // Set up the JavaScript event handlers for the select box filter interface + addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); + addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); + addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); }); + addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); }); + addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); + SelectBox.init(field_id + '_from'); + SelectBox.init(field_id + '_to'); + // Move selected from_box options to to_box + SelectBox.move(field_id + '_from', field_id + '_to'); + }, + filter_key_up: function(event, field_id) { + from = document.getElementById(field_id + '_from'); + // don't submit form if user pressed Enter + if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { + from.selectedIndex = 0; + SelectBox.move(field_id + '_from', field_id + '_to'); + from.selectedIndex = 0; + return false; + } + var temp = from.selectedIndex; + SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value); + from.selectedIndex = temp; + return true; + }, + filter_key_down: function(event, field_id) { + from = document.getElementById(field_id + '_from'); + // right arrow -- move across + if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) { + var old_index = from.selectedIndex; + SelectBox.move(field_id + '_from', field_id + '_to'); + from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index; + return false; + } + // down arrow -- wrap around + if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) { + from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1; + } + // up arrow -- wrap around + if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) { + from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1; + } + return true; + } +} diff --git a/lib/grappelli/media/js/actions.js b/lib/grappelli/media/js/actions.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/actions.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/actions.min.js b/lib/grappelli/media/js/actions.min.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/actions.min.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/admin/DateTimeShortcuts.js b/lib/grappelli/media/js/admin/DateTimeShortcuts.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/admin/DateTimeShortcuts.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/admin/RelatedObjectLookups.js b/lib/grappelli/media/js/admin/RelatedObjectLookups.js new file mode 100644 index 0000000..6ab56dc --- /dev/null +++ b/lib/grappelli/media/js/admin/RelatedObjectLookups.js @@ -0,0 +1,3 @@ +// dropped +// use grappelli.RelatedObjectLookups.js instead +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/admin/ordering.js b/lib/grappelli/media/js/admin/ordering.js new file mode 100644 index 0000000..53c42f3 --- /dev/null +++ b/lib/grappelli/media/js/admin/ordering.js @@ -0,0 +1,137 @@ +addEvent(window, 'load', reorder_init); + +var lis; +var top = 0; +var left = 0; +var height = 30; + +function reorder_init() { + lis = document.getElementsBySelector('ul#orderthese li'); + var input = document.getElementsBySelector('input[name=order_]')[0]; + setOrder(input.value.split(',')); + input.disabled = true; + draw(); + // Now initialise the dragging behaviour + var limit = (lis.length - 1) * height; + for (var i = 0; i < lis.length; i++) { + var li = lis[i]; + var img = document.getElementById('handle'+li.id); + li.style.zIndex = 1; + Drag.init(img, li, left + 10, left + 10, top + 10, top + 10 + limit); + li.onDragStart = startDrag; + li.onDragEnd = endDrag; + img.style.cursor = 'move'; + } +} + +function submitOrderForm() { + var inputOrder = document.getElementsBySelector('input[name=order_]')[0]; + inputOrder.value = getOrder(); + inputOrder.disabled=false; +} + +function startDrag() { + this.style.zIndex = '10'; + this.className = 'dragging'; +} + +function endDrag(x, y) { + this.style.zIndex = '1'; + this.className = ''; + // Work out how far along it has been dropped, using x co-ordinate + var oldIndex = this.index; + var newIndex = Math.round((y - 10 - top) / height); + // 'Snap' to the correct position + this.style.top = (10 + top + newIndex * height) + 'px'; + this.index = newIndex; + moveItem(oldIndex, newIndex); +} + +function moveItem(oldIndex, newIndex) { + // Swaps two items, adjusts the index and left co-ord for all others + if (oldIndex == newIndex) { + return; // Nothing to swap; + } + var direction, lo, hi; + if (newIndex > oldIndex) { + lo = oldIndex; + hi = newIndex; + direction = -1; + } else { + direction = 1; + hi = oldIndex; + lo = newIndex; + } + var lis2 = new Array(); // We will build the new order in this array + for (var i = 0; i < lis.length; i++) { + if (i < lo || i > hi) { + // Position of items not between the indexes is unaffected + lis2[i] = lis[i]; + continue; + } else if (i == newIndex) { + lis2[i] = lis[oldIndex]; + continue; + } else { + // Item is between the two indexes - move it along 1 + lis2[i] = lis[i - direction]; + } + } + // Re-index everything + reIndex(lis2); + lis = lis2; + draw(); +// document.getElementById('hiddenOrder').value = getOrder(); + document.getElementsBySelector('input[name=order_]')[0].value = getOrder(); +} + +function reIndex(lis) { + for (var i = 0; i < lis.length; i++) { + lis[i].index = i; + } +} + +function draw() { + for (var i = 0; i < lis.length; i++) { + var li = lis[i]; + li.index = i; + li.style.position = 'absolute'; + li.style.left = (10 + left) + 'px'; + li.style.top = (10 + top + (i * height)) + 'px'; + } +} + +function getOrder() { + var order = new Array(lis.length); + for (var i = 0; i < lis.length; i++) { + order[i] = lis[i].id.substring(1, 100); + } + return order.join(','); +} + +function setOrder(id_list) { + /* Set the current order to match the lsit of IDs */ + var temp_lis = new Array(); + for (var i = 0; i < id_list.length; i++) { + var id = 'p' + id_list[i]; + temp_lis[temp_lis.length] = document.getElementById(id); + } + reIndex(temp_lis); + lis = temp_lis; + draw(); +} + +function addEvent(elm, evType, fn, useCapture) +// addEvent and removeEvent +// cross-browser event handling for IE5+, NS6 and Mozilla +// By Scott Andrew +{ + if (elm.addEventListener){ + elm.addEventListener(evType, fn, useCapture); + return true; + } else if (elm.attachEvent){ + var r = elm.attachEvent("on"+evType, fn); + return r; + } else { + elm['on'+evType] = fn; + } +} diff --git a/lib/grappelli/media/js/calendar.js b/lib/grappelli/media/js/calendar.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/calendar.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/collapse.js b/lib/grappelli/media/js/collapse.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/collapse.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/collapse.min.js b/lib/grappelli/media/js/collapse.min.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/collapse.min.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/compress.py b/lib/grappelli/media/js/compress.py new file mode 100644 index 0000000..8d2caa2 --- /dev/null +++ b/lib/grappelli/media/js/compress.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +import os +import optparse +import subprocess +import sys + +here = os.path.dirname(__file__) + +def main(): + usage = "usage: %prog [file1..fileN]" + description = """With no file paths given this script will automatically +compress all jQuery-based files of the admin app. Requires the Google Closure +Compiler library and Java version 6 or later.""" + parser = optparse.OptionParser(usage, description=description) + parser.add_option("-c", dest="compiler", default="~/bin/compiler.jar", + help="path to Closure Compiler jar file") + parser.add_option("-v", "--verbose", + action="store_true", dest="verbose") + parser.add_option("-q", "--quiet", + action="store_false", dest="verbose") + (options, args) = parser.parse_args() + + compiler = os.path.expanduser(options.compiler) + if not os.path.exists(compiler): + sys.exit("Google Closure compiler jar file %s not found. Please use the -c option to specify the path." % compiler) + + if not args: + if options.verbose: + sys.stdout.write("No filenames given; defaulting to admin scripts\n") + args = [os.path.join(here, f) for f in [ + "actions.js", "collapse.js", "inlines.js", "prepopulate.js"]] + + for arg in args: + if not arg.endswith(".js"): + arg = arg + ".js" + to_compress = os.path.expanduser(arg) + if os.path.exists(to_compress): + to_compress_min = "%s.min.js" % "".join(arg.rsplit(".js")) + cmd = "java -jar %s --js %s --js_output_file %s" % (compiler, to_compress, to_compress_min) + if options.verbose: + sys.stdout.write("Running: %s\n" % cmd) + subprocess.call(cmd.split()) + else: + sys.stdout.write("File %s not found. Sure it exists?\n" % to_compress) + +if __name__ == '__main__': + main() diff --git a/lib/grappelli/media/js/core.js b/lib/grappelli/media/js/core.js new file mode 100644 index 0000000..3ca8ad0 --- /dev/null +++ b/lib/grappelli/media/js/core.js @@ -0,0 +1,221 @@ +// Core javascript helper functions + +// basic browser identification & version +var isOpera = (navigator.userAgent.indexOf("Opera")>=0) && parseFloat(navigator.appVersion); +var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]); + +// Cross-browser event handlers. +function addEvent(obj, evType, fn) { + if (obj.addEventListener) { + obj.addEventListener(evType, fn, false); + return true; + } else if (obj.attachEvent) { + var r = obj.attachEvent("on" + evType, fn); + return r; + } else { + return false; + } +} + +function removeEvent(obj, evType, fn) { + if (obj.removeEventListener) { + obj.removeEventListener(evType, fn, false); + return true; + } else if (obj.detachEvent) { + obj.detachEvent("on" + evType, fn); + return true; + } else { + return false; + } +} + +// quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]); +function quickElement() { + var obj = document.createElement(arguments[0]); + if (arguments[2] != '' && arguments[2] != null) { + var textNode = document.createTextNode(arguments[2]); + obj.appendChild(textNode); + } + var len = arguments.length; + for (var i = 3; i < len; i += 2) { + obj.setAttribute(arguments[i], arguments[i+1]); + } + arguments[1].appendChild(obj); + return obj; +} + +// ---------------------------------------------------------------------------- +// Cross-browser xmlhttp object +// from http://jibbering.com/2002/4/httprequest.html +// ---------------------------------------------------------------------------- +var xmlhttp; +/*@cc_on @*/ +/*@if (@_jscript_version >= 5) + try { + xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + try { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (E) { + xmlhttp = false; + } + } +@else + xmlhttp = false; +@end @*/ +if (!xmlhttp && typeof XMLHttpRequest != 'undefined') { + xmlhttp = new XMLHttpRequest(); +} + +// ---------------------------------------------------------------------------- +// Find-position functions by PPK +// See http://www.quirksmode.org/js/findpos.html +// ---------------------------------------------------------------------------- +function findPosX(obj) { + var curleft = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft); + obj = obj.offsetParent; + } + // IE offsetParent does not include the top-level + if (isIE && obj.parentElement){ + curleft += obj.offsetLeft - obj.scrollLeft; + } + } else if (obj.x) { + curleft += obj.x; + } + return curleft; +} + +function findPosY(obj) { + var curtop = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop); + obj = obj.offsetParent; + } + // IE offsetParent does not include the top-level + if (isIE && obj.parentElement){ + curtop += obj.offsetTop - obj.scrollTop; + } + } else if (obj.y) { + curtop += obj.y; + } + return curtop; +} + +//----------------------------------------------------------------------------- +// Date object extensions +// ---------------------------------------------------------------------------- +Date.prototype.getCorrectYear = function() { + // Date.getYear() is unreliable -- + // see http://www.quirksmode.org/js/introdate.html#year + var y = this.getYear() % 100; + return (y < 38) ? y + 2000 : y + 1900; +} + +Date.prototype.getTwelveHours = function() { + hours = this.getHours(); + if (hours == 0) { + return 12; + } + else { + return hours <= 12 ? hours : hours-12 + } +} + +Date.prototype.getTwoDigitMonth = function() { + return (this.getMonth() < 9) ? '0' + (this.getMonth()+1) : (this.getMonth()+1); +} + +Date.prototype.getTwoDigitDate = function() { + return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); +} + +Date.prototype.getTwoDigitTwelveHour = function() { + return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); +} + +Date.prototype.getTwoDigitHour = function() { + return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); +} + +Date.prototype.getTwoDigitMinute = function() { + return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); +} + +Date.prototype.getTwoDigitSecond = function() { + return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); +} + +Date.prototype.getISODate = function() { + return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate(); +} + +Date.prototype.getHourMinute = function() { + return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); +} + +Date.prototype.getHourMinuteSecond = function() { + return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond(); +} + +Date.prototype.strftime = function(format) { + var fields = { + c: this.toString(), + d: this.getTwoDigitDate(), + H: this.getTwoDigitHour(), + I: this.getTwoDigitTwelveHour(), + m: this.getTwoDigitMonth(), + M: this.getTwoDigitMinute(), + p: (this.getHours() >= 12) ? 'PM' : 'AM', + S: this.getTwoDigitSecond(), + w: '0' + this.getDay(), + x: this.toLocaleDateString(), + X: this.toLocaleTimeString(), + y: ('' + this.getFullYear()).substr(2, 4), + Y: '' + this.getFullYear(), + '%' : '%' + }; + var result = '', i = 0; + while (i < format.length) { + if (format.charAt(i) === '%') { + result = result + fields[format.charAt(i + 1)]; + ++i; + } + else { + result = result + format.charAt(i); + } + ++i; + } + return result; +} + +// ---------------------------------------------------------------------------- +// String object extensions +// ---------------------------------------------------------------------------- +String.prototype.pad_left = function(pad_length, pad_string) { + var new_string = this; + for (var i = 0; new_string.length < pad_length; i++) { + new_string = pad_string + new_string; + } + return new_string; +} + +// ---------------------------------------------------------------------------- +// Get the computed style for and element +// ---------------------------------------------------------------------------- +function getStyle(oElm, strCssRule){ + var strValue = ""; + if(document.defaultView && document.defaultView.getComputedStyle){ + strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule); + } + else if(oElm.currentStyle){ + strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){ + return p1.toUpperCase(); + }); + strValue = oElm.currentStyle[strCssRule]; + } + return strValue; +} diff --git a/lib/grappelli/media/js/dateparse.js b/lib/grappelli/media/js/dateparse.js new file mode 100644 index 0000000..e1c870e --- /dev/null +++ b/lib/grappelli/media/js/dateparse.js @@ -0,0 +1,233 @@ +/* 'Magic' date parsing, by Simon Willison (6th October 2003) + http://simon.incutio.com/archive/2003/10/06/betterDateInput + Adapted for 6newslawrence.com, 28th January 2004 +*/ + +/* Finds the index of the first occurence of item in the array, or -1 if not found */ +if (typeof Array.prototype.indexOf == 'undefined') { + Array.prototype.indexOf = function(item) { + var len = this.length; + for (var i = 0; i < len; i++) { + if (this[i] == item) { + return i; + } + } + return -1; + }; +} +/* Returns an array of items judged 'true' by the passed in test function */ +if (typeof Array.prototype.filter == 'undefined') { + Array.prototype.filter = function(test) { + var matches = []; + var len = this.length; + for (var i = 0; i < len; i++) { + if (test(this[i])) { + matches[matches.length] = this[i]; + } + } + return matches; + }; +} + +var monthNames = gettext("January February March April May June July August September October November December").split(" "); +var weekdayNames = gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday").split(" "); + +/* Takes a string, returns the index of the month matching that string, throws + an error if 0 or more than 1 matches +*/ +function parseMonth(month) { + var matches = monthNames.filter(function(item) { + return new RegExp("^" + month, "i").test(item); + }); + if (matches.length == 0) { + throw new Error("Invalid month string"); + } + if (matches.length > 1) { + throw new Error("Ambiguous month"); + } + return monthNames.indexOf(matches[0]); +} +/* Same as parseMonth but for days of the week */ +function parseWeekday(weekday) { + var matches = weekdayNames.filter(function(item) { + return new RegExp("^" + weekday, "i").test(item); + }); + if (matches.length == 0) { + throw new Error("Invalid day string"); + } + if (matches.length > 1) { + throw new Error("Ambiguous weekday"); + } + return weekdayNames.indexOf(matches[0]); +} + +/* Array of objects, each has 're', a regular expression and 'handler', a + function for creating a date from something that matches the regular + expression. Handlers may throw errors if string is unparseable. +*/ +var dateParsePatterns = [ + // Today + { re: /^tod/i, + handler: function() { + return new Date(); + } + }, + // Tomorrow + { re: /^tom/i, + handler: function() { + var d = new Date(); + d.setDate(d.getDate() + 1); + return d; + } + }, + // Yesterday + { re: /^yes/i, + handler: function() { + var d = new Date(); + d.setDate(d.getDate() - 1); + return d; + } + }, + // 4th + { re: /^(\d{1,2})(st|nd|rd|th)?$/i, + handler: function(bits) { + var d = new Date(); + d.setDate(parseInt(bits[1], 10)); + return d; + } + }, + // 4th Jan + { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, + handler: function(bits) { + var d = new Date(); + d.setDate(parseInt(bits[1], 10)); + d.setMonth(parseMonth(bits[2])); + return d; + } + }, + // 4th Jan 2003 + { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i, + handler: function(bits) { + var d = new Date(); + d.setDate(parseInt(bits[1], 10)); + d.setMonth(parseMonth(bits[2])); + d.setYear(bits[3]); + return d; + } + }, + // Jan 4th + { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i, + handler: function(bits) { + var d = new Date(); + d.setDate(parseInt(bits[2], 10)); + d.setMonth(parseMonth(bits[1])); + return d; + } + }, + // Jan 4th 2003 + { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i, + handler: function(bits) { + var d = new Date(); + d.setDate(parseInt(bits[2], 10)); + d.setMonth(parseMonth(bits[1])); + d.setYear(bits[3]); + return d; + } + }, + // next Tuesday - this is suspect due to weird meaning of "next" + { re: /^next (\w+)$/i, + handler: function(bits) { + var d = new Date(); + var day = d.getDay(); + var newDay = parseWeekday(bits[1]); + var addDays = newDay - day; + if (newDay <= day) { + addDays += 7; + } + d.setDate(d.getDate() + addDays); + return d; + } + }, + // last Tuesday + { re: /^last (\w+)$/i, + handler: function(bits) { + throw new Error("Not yet implemented"); + } + }, + // mm/dd/yyyy (American style) + { re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/, + handler: function(bits) { + var d = new Date(); + d.setYear(bits[3]); + d.setDate(parseInt(bits[2], 10)); + d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0 + return d; + } + }, + // yyyy-mm-dd (ISO style) + { re: /(\d{4})-(\d{1,2})-(\d{1,2})/, + handler: function(bits) { + var d = new Date(); + d.setYear(parseInt(bits[1])); + d.setMonth(parseInt(bits[2], 10) - 1); + d.setDate(parseInt(bits[3], 10)); + return d; + } + }, +]; + +function parseDateString(s) { + for (var i = 0; i < dateParsePatterns.length; i++) { + var re = dateParsePatterns[i].re; + var handler = dateParsePatterns[i].handler; + var bits = re.exec(s); + if (bits) { + return handler(bits); + } + } + throw new Error("Invalid date string"); +} + +function fmt00(x) { + // fmt00: Tags leading zero onto numbers 0 - 9. + // Particularly useful for displaying results from Date methods. + // + if (Math.abs(parseInt(x)) < 10){ + x = "0"+ Math.abs(x); + } + return x; +} + +function parseDateStringISO(s) { + try { + var d = parseDateString(s); + return d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' + fmt00(d.getDate()) + } + catch (e) { return s; } +} +function magicDate(input) { + var messagespan = input.id + 'Msg'; + try { + var d = parseDateString(input.value); + input.value = d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' + + fmt00(d.getDate()); + input.className = ''; + // Human readable date + if (document.getElementById(messagespan)) { + document.getElementById(messagespan).firstChild.nodeValue = d.toDateString(); + document.getElementById(messagespan).className = 'normal'; + } + } + catch (e) { + input.className = 'error'; + var message = e.message; + // Fix for IE6 bug + if (message.indexOf('is null or not an object') > -1) { + message = 'Invalid date string'; + } + if (document.getElementById(messagespan)) { + document.getElementById(messagespan).firstChild.nodeValue = message; + document.getElementById(messagespan).className = 'error'; + } + } +} diff --git a/lib/grappelli/media/js/documentation.js b/lib/grappelli/media/js/documentation.js new file mode 100644 index 0000000..b00c120 --- /dev/null +++ b/lib/grappelli/media/js/documentation.js @@ -0,0 +1,21 @@ +(function($) { + $(document).ready(function(){ + // Correct the position of anchors (because "#header" & "#breadcrumbs" have a "position:fixed") + $('.table-of-contents a').click(function(){ + var myReference = ".rte " + $(this).attr('href'); + // if collapsible + var myParentCollapsible = $(myReference).parent().parent(); + if ($(myParentCollapsible).hasClass('closed')){ + $(myParentCollapsible).toggleClass('open').toggleClass('closed'); + } + // anchor offset + var targetOffset = $(myReference).offset().top; + $('html,body').scrollTop(targetOffset - 60); + return(false); + }) + // Remove emtpy elements: wrkaround for problem reported in django-ticket #11817 + $('.rte h4:empty').remove(); + $('.rte p:empty').remove(); + $('.rte hr').remove(); + }); +})(django.jQuery);
\ No newline at end of file diff --git a/lib/grappelli/media/js/getElementsBySelector.js b/lib/grappelli/media/js/getElementsBySelector.js new file mode 100644 index 0000000..15b57a1 --- /dev/null +++ b/lib/grappelli/media/js/getElementsBySelector.js @@ -0,0 +1,167 @@ +/* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelect('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails +*/ + +function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); +} + +document.getElementsBySelector = function(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document.getElementsByTagName) { + return new Array(); + } + // Split selector in to tokens + var tokens = selector.split(' '); + var currentContext = new Array(document); + for (var i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');; + if (token.indexOf('#') > -1) { + // Token is an ID selector + var bits = token.split('#'); + var tagName = bits[0]; + var id = bits[1]; + var element = document.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // ID not found or tag with that ID not found, return false. + return new Array(); + } + // Set currentContext to contain just this element + currentContext = new Array(element); + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + var bits = token.split('.'); + var tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements; + if (tagName == '*') { + elements = getAllChildren(currentContext[h]); + } else { + try { + elements = currentContext[h].getElementsByTagName(tagName); + } + catch(e) { + elements = []; + } + } + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = new Array; + var currentContextIndex = 0; + for (var k = 0; k < found.length; k++) { + if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) { + currentContext[currentContextIndex++] = found[k]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) { + var tagName = RegExp.$1; + var attrName = RegExp.$2; + var attrOperator = RegExp.$3; + var attrValue = RegExp.$4; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements; + if (tagName == '*') { + elements = getAllChildren(currentContext[h]); + } else { + elements = currentContext[h].getElementsByTagName(tagName); + } + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = new Array; + var currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); }; + break; + case '^': // Match starts with value + checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); }; + break; + case '*': // Match ends with value + checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); }; + break; + default : + // Just test for existence of attribute + checkFunction = function(e) { return e.getAttribute(attrName); }; + } + currentContext = new Array; + var currentContextIndex = 0; + for (var k = 0; k < found.length; k++) { + if (checkFunction(found[k])) { + currentContext[currentContextIndex++] = found[k]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + var found = new Array; + var foundCount = 0; + for (var h = 0; h < currentContext.length; h++) { + var elements = currentContext[h].getElementsByTagName(tagName); + for (var j = 0; j < elements.length; j++) { + found[foundCount++] = elements[j]; + } + } + currentContext = found; + } + return currentContext; +} + +/* That revolting regular expression explained +/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/ + \---/ \---/\-------------/ \-------/ + | | | | + | | | The value + | | ~,|,^,$,* or = + | Attribute + Tag +*/ diff --git a/lib/grappelli/media/js/grappelli.RelatedObjectLookups.js b/lib/grappelli/media/js/grappelli.RelatedObjectLookups.js new file mode 100644 index 0000000..2857a39 --- /dev/null +++ b/lib/grappelli/media/js/grappelli.RelatedObjectLookups.js @@ -0,0 +1,341 @@ +// TODO: klemens: drop ADMIN_URL + +// Handles related-objects functionality: lookup link for raw_id_fields +// and Add Another links. + +function html_unescape(text) { + // Unescape a string that was escaped using django.utils.html.escape. + text = text.replace(/</g, '<'); + text = text.replace(/>/g, '>'); + text = text.replace(/"/g, '"'); + text = text.replace(/'/g, "'"); + text = text.replace(/&/g, '&'); + return text; +} + +// IE doesn't accept periods or dashes in the window name, but the element IDs +// we use to generate popup window names may contain them, therefore we map them +// to allowed characters in a reversible way so that we can locate the correct +// element when the popup window is dismissed. +function id_to_windowname(text) { + text = text.replace(/\./g, '__dot__'); + text = text.replace(/\-/g, '__dash__'); + return text; +} + +function windowname_to_id(text) { + text = text.replace(/__dot__/g, '.'); + text = text.replace(/__dash__/g, '-'); + return text; +} + +var CHAR_MAX_LENGTH = 30; + +// customized from RelatedObjectLoopups.js +function showRelatedObjectLookupPopup(triggeringLink) { + var name = triggeringLink.id.replace(/^lookup_/, ''); + name = id_to_windowname(name); + var href; + if (triggeringLink.href.search(/\?/) >= 0) { + href = triggeringLink.href + '&pop=1'; + } else { + href = triggeringLink.href + '?pop=1'; + } + //grappelli custom + var win = window.open(href, name, 'height=500,width=980,resizable=yes,scrollbars=yes'); + // end + win.focus(); + return false; +} + +// customized from RelatedObjectLoopups.js +function dismissRelatedLookupPopup(win, chosenId) { + var name = windowname_to_id(win.name); + var elem = document.getElementById(name); + if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { + elem.value += ',' + chosenId; + } else { + document.getElementById(name).value = chosenId; + } + // grappelli custom + elem.focus(); + // end + win.close(); +} + +// customized from RelatedObjectLoopups.js +function showAddAnotherPopup(triggeringLink) { + var name = triggeringLink.id.replace(/^add_/, ''); + name = id_to_windowname(name); + href = triggeringLink.href; + if (href.indexOf('?') == -1) { + href += '?_popup=1'; + } else { + href += '&_popup=1'; + } + var win = window.open(href, name, 'height=500,width=1000,resizable=yes,scrollbars=yes'); + win.focus(); + return false; +} + +// customized from RelatedObjectLoopups.js +function dismissAddAnotherPopup(win, newId, newRepr) { + // newId and newRepr are expected to have previously been escaped by + // django.utils.html.escape. + newId = html_unescape(newId); + newRepr = html_unescape(newRepr); + var name = windowname_to_id(win.name); + var elem = document.getElementById(name); + if (elem) { + if (elem.nodeName == 'SELECT') { + var o = new Option(newRepr, newId); + elem.options[elem.options.length] = o; + o.selected = true; + } else if (elem.nodeName == 'INPUT') { + if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { + elem.value += ',' + newId; + } else { + elem.value = newId; + } + // NOTE: via http://code.djangoproject.com/attachment/ticket/10191/RelatedObjectLookups-updated.js.patch + // check if the className contains radiolist - if it's HORIZONTAL, then it won't match if we compare explicitly + } else if (elem.className.indexOf('radiolist') > -1) { + var cnt = elem.getElementsByTagName('li').length; + var idName = elem.id+'_'+cnt; + var newLi = document.createElement('li'); + var newLabel = document.createElement('label'); + var newText = document.createTextNode(' '+newRepr); + try { + // IE doesn't support settings name, type, or class by setAttribute + var newInput = document.createElement('<input type=\'radio\' name=\''+name.slice(3)+'\' checked=\'checked\' class=\''+elem.className+'\' />'); + } catch(err) { + var newInput = document.createElement('input'); + newInput.setAttribute('class', elem.className); + newInput.setAttribute('type', 'radio'); + newInput.setAttribute('name', name.slice(3)); + } + newLabel.setAttribute('for', idName); + newInput.setAttribute('id', idName); + newInput.setAttribute('value', newId); + newInput.setAttribute('checked', 'checked'); + newLabel.appendChild(newInput); + // check if the content being added is a tag - useful for image lists + if (newRepr.charAt(0) == '<' && newRepr.charAt(newRepr.length-1) == '>') { + newLabel.innerHTML += newRepr; + } + else { + newLabel.appendChild(newText); + } + newLi.appendChild(newLabel); + elem.appendChild(newLi); + } + } else { + var toId = name + "_to"; + elem = document.getElementById(toId); + var o = new Option(newRepr, newId); + SelectBox.add_to_cache(toId, o); + SelectBox.redisplay(toId); + } + + win.close(); +} + +(function($) { + function RelatedLookup(obj) { + // check if val isn't empty string or the same value as before + if (obj.val() == obj.data('old_val')) return; + obj.data('old_val', obj.val()); + + var link = obj.next(); + var spliturl = link.attr('href').split('/'); + var app_label = spliturl[spliturl.length-3]; + var model_name= spliturl[spliturl.length-2]; + + var text = obj.next().next(); + if (obj.val() == "") { + text.text(''); + return; + } + + text.text('loading ...'); + + // get object + $.get('/grappelli/lookup/related/', { + object_id: obj.val(), + app_label: app_label, + model_name: model_name + }, function(data) { + var item = data; + text.text(''); + if (item) { + if (item.length > CHAR_MAX_LENGTH) { + text.text(decodeURI(item.substr(0, CHAR_MAX_LENGTH) + " ...")); + } else { + text.text(decodeURI(item)); + } + + } + }); + } + + function M2MLookup(obj) { + // check if val isn't empty string or the same value as before + if (obj.val() == obj.data('old_val')) return; + obj.data('old_val', obj.val()); + + var link = obj.next(); + var spliturl = link.attr('href').split('/'); + var app_label = spliturl[spliturl.length-3]; + var model_name= spliturl[spliturl.length-2]; + + var text = obj.next().next(); + if (obj.val() == "") { + text.text(''); + return; + } + + text.text('loading ...'); + + // get object + $.get('/grappelli/lookup/m2m/', { + object_id: obj.val(), + app_label: app_label, + model_name: model_name + }, function(data) { + var item = data; + text.text(''); + if (item) { + if (item.length > CHAR_MAX_LENGTH) { + text.text(decodeURI(item.substr(0, CHAR_MAX_LENGTH) + " ...")); + } else { + text.text(decodeURI(item)); + } + } + }); + } + + function GenericLookup(obj, force_update) { + // check if val isn't empty string or the same value as before + if (!force_update && obj.val() == obj.data('old_val')) return; + obj.data('old_val', obj.val()); + + var text = obj.next().next(); + if (obj.val() == "") { + text.text(""); + return; + } + text.text('loading ...'); + + var link = obj.next(); + if (link.length == 0) return; + var spliturl = link.attr('href').split('/'); + var app_label = spliturl[spliturl.length-3]; + var model_name= spliturl[spliturl.length-2]; + + // get object + $.get('/grappelli/lookup/related/', {object_id: obj.val(), app_label: app_label, model_name: model_name}, function(data) { + var item = data; + text.text(''); + if (item) { + if (item.length > CHAR_MAX_LENGTH) { + text.text(decodeURI(item.substr(0, CHAR_MAX_LENGTH) + " ...")); + } else { + text.text(decodeURI(item)); + } + } + }); + } + + function RelatedHandler(obj) { + // related lookup handler + obj.bind("change focus keyup blur", function() { + RelatedLookup($(this)); + }); + } + + function M2MHandler(obj) { + // related lookup handler + obj.bind("change focus keyup blur", function() { + M2MLookup($(this)); + }); + } + + function InitObjectID(obj) { + obj.each(function() { + var ct = $(this).closest('div[class*="object_id"]').prev().find(':input[name*="content_type"]').val(); + if (ct) { + var lookupLink = $('<a class="related-lookup"></a>'); + lookupLink.attr('id', 'lookup_'+this.id); + lookupLink.attr('href', ADMIN_URL + MODEL_URL_ARRAY[ct].app + "/" + MODEL_URL_ARRAY[ct].model + '/?t=id'); + lookupLink.attr('onClick', 'return showRelatedObjectLookupPopup(this);'); + var lookupText = '<strong> </strong>'; + $(this).after(lookupText).after(lookupLink); + if ($(this).val() != "") { + lookupText = GenericLookup($(this)); + } + } + }); + } + + function InitContentType(obj) { + obj.bind("change", function() { + var node = $(this).closest('div[class*="content_type"]').next(), + lookupLink = node.find('a.related-lookup'), + obj_id = node.find('input[name*="object_id"]'); + if ($(this).val()) { + var href = ADMIN_URL + MODEL_URL_ARRAY[$(this).val()].app + "/" + MODEL_URL_ARRAY[$(this).val()].model + '/?t=id'; + if (lookupLink.attr('href')) { + lookupLink.attr('href', href); + } else { + lookupLink = $('<a class="related-lookup"></a>'); + lookupLink.attr('id', 'lookup_' + obj_id.attr('id')); + lookupLink.attr('href', ADMIN_URL + MODEL_URL_ARRAY[$(this).val()].app + "/" + MODEL_URL_ARRAY[$(this).val()].model + '/?t=id'); + lookupLink.attr('onClick', 'return showRelatedObjectLookupPopup(this);'); + var lookupText = '<strong> </strong>'; + obj_id.after(lookupText).after(lookupLink); + } + GenericLookup(obj_id, true); + } else { + obj_id.val(''); + lookupLink.remove(); + node.find('strong').remove(); + } + }); + } + + function GenericHandler(obj) { + // related lookup handler + obj.bind("change focus keyup", function() { + GenericLookup($(this)); + }); + } + $(document).ready(function() { + // change related-lookups in order to get the right URL. + $('a.related-lookup').each(function() { + href = $(this).attr('href').replace('../../../', ADMIN_URL); + $(this).attr('href', href); + }); + + // related lookup setup + $("input.vForeignKeyRawIdAdminField").each(function() { + // insert empty text-elements after all empty foreignkeys + if ($(this).val() == "") { + $(this).next().after(' <strong></strong>'); + } + }); + + // m2m lookup setup + $("input.vManyToManyRawIdAdminField").each(function() { + // insert empty text-elements after all m2m fields + $(this).next().after(' <strong> </strong>'); + M2MLookup($(this)); + }); + + RelatedHandler($("input.vForeignKeyRawIdAdminField")); + M2MHandler($("input.vManyToManyRawIdAdminField")); + + InitObjectID($('input[name*="object_id"]')); + InitContentType($(':input[name*="content_type"]')); + GenericHandler($('input[name*="object_id"]')); + }); +})(django.jQuery); diff --git a/lib/grappelli/media/js/grappelli.change_list.js b/lib/grappelli/media/js/grappelli.change_list.js new file mode 100644 index 0000000..3a8c967 --- /dev/null +++ b/lib/grappelli/media/js/grappelli.change_list.js @@ -0,0 +1,317 @@ +/** + * manages all interactions in the admin change_list + */ +(function($) { + $.fn.change_list = function(opts) { + var options = $.extend({}, $.fn.change_list.defaults, opts), + actionCheckboxes = $(options.actionCheckboxes); + + checker = function(checked) { + if (checked) { + showQuestion(); + } else { + reset(); + } + actionCheckboxes.attr("checked", checked).parent().parent().toggleClass(options.selectedClass, checked); + }; + + updateCounter = function() { + var count = actionCheckboxes.filter(":checked").length; + + if (count > 0) { + hideSubmitFooter(); + $(options.actionContainer).show(); + } else { + $(options.actionContainer).hide(); + showSubmitFooter(); + } + + if ($(options.actionContainer).find("span._acnt").length > 0) { + $(options.actionContainer).find("span._acnt").html(count); + } else { + var actionCounter = $(options.actionContainer).find("span.action-counter"), + text = actionCounter.html(); + actionCounter.html('<span class="_acnt">' + count + '</span>' + text.substring(1)); + } + + $(options.allToggle).attr("checked", function() { + if (count == actionCheckboxes.length) { + value = true; + showQuestion(); + } else { + value = false; + clearAcross(); + } + return value; + }); + }; + + showQuestion = function() { + $(options.acrossClears).hide(); + $(options.acrossQuestions).show(); + $(options.allContainer).hide(); + }; + + showClear = function() { + $(options.acrossClears).show(); + $(options.allContainer).show(); + + $(options.acrossQuestions).hide(); + $(options.counterContainer).hide(); + + $(options.actionContainer).toggleClass(options.selectedClass); + }; + + reset = function() { + $(options.acrossClears).hide(); + $(options.acrossQuestions).hide(); + $(options.allContainer).hide(); + $(options.counterContainer).show(); + }; + + clearAcross = function() { + reset(); + $(options.acrossInput).val(0); + $(options.actionContainer).removeClass(options.selectedClass); + }; + + clearSelection = function() { + $(options.allToggle).attr("checked", false); + clearAcross(); + checker(false); + updateCounter(); + }; + + initializeFlexibleLayout = function(content) { + var SpanGrid, + PaddingRight, + MarginRight; + $('#content').addClass('content-flexible'); + $(content).find('.span-flexible').next('.column').parent().addClass('layout-flexible-grid'); + $(content).find('.column').next('.span-flexible').parent().addClass('layout-grid-flexible'); + $(content).append('<br clear="all" />'); + // Layout Flexible + Grid + if ($(content).hasClass('layout-flexible-grid')) { + SpanGrid = $(content).find('.span-flexible').next('.column').outerWidth(); + PaddingRight = SpanGrid + 20; + MarginRight = - SpanGrid - 20; + $(content).css({ + 'padding-right': PaddingRight + }); + $(content).find('.span-flexible').next('.column').css({ + 'margin-right': -10000 + }); + } + // Layout Grid + Flexible + if ($(content).hasClass('layout-grid-flexible')) { + SpanGrid = $(content).find('.span-flexible').prev('.column').outerWidth(); + PaddingLeft = SpanGrid + 20; + MarginLeft = - SpanGrid - 20; + $(content).css({ + 'padding-left': PaddingLeft + }); + $(content).find('.span-flexible').prev('.column').css({ + 'margin-left': MarginLeft + }); + } + }; + + HorizontalOverflow = function(content) { + var TableWidth = $(content).find('table').outerWidth(); + var SpanFlexibleWidth = $(content).find('.span-flexible').outerWidth(); + if (TableWidth > SpanFlexibleWidth) { + $(content).find('.span-flexible').css({ + 'min-width' : TableWidth + 1 + 'px' + }); + $(content).find('.span-flexible').next('.column').css({ + 'border-right' : '20px solid transparent' + }); + } + if (TableWidth < SpanFlexibleWidth) { + $(content).find('.span-flexible').css({ + 'min-width': 'auto' + }); + } + }; + + ModifyTableElements = function() { + // UGLY HACK: add no-wrap to table-elements + // should be there already. + $('.changelist-results a.add-another').parent().addClass('nowrap'); + }; + + initFilter = function() { + $("a.toggle-filters").click(function() { + $(".filter-pulldown").toggle(); + $("#filters").toggleClass("open"); + }); + + $(".filter_choice").change(function(){ + location.href = $(this).val(); + }); + + var filter_choice = $(".filter_choice"); + + for (var i = 0; i < filter_choice.length; i++) { + if (!$(filter_choice[i]).find(':first-child').attr('selected')) { + $("#filters").addClass('selected'); + } + } + }; + + initLayout = function() { + initializeFlexibleLayout('.container-flexible'); + + $(window).resize(function(){ + HorizontalOverflow('.container-flexible'); + }); + + //window.onload = function () { + HorizontalOverflow('.container-flexible'); + //}; + }; + + showSubmitFooter = function() { + $("div#submit").show(); + + // need to uncheck all actions checkboxes and update counter + // (actions are not working if you want to edit items in the change_list) + + //actionCheckboxes.attr("checked", false); + //clearSelection(); + }; + + hideSubmitFooter = function() { + $("div#submit").hide(); + }; + + // Show counter by default + $(options.counterContainer).show(); + + $(options.allToggle).show().click(function() { + checker($(this).attr("checked")); + updateCounter(); + }); + + $("div.changelist-actions li.question a").click(function(event) { + event.preventDefault(); + $(options.acrossInput).val(1); + showClear(); + }); + + $("div.changelist-actions li.clear-selection a").click(function(event) { + event.preventDefault(); + clearSelection(); + }); + + lastChecked = null; + actionCheckboxes.click(function(event) { + if (!event) { + event = window.event; + } + var target = event.target ? event.target : event.srcElement; + if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey == true) { + var inrange = false; + $(lastChecked).attr("checked", target.checked).parent().parent().toggleClass(options.selectedClass, target.checked); + actionCheckboxes.each(function() { + if ($.data(this) == $.data(lastChecked) || $.data(this) == $.data(target)) { + inrange = (inrange) ? false : true; + } + if (inrange) { + $(this).attr("checked", target.checked) + .parent().parent().toggleClass(options.selectedClass, target.checked); + } + }); + } + $(target).parent().parent().toggleClass(options.selectedClass, target.checked); + lastChecked = target; + updateCounter(); + }); + + $(options.actionSelect).attr("autocomplete", "off").change(function(evt){ + $(this).parents("form").submit(); + }); + + initLayout(); + + initFilter(); + /* + * django version < 1.2.3 only + * hide the last coll if its an editable list + * because this coll has just the hidden input with the id (breaks ui) + + if ($("#changelist").hasClass("editable")) { + // UGLY HACK: add th for thead when list_editables are activated. + $(".changelist-results tr").each(function() { + $(this).find("td:last").hide(); + }); + } + */ + + + /* + * deprecated code + * used to show submit footer if something changed only. + * way to complex to achieve this feature. + * now submit footer is visible if list_editable (and toggles visibility with action footer). + + $("input.action-select, input#action-toggle, a.cancel-link").click(function() { + $("div#submit").hide(); + }); + + //var edit_inlines = $("input[name!='_selected_action'][id!='action-toggle'][id!='searchbar']"); + //edit_inlines.focus(showSubmitFooter); + // safari (5) needs this because focus event doesn't work on checkboxes (anymore) + //edit_inlines.end().find(":checkbox").click(showSubmitFooter); + var input_nodes = $("input[name!='_selected_action'][id!='action-toggle'][id!='searchbar']"); + input_nodes.bind("change click", showSubmitFooter); + $("select[class!='filter_choice'][name!='action']").bind("change click", showSubmitFooter); + // FilebrowseField's button + $("a.fb_show").click(showSubmitFooter); + */ + + $("td input.vForeignKeyRawIdAdminField, td input.vFileBrowseField, td a.add-another").each(function() { + $(this).parent().addClass('nowrap'); + }); + + // Check state of checkboxes and reinit state if needed + actionCheckboxes.filter(":checked").each(function(i) { + $(this).parent().parent().toggleClass(options.selectedClass); + updateCounter(); + if ($(options.acrossInput).val() == 1) { + showClear(); + } + }); + + // if (!$("input#searchbar").val()) { + // $("input#searchbar").val(options.initialSearchVal); + // } + // $("input#searchbar").focus(function(){ + // if ($(this).val() == options.initialSearchVal) { + // $(this).val(""); + // } + // }); + // $("input#searchbar").blur(function(){ + // if (!$(this).val()) { + // $(this).val(options.initialSearchVal); + // } + // }); + + }; + + /* Setup plugin defaults */ + $.fn.change_list.defaults = { + actionCheckboxes: "input.action-select", + actionContainer: "div.changelist-actions", + counterContainer: "li.action-counter", + allContainer: "div.changelist-actions li.all", + acrossInput: "div.changelist-actions input.select-across", + acrossQuestions: "div.changelist-actions li.question", + acrossClears: "div.changelist-actions li.clear-selection", + allToggle: "#action-toggle", + selectedClass: "selected", + actionSelect: "div.changelist-actions select", + initialSearchVal: "Search" + }; +})(django.jQuery); + diff --git a/lib/grappelli/media/js/grappelli.init.js b/lib/grappelli/media/js/grappelli.init.js new file mode 100644 index 0000000..85a7973 --- /dev/null +++ b/lib/grappelli/media/js/grappelli.init.js @@ -0,0 +1,4 @@ +// Puts the included jQuery into our own namespace +var django = { + "jQuery": jQuery.noConflict(true) +}; diff --git a/lib/grappelli/media/js/grappelli.js b/lib/grappelli/media/js/grappelli.js new file mode 100644 index 0000000..f9a540a --- /dev/null +++ b/lib/grappelli/media/js/grappelli.js @@ -0,0 +1,131 @@ +var grappelli = {}; + +(function($) { + grappelli.collapseHandlerClass = "collapse-handler"; + grappelli.collapsedBlockedClass = "collapse-blocked"; + grappelli.openAllClass = "open-handler"; + grappelli.closeAllClass = "close-handler"; + grappelli.collapseClass = "collapse"; + grappelli.closedClass = "closed"; + grappelli.openClass = "open"; + + grappelli.collapseHandler = function() { + if (!$("body").hasClass(grappelli.collapsedBlockedClass)) { + $(this).parents("." + grappelli.collapseClass).first() + .toggleClass(grappelli.closedClass) + .toggleClass(grappelli.openClass); + } + return false; + }; + + grappelli.addCollapseHandlerClass = function() { + $("." + this.collapseClass).each(function() { + var node = $(this).children().first(); + if (node.is("h2") || node.is("h3") || node.is("h4")) { + node.addClass(grappelli.collapseHandlerClass) + } + }); + }; + + grappelli.registerCollapseHandler = function() { + $("." + this.collapseHandlerClass).click(this.collapseHandler); + }; + + grappelli.registerOpenAllHandler = function() { + $("." + this.openAllClass).click(this.openAllHandler); + }; + + /* + * Open all + */ + grappelli.openAllHandler = function() { + // get .group and not .collapse because it doesn't necessarily have .collapse + $(this).parents(".group") + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass) + .find("." + grappelli.collapseClass) + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass); + }; + + grappelli.registerCloseAllHandler = function() { + $("." + this.closeAllClass).click(this.closeAllHandler); + }; + + /* + * Close all + */ + grappelli.closeAllHandler = function() { + // get .group and not .collapse because it doesn't necessarily have .collapse + $(this).parents(".group") + .find("." + grappelli.collapseClass) + .removeClass(grappelli.openClass) + .addClass(grappelli.closedClass); + }; + + grappelli.initCollapsible = function() { + grappelli.addCollapseHandlerClass(); + grappelli.registerCollapseHandler(); + + grappelli.registerOpenAllHandler(); + grappelli.registerCloseAllHandler(); + + $("." + grappelli.collapseClass + " ul.errorlist").each(function() { + $(this).parents("." + grappelli.collapseClass) + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass); + }); + }; + + grappelli.getFormat = function(type) { + if (type == "date") { + var format = DATE_FORMAT.toLowerCase().replace(/%\w/g, function(str) { + str = str.replace(/%/, ''); + return str + str; + }); + } + return format; + } + + grappelli.initDateAndTimePicker = function() { + var options = { + //appendText: '(mm/dd/yyyy)', + showOn: 'button', + buttonImageOnly: false, + buttonText: '', + dateFormat: grappelli.getFormat('date'), + showAnim: '' + }; + var dateFields = $("input[class*='vDateField']:not([id*='__prefix__'])"); + dateFields.datepicker(options); + + if (typeof IS_POPUP != "undefined" && IS_POPUP) { + dateFields.datepicker('disable'); + } + $("input[class*='vTimeField']:not([id*='__prefix__'])").timepicker(); + }; + + grappelli.initHacks = function() { + $('p.datetime').each(function() { + var text = $(this).html(); + text = text.replace(/^\w*: /, ""); + text = text.replace(/<br>.*: /, "<br>"); + $(this).html(text); + }); + }; + + // Using window.load instead of document ready for better performances + // It prevents lots of glitches, like divs that moves around upon loading + // + // Use $(document).ready only if you have to deal with images since it will + // wait for the document to be fully loaded/rendered before running the function + // while window.load method will run as soon as the DOM/CSS is loaded. + + $(window).load(function() { + // we do the hacks first! + // because we manipulate dom via innerHTML => loose events + grappelli.initHacks(); + grappelli.initCollapsible(); + grappelli.initDateAndTimePicker(); + }); +})(django.jQuery); diff --git a/lib/grappelli/media/js/grappelli.min.js b/lib/grappelli/media/js/grappelli.min.js new file mode 100644 index 0000000..e762690 --- /dev/null +++ b/lib/grappelli/media/js/grappelli.min.js @@ -0,0 +1,252 @@ +var grappelli = {}; + +(function($) { + grappelli.collapseHandlerClass = "collapse-handler"; + grappelli.collapsedBlockedClass = "collapse-blocked"; + grappelli.openAllClass = "open-handler"; + grappelli.closeAllClass = "close-handler"; + grappelli.collapseClass = "collapse"; + grappelli.closedClass = "closed"; + grappelli.openClass = "open"; + + + /** + * if collapsible in menu is opened it has to close if it "looses focus" + */ + grappelli.collapseBlurHandler = function(evt) { + var that = $(this), + parents = $(evt.originalEvent.explicitOriginalTarget).parents(), + handler_parent = that.parents("ul.navigation-menu")[0] || that.parents("ul#user-tools")[0]; + for (var i = 0; i < parents.length; i++) { + if (parents[i] == handler_parent) { + var target = evt.originalEvent.explicitOriginalTarget; + if (target.nodeType == 3) { + target = $(target).parent() + } else { + target = $(target); + } + target.one('blur', grappelli.collapseBlurHandler); + return; + } + } + that.parents("." + grappelli.collapseClass).first() + .toggleClass(grappelli.closedClass) + .toggleClass(grappelli.openClass); + }; + + /** + * handles assignment of classes (open/close) when collapse handler is clicked + * NOTE: special case if collapsible is within the header menu + */ + grappelli.collapseHandler = function() { + if (!$("body").hasClass(grappelli.collapsedBlockedClass)) { + var that = $(this); + that.parents("." + grappelli.collapseClass).first() + .toggleClass(grappelli.closedClass) + .toggleClass(grappelli.openClass); + + // close again on blur if it's an menu dropdown from the header (i.e. bookmarks) + if (that.parent(".menu-item").length || that.parent(".user-options-container").length) { + menu = $(this).parent('li').children('ul'); + menumaxheight = $(window).height() - 80; + menuheight = menu.height(); + if (menumaxheight < menuheight) { + $(menu).css({ + "max-height": menumaxheight, + "overflow-y": "scroll" + }); + } + + that.one('blur', grappelli.collapseBlurHandler); + } + } + return false; + }; + + grappelli.addCollapseHandlerClass = function() { + $("." + this.collapseClass).each(function() { + var node = $(this).children().first(); + if (node.is("h2") || node.is("h3") || node.is("h4")) { + node.addClass(grappelli.collapseHandlerClass) + } + }); + }; + + /** + * add click handler to collapsibles + * NOTE: secial case for collapse handler in menu + */ + grappelli.registerCollapseHandler = function() { + $("." + this.collapseHandlerClass).click(this.collapseHandler); + + // if the item has children you generally open the cildren with the button next to it. + // open the menu anyways if it has an empty link + $('a.parent.item-collapse-handler-container').click(function(){ + var that = $(this); + + if (that.attr('href') == "#") { + that.next().click(); + } + }); + + // do this each time a submenu is opened + $('ul.navigation-menu a.item-collapse-handler').click(function(){ + // Collapse + $(this).closest('li.item-collapse').toggleClass("item-closed").toggleClass("item-open"); + // Calculate Menu Height + menu = $(this).closest('ul.navigation-menu>li>ul'); + $(menu).removeAttr("style"); + menumaxheight = $(window).height() - 80; + menuheight = menu.height(); + menuwidth = menu.outerWidth() + 15; + if (menumaxheight < menuheight) { + // $(menu).addClass(""); + $(menu).css({ + "width": menuwidth, + "height": menuheight, + "max-height": menumaxheight, + "overflow-y": "scroll", + "overflow-x": "hidden !important" + }); + } + }); + }; + + grappelli.registerOpenAllHandler = function() { + $("." + this.openAllClass).click(this.openAllHandler); + }; + + /* + * Open all + */ + grappelli.openAllHandler = function() { + // get .group and not .collapse because it doesn't necessarily have .collapse + $(this).parents(".group") + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass) + .find("." + grappelli.collapseClass) + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass); + }; + + grappelli.registerCloseAllHandler = function() { + $("." + this.closeAllClass).click(this.closeAllHandler); + }; + + /* + * Close all + */ + grappelli.closeAllHandler = function() { + // get .group and not .collapse because it doesn't necessarily have .collapse + $(this).parents(".group") + .find("." + grappelli.collapseClass) + .removeClass(grappelli.openClass) + .addClass(grappelli.closedClass); + }; + + grappelli.initCollapsible = function() { + grappelli.addCollapseHandlerClass(); + grappelli.registerCollapseHandler(); + + grappelli.registerOpenAllHandler(); + grappelli.registerCloseAllHandler(); + + $("." + grappelli.collapseClass + " ul.errorlist").each(function() { + $(this).parents("." + grappelli.collapseClass) + .removeClass(grappelli.closedClass) + .addClass(grappelli.openClass); + }); + }; + + grappelli.getFormat = function(type) { + if (type == "date") { + var format = DATE_FORMAT.toLowerCase().replace(/%\w/g, function(str) { + str = str.replace(/%/, ''); + return str + str; + }); + } + return format; + } + + grappelli.initDateAndTimePicker = function() { + var options = { + //appendText: '(mm/dd/yyyy)', + showOn: 'button', + buttonImageOnly: false, + buttonText: '', + dateFormat: grappelli.getFormat('date'), + showButtonPanel: true, + showAnim: '', + // HACK: sets the current instance to a global var. + // needed to actually select today if the today-button is clicked. + // see onClick handler for ".ui-datepicker-current" + beforeShow: function(year, month, inst) { + grappelli.datepicker_instance = this; + } + }; + var dateFields = $("input[class*='vDateField']:not([id*='__prefix__'])"); + dateFields.datepicker(options); + + if (typeof IS_POPUP != "undefined" && IS_POPUP) { + dateFields.datepicker('disable'); + } + + // HACK: adds an event listener to the today button of datepicker + // if clicked today gets selected and datepicker hides. + // user live() because there is no hoock after datepicker generates it's complete dom. + $(".ui-datepicker-current").live('click', function() { + $.datepicker._selectDate(grappelli.datepicker_instance); + grappelli.datepicker_instance = null; + }) + + // inti timepicker + $("input[class*='vTimeField']:not([id*='__prefix__'])").timepicker(); + }; + + grappelli.initHacks = function() { + // to get rid of text after DateField (hardcoded in django.admin) + $('p.datetime').each(function() { + var text = $(this).html(); + text = text.replace(/^\w*: /, ""); + text = text.replace(/<br>.*: /, "<br>"); + $(this).html(text); + }); + }; + + grappelli.initSearchbar = function() { + var searchbar = $("input#searchbar"); + // var searchbar = $("input#searchbar"), + // searchbar_tooltip = $('div#searchbar_tooltip'); + // if (searchbar_tooltip.length == 0) return; + + searchbar.focus(); + + // searchbar.bind('keydown', function() { + // searchbar_tooltip.hide(); + // }); + // searchbar.bind("mouseover", function() { + // searchbar_tooltip.show(); + // }); + // searchbar.bind("mouseout", function() { + // searchbar_tooltip.hide(); + // }); + }; + + // Using window.load instead of document ready for better performances + // It prevents lots of glitches, like divs that moves around upon loading + // + // Use $(document).ready only if you have to deal with images since it will + // wait for the document to be fully loaded/rendered before running the function + // while window.load method will run as soon as the DOM/CSS is loaded. + + $(window).load(function() { + // we do the hacks first! + // because we manipulate dom via innerHTML => loose events + grappelli.initHacks(); + grappelli.initCollapsible(); + grappelli.initDateAndTimePicker(); + grappelli.initSearchbar(); + + // add something for clicking outside the navigation-menu + }); +})(django.jQuery); diff --git a/lib/grappelli/media/js/grappelli.timepicker.js b/lib/grappelli/media/js/grappelli.timepicker.js new file mode 100644 index 0000000..05bedcc --- /dev/null +++ b/lib/grappelli/media/js/grappelli.timepicker.js @@ -0,0 +1,171 @@ +/** + * this is grappellis timepicker. + * works pretty similar to ui.datepicker: + * - adds a button to the element + * - creates a node (div) at the bottom called ui-timepicker + * - element.onClick fills the ui-timepicker node with the time_list (all times you can select) + */ + +(function( $ ) { +$.widget("ui.timepicker", { + // default options + options: { + // template for the container of the timepicker + template: '<div id="ui-timepicker" class="module" style="position: absolute; display: none;"></div>', + // selector to get the ui-timepicker once it's added to the dom + timepicker_selector: "#ui-timepicker", + // needed offset of the container from the element + offset: { + top: 0 + }, + // if time_list wasn't sent when calling the timepicker we use this + default_time_list: [ + 'now', + '00:00', + '01:00', + '02:00', + '03:00', + '04:00', + '05:00', + '06:00', + '07:00', + '08:00', + '09:00', + '10:00', + '11:00', + '12:00', + '13:00', + '14:00', + '15:00', + '16:00', + '17:00', + '18:00', + '19:00', + '20:00', + '21:00', + '22:00', + '23:00' + ], + // leave this empty!!! + // NOTE: you can't set a default for time_list because if you call: + // $("node").timepicker({time_list: ["01:00", "02:00"]}) + // ui.widget will extend/merge the options.time_list whith the one you sent. + time_list: [] + }, + + /** + * init timepicker for a specific element + */ + _create: function() { + // for the events + var self = this; + + // to close timpicker if you click somewhere in the document + $(document).mousedown(function(evt) { + if (self.timepicker.is(":visible")) { + var $target = $(evt.target); + if ($target[0].id != self.timepicker[0].id && $target.parents(self.options.timepicker_selector).length == 0 && !$target.hasClass('hasTimepicker') && !$target.hasClass('ui-timepicker-trigger')) { + self.timepicker.hide(); + } + } + }); + + // get/create timepicker's container + if ($(this.options.timepicker_selector).size() == 0) { + $(this.options.template).appendTo('body'); + } + this.timepicker = $(this.options.timepicker_selector); + this.timepicker.hide(); + + // modify the element and create the button + this.element.addClass("hasTimepicker"); + this.button = $('<button type="button" class="ui-timepicker-trigger"></button>'); + this.element.after(this.button); + + // disable button if element is disabled + if (this.element.attr("disabled")) { + this.button.attr("disabled", true); + } + // register event + else { + this.button.click(function() { + self._toggleTimepicker(); + }); + } + }, + + /** + * called when button is clicked + */ + _toggleTimepicker: function() { + if (this.timepicker.is(":visible")) { + this.timepicker.hide(); + } else { + this.element.focus(); + this._generateTimepickerContents(); + this._showTimepicker(); + } + }, + + /** + * fills timepicker with time_list of element and shows it. + * + * called by _toggleTimepicker + */ + _generateTimepickerContents: function() { + var self = this, + template_str = "<ul>"; + + // there is no time_list for this instance so use the default one + if (this.options.time_list.length == 0) { + this.options.time_list = this.options.default_time_list; + } + + for (var i = 0; i < this.options.time_list.length; i++) { + if (this.options.time_list[i] == "now") { + var now = new Date(), + hours = now.getHours(), + minutes = now.getMinutes(); + + hours = ((hours < 10) ? "0" + hours : hours); + minutes = ((minutes < 10) ? "0" + minutes : minutes); + + template_str += '<li class="ui-state-active row">' + hours + ":" + minutes + '</li>'; + } else { + template_str += '<li class="ui-state-default row">' + this.options.time_list[i] + '</li>'; + } + } + template_str += "</ul>"; + + // fill timepicker container + this.timepicker.html(template_str); + + // click handler for items (times) in timepicker + this.timepicker.find('li').click(function() { + // remove active class from all items + $(this).parent().children('li').removeClass("ui-state-active"); + // mark clicked item as active + $(this).addClass("ui-state-active"); + + // set the new value and hide the timepicker + self.element.val($(this).html()); + self.timepicker.hide(); + }); + }, + + /** + * sets offset and shows timepicker containter + */ + _showTimepicker: function() { + this.timepicker_offset = this.element.offset(); + this.timepicker_offset.top += this.element.outerHeight() + this.options.offset.top; + this.timepicker.css(this.timepicker_offset); + this.timepicker.show(); + }, + + destroy: function() { + $.Widget.prototype.destroy.apply(this, arguments); // default destroy + // now do other stuff particular to this widget + } +}); +})(django.jQuery);
\ No newline at end of file diff --git a/lib/grappelli/media/js/inlines.js b/lib/grappelli/media/js/inlines.js new file mode 100644 index 0000000..3725975 --- /dev/null +++ b/lib/grappelli/media/js/inlines.js @@ -0,0 +1,354 @@ +/** + * helper functions for sortable inlines (tabular and stacked) + */ + +function reinitDateTimeFields(row) { + row.find(".vDateField").datepicker({ + //appendText: '(mm/dd/yyyy)', + showOn: 'button', + buttonImageOnly: false, + buttonText: '', + dateFormat: grappelli.getFormat('date'), + }); + row.find(".vTimeField").timepicker(); +} + +function updateSelectFilter(row) { + // If any SelectFilter widgets were added, instantiate a new instance. + if (typeof SelectFilter != "undefined"){ + row.find(".selectfilter").each(function(index, value){ + var namearr = value.name.split('-'); + SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}"); + }); + row.find(".selectfilterstacked").each(function(index, value){ + var namearr = value.name.split('-'); + SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}"); + }); + } +} + +/** + * reorder of inlines dom + * works pretty similar to updateFormIndex() of the inline() widget + * helper function for sortable + */ +function sortable_updateFormIndex(form, idx, prefix) { + var re = /-\d+-/g; + form.attr('id', prefix + idx); + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = django.jQuery(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, "-" + idx + "-")); + if (node_name) node.attr('name', node_name.replace(re, "-" + idx + "-")); + if (node_for) node.attr('for', node_for.replace(re, "-" + idx + "-")); + if (node_href) node.attr('href', node_href.replace(re, "-" + idx + "-")); + }); +} + +/** + * checks if inline form is filled + * helper function for sortable + */ +function is_form_filled(form) { + var input_tags = form.find("input"), + input_tag; + for (var i = 0; i < input_tags.length; i++) { + input_tag = django.jQuery(input_tags[i]); + if (input_tag.val()) { + if (input_tag.attr("type") == "checkbox" || input_tag.attr("type") == "radio") { + if (input_tag.attr("checked")) { + return true; + } + } else if (input_tag.attr("type") != "hidden"){ + return true; + } + } + } + return false; +} + +// updates label of inlines +// to keep them in sequence (#1,#2,#3,...) +function stacked_updateInlineLabel(row) { + row.parent().find("div.module").find("h3:first").each(function(i) { + var h3_node = django.jQuery(this); + h3_node.html(h3_node.html().replace(/(#\d+)/g, "#" + ++i)); + }); +} + +// init tinymce for new inlines +function reinitTinyMCE(row) { + row.find("textarea.vLargeTextField").each(function() { + tinyMCE.execCommand('mceAddControl', false, this.id); + }); +} + +// need to remove tinymce form removed inline +function deleteTinyMCE(row) { + row.find("textarea.vLargeTextField").each(function() { + if (tinyMCE.getInstanceById(this.id)) { + tinyMCE.execCommand('mceRemoveControl', false, this.id); + } + }); +} + +function tabular_onAdded(row) { + reinitDateTimeFields(row); + updateSelectFilter(row); +} + +function stacked_onAdded(row) { + reinitTinyMCE(row); + reinitDateTimeFields(row); + updateSelectFilter(row); + stacked_updateInlineLabel(row); +} + +function stacked_onRemoved(row) { + stacked_updateInlineLabel(row); + deleteTinyMCE(row); +} + + +(function($) { +$.fn.inline = function(options) { + var defaults = { + prefix: "form", // The form prefix for your django formset + addText: "add another", // Text for the add link + deleteText: "remove", // Text for the delete link + addCssClass: "add-handler", // CSS class applied to the add link + deleteCssClass: "delete-handler", // CSS class applied to the delete link + removeCssClass: "remove-handler", // CSS class applied to the remove link + emptyCssClass: "empty-form", // CSS class applied to the empty row + formCssClass: "dynamic-form", // CSS class applied to each form in a formset + predeleteCssClass: "predelete", + onAdded: null, // Function called each time a new form is added + onRemoved: null // Function called each time a form is deleted + }; + + options = $.extend(defaults, options); + + return this.each(function() { + + var inline = $(this), // the current inline node + totalForms = inline.find("#id_" + options.prefix + "-TOTAL_FORMS"), // current forms total + maxForms = inline.find("#id_" + options.prefix + "-MAX_NUM_FORMS"), // max forms in this inline (0 if no limit) + addButtons = inline.find("a." + options.addCssClass), + template = inline.find("#" + options.prefix + "-empty"), // the hidden node we copy to create an additional form + template_id = template.attr('id'), + template_ready = false, + /* + updateElementIndex = function(el, prefix, ndx) { + var id_regex = new RegExp("(" + prefix + "-\\d+)"); + var replacement = prefix + "-" + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }, + */ + initAddButtons = function() { + if (maxForms.val() == 0 || (maxForms.val() - totalForms.val()) > 0) { + addButtons.click(addButtonHandler); + } else { + // hide add-buttons + hideAddBottons(); + } + }, + + initFormIndex = function(form, nextIndex) { + var re = /__prefix__/g; + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = $(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, nextIndex)); + if (node_name) node.attr('name', node_name.replace(re, nextIndex)); + if (node_for) node.attr('for', node_for.replace(re, nextIndex)); + if (node_href) node.attr('href', node_href.replace(re, nextIndex)); + }); + }, + + updateFormIndex = function(form, idx) { + // need to trigger onRemove and + // onAdded (on bottom of function) to reinint rows + if (options.onRemoved) { + options.onRemoved(form); + } + var re = /-\d+-/g; + form.attr('id', options.prefix + idx); + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = $(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, "-" + idx + "-")); + if (node_name) node.attr('name', node_name.replace(re, "-" + idx + "-")); + if (node_for) node.attr('for', node_for.replace(re, "-" + idx + "-")); + if (node_href) node.attr('href', node_href.replace(re, "-" + idx + "-")); + }); + if (options.onAdded) { + options.onAdded(form); + } + }, + + addButtonHandler = function() { + // FIXME wrong place to do this + // choices: + // 1) create a new event (beforAdded) and try to do this form outside :( + // 2) add the "editor_deselector" class to the templates textarea + // ... + if (!options.template_ready) { + if (typeof tinyMCE !== "undefined") { + template.find('textarea').each(function(e) { + if (tinyMCE.getInstanceById(this.id)) { + tinyMCE.execCommand('mceRemoveControl', false, this.id); + } + }); + } + options.template_ready = true; + } + + var nextIndex = parseInt(totalForms.val(), 10); + + // create new from (add it as last) + var form = template.clone(true); + + form.removeClass(options.emptyCssClass) + .attr("id", template_id.replace("-empty", nextIndex)) + .insertBefore(template) + .addClass(options.formCssClass); + + initFormIndex(form, nextIndex); + + totalForms.val(nextIndex + 1); + + // Hide add button in case we've hit the max, except we want to add infinitely + if ((maxForms.val() != 0) && (maxForms.val() - totalForms.val()) <= 0) { + // hide stuff + hideAddBottons(); + } + + // If a post-add callback was supplied, call it with the added form + if (options.onAdded) { + options.onAdded(form); + } + return false; + }, + + hideAddBottons = function() { + addButtons.hide().parents('div.add-item').hide(); + }, + + showAddButtons = function() { + addButtons.show().parents('div.add-item').show(); + }, + + deleteHandler = function() { + var deleteInput = $(this).prev(), + form = deleteInput.parents("." + options.formCssClass).first(); + if (form.hasClass("has_original")) { // toggle delete checkbox and delete css class + form.toggleClass(options.predeleteCssClass); + if (deleteInput.attr("checked")) { + deleteInput.removeAttr("checked"); + } else { + deleteInput.attr("checked", 'checked'); + } + } + return false; + }, + + removeHandler = function() { + var deleteInput = $(this).prev(), + form = deleteInput.parents("." + options.formCssClass).first(); + // last one stays + // else if (totalForms.val() == 1) + // alert("letztes bleibt da!"); + // return false; + // remove form + // Remove the parent form containing this button: + form.remove(); + // If a post-delete callback was provided, call it with the deleted form: + if (options.onRemoved) { + options.onRemoved(form); + } + // Update the TOTAL_FORMS form count. + var forms = inline.find("." + options.formCssClass); + totalForms.val(parseInt(totalForms.val(), 10) - 1); + + // Show add button again once we drop below max + if ((maxForms.val() == 0) || (maxForms.val() >= forms.length)) { + showAddButtons(); + } + // Also, update names and ids for all remaining form controls + // so they remain in sequence: + var startReplaceAt = form.attr("id"); + startReplaceAt = parseInt(startReplaceAt.replace(options.prefix, ""), 10); + for (var i = startReplaceAt; i < forms.length; i++) { + updateFormIndex($(forms[i]), i); + } + return false; + }, + + initInlineForms = function() { + var hasErrors = false; + //inline.find("div.items div.module").each(function() { + inline.find("div.module").each(function() { + var form = $(this); + // add the options.formCssClass to all forms in the inline + if (form.attr('id') !== "") { + form.not("." + options.emptyCssClass).addClass(options.formCssClass); + } + // open the form if it has errors + if (form.find("ul.errorlist").length > 0) { + form.removeClass('closed').addClass('open'); + // to open the inline + hasErrors = true; + } + }); + + // open the inline if it has forms with errors in it + if (hasErrors) { + inline.removeClass('closed').addClass('open'); + } + }; + + // set this to prevent the browser from keeping the current value after reload + totalForms.attr("autocomplete", "off"); + + initInlineForms(); + + initAddButtons(); + + // delete button + // toggle the delete-checkbox and add the predelete-class to the row + inline.find("a." + options.deleteCssClass).click(deleteHandler); + inline.find("a." + options.removeCssClass).click(removeHandler); + + // add options.predeleteCssClass to forms with the delete checkbox checked + inline.find("li.delete-handler-container input").each(function() { + var deleteInput = $(this); + if (deleteInput.attr("checked")) { + var form = $(deleteInput.parents("." + options.formCssClass).first()); + if (form.hasClass("has_original")) { + form.toggleClass(options.predeleteCssClass); + } + } + }); + }); +}; +})(django.jQuery); diff --git a/lib/grappelli/media/js/inlines.min.js b/lib/grappelli/media/js/inlines.min.js new file mode 100644 index 0000000..c4c9e74 --- /dev/null +++ b/lib/grappelli/media/js/inlines.min.js @@ -0,0 +1,380 @@ +/** + * helper functions for sortable inlines (tabular and stacked) + */ + +function reinitDateTimeFields(row) { + row.find(".vDateField").datepicker({ + //appendText: '(mm/dd/yyyy)', + showOn: 'button', + buttonImageOnly: false, + buttonText: '', + dateFormat: grappelli.getFormat('date'), + }); + row.find(".vTimeField").timepicker(); +} + +function updateSelectFilter(row) { + // If any SelectFilter widgets were added, instantiate a new instance. + if (typeof SelectFilter != "undefined"){ + row.find(".selectfilter").each(function(index, value){ + var namearr = value.name.split('-'); + SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}"); + }); + row.find(".selectfilterstacked").each(function(index, value){ + var namearr = value.name.split('-'); + SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}"); + }); + } +} + +/** + * reorder of inlines dom + * works pretty similar to updateFormIndex() of the inline() widget + * helper function for sortable + */ +function sortable_updateFormIndex(form, idx, prefix) { + var re = /-\d+-/g; + form.attr('id', prefix + idx); + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = django.jQuery(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, "-" + idx + "-")); + if (node_name) node.attr('name', node_name.replace(re, "-" + idx + "-")); + if (node_for) node.attr('for', node_for.replace(re, "-" + idx + "-")); + if (node_href) node.attr('href', node_href.replace(re, "-" + idx + "-")); + }); +} + +/** + * checks if inline form is filled + * helper function for sortable + */ +function is_form_filled(form) { + var input_tags = form.find("input"), + input_tag, + select_tags = form.find('select'), + select_tag, + select_template, + re = /-\d+-/g; + + // flagged for deletion + if (form.find("input[id$='DELETE']").attr("checked")) { + return false; + } + // loop thru all inputs and check if anything is set + for (var i = 0; i < input_tags.length; i++) { + input_tag = django.jQuery(input_tags[i]); + if (input_tag.val()) { + // checkboxes and radio need to be checked too + if (input_tag.attr("type") == "checkbox" || input_tag.attr("type") == "radio") { + if (input_tag.attr("checked")) { + return true; + } + // hidden fields are ignored + } else if (input_tag.attr("type") != "hidden"){ + return true; + } + } + } + + for (var i = 0; i < select_tags.length; i++) { + select_tag = django.jQuery(select_tags[i]); + if (select_tag.val()) { + // get the hidden empty form and compare against it's value (which is the default/empty value) + select_template = form.find("#" + select_tag.attr('id').replace(re, "__prefix__")); + // if they aren't equal the form is filled + if (select_tag.val() != select_template.val()) { + return true; + } + } + } + + return false; +} + +// updates label of inlines +// to keep them in sequence (#1,#2,#3,...) +function stacked_updateInlineLabel(row) { + row.parent().find("div.module").find("h3:first").each(function(i) { + var h3_node = django.jQuery(this); + h3_node.html(h3_node.html().replace(/(#\d+)/g, "#" + ++i)); + }); +} + +// init tinymce for new inlines +function reinitTinyMCE(row) { + row.find("textarea.vLargeTextField").each(function() { + tinyMCE.execCommand('mceAddControl', false, this.id); + }); +} + +// need to remove tinymce form removed inline +function deleteTinyMCE(row) { + row.find("textarea.vLargeTextField").each(function() { + if (tinyMCE.getInstanceById(this.id)) { + tinyMCE.execCommand('mceRemoveControl', false, this.id); + } + }); +} + +function tabular_onAdded(row) { + reinitDateTimeFields(row); + updateSelectFilter(row); +} + +function tabular_onRemoved(row) {} + +function stacked_onAdded(row) { + reinitTinyMCE(row); + reinitDateTimeFields(row); + updateSelectFilter(row); + stacked_updateInlineLabel(row); +} + +function stacked_onRemoved(row) { + stacked_updateInlineLabel(row); + deleteTinyMCE(row); +} + + +(function($) { +$.fn.inline = function(options) { + var defaults = { + prefix: "form", // The form prefix for your django formset + addText: "add another", // Text for the add link + deleteText: "remove", // Text for the delete link + addCssClass: "add-handler", // CSS class applied to the add link + deleteCssClass: "delete-handler", // CSS class applied to the delete link + removeCssClass: "remove-handler", // CSS class applied to the remove link + emptyCssClass: "empty-form", // CSS class applied to the empty row + formCssClass: "dynamic-form", // CSS class applied to each form in a formset + predeleteCssClass: "predelete", + onAdded: null, // Function called each time a new form is added + onRemoved: null // Function called each time a form is deleted + }; + + options = $.extend(defaults, options); + + return this.each(function() { + + var inline = $(this), // the current inline node + totalForms = inline.find("#id_" + options.prefix + "-TOTAL_FORMS"), // current forms total + maxForms = inline.find("#id_" + options.prefix + "-MAX_NUM_FORMS"), // max forms in this inline (0 if no limit) + addButtons = inline.find("a." + options.addCssClass), + template = inline.find("#" + options.prefix + "-empty"), // the hidden node we copy to create an additional form + template_id = template.attr('id'), + template_ready = false, + /* + updateElementIndex = function(el, prefix, ndx) { + var id_regex = new RegExp("(" + prefix + "-\\d+)"); + var replacement = prefix + "-" + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }, + */ + initAddButtons = function() { + addButtons.click(addButtonHandler); + if (maxForms.val() != 0 && (maxForms.val() - totalForms.val()) == 0) { + // hide add-buttons + hideAddBottons(); + } + }, + + initFormIndex = function(form, nextIndex) { + var re = /__prefix__/g; + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = $(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, nextIndex)); + if (node_name) node.attr('name', node_name.replace(re, nextIndex)); + if (node_for) node.attr('for', node_for.replace(re, nextIndex)); + if (node_href) node.attr('href', node_href.replace(re, nextIndex)); + }); + }, + + updateFormIndex = function(form, idx) { + // need to trigger onRemove and + // onAdded (on bottom of function) to reinint rows + if (options.onRemoved) { + options.onRemoved(form); + } + var re = /-\d+-/g; + form.attr('id', options.prefix + idx); + form.find(':input,span,table,iframe,label,a,ul,p,img').each(function() { + var node = $(this), + node_id = node.attr('id'), + node_name = node.attr('name'), + node_for = node.attr('for'), + node_href = node.attr("href"); + + if (node_id) node.attr('id', node_id.replace(re, "-" + idx + "-")); + if (node_name) node.attr('name', node_name.replace(re, "-" + idx + "-")); + if (node_for) node.attr('for', node_for.replace(re, "-" + idx + "-")); + if (node_href) node.attr('href', node_href.replace(re, "-" + idx + "-")); + }); + if (options.onAdded) { + options.onAdded(form); + } + }, + + addButtonHandler = function() { + // FIXME wrong place to do this + // choices: + // 1) create a new event (beforAdded) and try to do this form outside :( + // 2) add the "editor_deselector" class to the templates textarea + // ... + if (!options.template_ready) { + if (typeof tinyMCE !== "undefined") { + template.find('textarea').each(function(e) { + if (tinyMCE.getInstanceById(this.id)) { + tinyMCE.execCommand('mceRemoveControl', false, this.id); + } + }); + } + options.template_ready = true; + } + + var nextIndex = parseInt(totalForms.val(), 10); + + // create new from (add it as last) + var form = template.clone(true); + + form.removeClass(options.emptyCssClass) + .attr("id", template_id.replace("-empty", nextIndex)) + .insertBefore(template) + .addClass(options.formCssClass); + + initFormIndex(form, nextIndex); + + totalForms.val(nextIndex + 1); + + // Hide add button in case we've hit the max, except we want to add infinitely + if ((maxForms.val() != 0) && (maxForms.val() - totalForms.val()) <= 0) { + // hide stuff + hideAddBottons(); + } + + // If a post-add callback was supplied, call it with the added form + if (options.onAdded) { + options.onAdded(form); + } + return false; + }, + + hideAddBottons = function() { + addButtons.hide().parents('div.add-item').hide(); + }, + + showAddButtons = function() { + addButtons.show().parents('div.add-item').show(); + }, + + deleteHandler = function() { + var deleteInput = $(this).prev(), + form = deleteInput.parents("." + options.formCssClass).first(); + if (form.hasClass("has_original")) { // toggle delete checkbox and delete css class + form.toggleClass(options.predeleteCssClass); + if (deleteInput.attr("checked")) { + deleteInput.removeAttr("checked"); + } else { + deleteInput.attr("checked", 'checked'); + } + } + return false; + }, + + removeHandler = function() { + var deleteInput = $(this).prev(), + form = deleteInput.parents("." + options.formCssClass).first(); + // last one stays + // else if (totalForms.val() == 1) + // alert("letztes bleibt da!"); + // return false; + // remove form + // Remove the parent form containing this button: + form.remove(); + // If a post-delete callback was provided, call it with the deleted form: + if (options.onRemoved) { + options.onRemoved(form); + } + // Update the TOTAL_FORMS form count. + var forms = inline.find("." + options.formCssClass); + totalForms.val(parseInt(totalForms.val(), 10) - 1); + + // Show add button again once we drop below max + if ((maxForms.val() == 0) || (maxForms.val() >= forms.length)) { + showAddButtons(); + } + // Also, update names and ids for all remaining form controls + // so they remain in sequence: + var startReplaceAt = form.attr("id"); + startReplaceAt = parseInt(startReplaceAt.replace(options.prefix, ""), 10); + for (var i = startReplaceAt; i < forms.length; i++) { + updateFormIndex($(forms[i]), i); + } + return false; + }, + + initInlineForms = function() { + var hasErrors = false; + //inline.find("div.items div.module").each(function() { + inline.find("div.module").each(function() { + var form = $(this); + // add the options.formCssClass to all forms in the inline + if (form.attr('id') !== "") { + form.not("." + options.emptyCssClass).addClass(options.formCssClass); + } + // open the form if it has errors + if (form.find("ul.errorlist").length > 0) { + form.removeClass('closed').addClass('open'); + // to open the inline + hasErrors = true; + } + }); + + // open the inline if it has forms with errors in it + if (hasErrors) { + inline.removeClass('closed').addClass('open'); + } + }; + + // set this to prevent the browser from keeping the current value after reload + totalForms.attr("autocomplete", "off"); + + initInlineForms(); + + initAddButtons(); + + // delete button + // toggle the delete-checkbox and add the predelete-class to the row + inline.find("a." + options.deleteCssClass).click(deleteHandler); + inline.find("a." + options.removeCssClass).click(removeHandler); + + // add options.predeleteCssClass to forms with the delete checkbox checked + inline.find("li.delete-handler-container input").each(function() { + var deleteInput = $(this); + if (deleteInput.attr("checked")) { + var form = $(deleteInput.parents("." + options.formCssClass).first()); + if (form.hasClass("has_original")) { + form.toggleClass(options.predeleteCssClass); + } + } + }); + }); +}; +})(django.jQuery); diff --git a/lib/grappelli/media/js/jquery.init.js b/lib/grappelli/media/js/jquery.init.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/jquery.init.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/jquery.js b/lib/grappelli/media/js/jquery.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/jquery.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/jquery.min.js b/lib/grappelli/media/js/jquery.min.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/jquery.min.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/prepopulate.min.js b/lib/grappelli/media/js/prepopulate.min.js new file mode 100644 index 0000000..b8a75ab --- /dev/null +++ b/lib/grappelli/media/js/prepopulate.min.js @@ -0,0 +1,3 @@ +// dropped +// not used in grappelli +// kept this file to prevent 404
\ No newline at end of file diff --git a/lib/grappelli/media/js/timeparse.js b/lib/grappelli/media/js/timeparse.js new file mode 100644 index 0000000..882f41d --- /dev/null +++ b/lib/grappelli/media/js/timeparse.js @@ -0,0 +1,94 @@ +var timeParsePatterns = [ + // 9 + { re: /^\d{1,2}$/i, + handler: function(bits) { + if (bits[0].length == 1) { + return '0' + bits[0] + ':00'; + } else { + return bits[0] + ':00'; + } + } + }, + // 13:00 + { re: /^\d{2}[:.]\d{2}$/i, + handler: function(bits) { + return bits[0].replace('.', ':'); + } + }, + // 9:00 + { re: /^\d[:.]\d{2}$/i, + handler: function(bits) { + return '0' + bits[0].replace('.', ':'); + } + }, + // 3 am / 3 a.m. / 3am + { re: /^(\d+)\s*([ap])(?:.?m.?)?$/i, + handler: function(bits) { + var hour = parseInt(bits[1]); + if (hour == 12) { + hour = 0; + } + if (bits[2].toLowerCase() == 'p') { + if (hour == 12) { + hour = 0; + } + return (hour + 12) + ':00'; + } else { + if (hour < 10) { + return '0' + hour + ':00'; + } else { + return hour + ':00'; + } + } + } + }, + // 3.30 am / 3:15 a.m. / 3.00am + { re: /^(\d+)[.:](\d{2})\s*([ap]).?m.?$/i, + handler: function(bits) { + var hour = parseInt(bits[1]); + var mins = parseInt(bits[2]); + if (mins < 10) { + mins = '0' + mins; + } + if (hour == 12) { + hour = 0; + } + if (bits[3].toLowerCase() == 'p') { + if (hour == 12) { + hour = 0; + } + return (hour + 12) + ':' + mins; + } else { + if (hour < 10) { + return '0' + hour + ':' + mins; + } else { + return hour + ':' + mins; + } + } + } + }, + // noon + { re: /^no/i, + handler: function(bits) { + return '12:00'; + } + }, + // midnight + { re: /^mid/i, + handler: function(bits) { + return '00:00'; + } + } +]; + +function parseTimeString(s) { + for (var i = 0; i < timeParsePatterns.length; i++) { + var re = timeParsePatterns[i].re; + var handler = timeParsePatterns[i].handler; + var bits = re.exec(s); + if (bits) { + return handler(bits); + } + } + return s; +} diff --git a/lib/grappelli/media/js/urlify.js b/lib/grappelli/media/js/urlify.js new file mode 100644 index 0000000..d8f2549 --- /dev/null +++ b/lib/grappelli/media/js/urlify.js @@ -0,0 +1,140 @@ +var LATIN_MAP = { + 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': + 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', + 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': + 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', + 'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä': + 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', + 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': + 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', + 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' +} +var LATIN_SYMBOLS_MAP = { + '©':'(c)' +} +var GREEK_MAP = { + 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', + 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', + 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', + 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', + 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', + 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', + 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', + 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', + 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', + 'Ϋ':'Y' +} +var TURKISH_MAP = { + 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', + 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' +} +var RUSSIAN_MAP = { + 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', + 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', + 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', + 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', + 'я':'ya', + 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', + 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', + 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', + 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', + 'Я':'Ya' +} +var UKRAINIAN_MAP = { + 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' +} +var CZECH_MAP = { + 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', + 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', + 'Ů':'U', 'Ž':'Z' +} + +var POLISH_MAP = { + 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', + 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'e', 'Ł':'L', 'Ń':'N', 'Ó':'o', 'Ś':'S', + 'Ź':'Z', 'Ż':'Z' +} + +var LATVIAN_MAP = { + 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', + 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'i', + 'Ķ':'k', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'u', 'Ž':'Z' +} + +var ALL_DOWNCODE_MAPS=new Array() +ALL_DOWNCODE_MAPS[0]=LATIN_MAP +ALL_DOWNCODE_MAPS[1]=LATIN_SYMBOLS_MAP +ALL_DOWNCODE_MAPS[2]=GREEK_MAP +ALL_DOWNCODE_MAPS[3]=TURKISH_MAP +ALL_DOWNCODE_MAPS[4]=RUSSIAN_MAP +ALL_DOWNCODE_MAPS[5]=UKRAINIAN_MAP +ALL_DOWNCODE_MAPS[6]=CZECH_MAP +ALL_DOWNCODE_MAPS[7]=POLISH_MAP +ALL_DOWNCODE_MAPS[8]=LATVIAN_MAP + +var Downcoder = new Object(); +Downcoder.Initialize = function() +{ + if (Downcoder.map) // already made + return ; + Downcoder.map ={} + Downcoder.chars = '' ; + for(var i in ALL_DOWNCODE_MAPS) + { + var lookup = ALL_DOWNCODE_MAPS[i] + for (var c in lookup) + { + Downcoder.map[c] = lookup[c] ; + Downcoder.chars += c ; + } + } + Downcoder.regex = new RegExp('[' + Downcoder.chars + ']|[^' + Downcoder.chars + ']+','g') ; +} + +downcode= function( slug ) +{ + Downcoder.Initialize() ; + var downcoded ="" + var pieces = slug.match(Downcoder.regex); + if(pieces) + { + for (var i = 0 ; i < pieces.length ; i++) + { + if (pieces[i].length == 1) + { + var mapped = Downcoder.map[pieces[i]] ; + if (mapped != null) + { + downcoded+=mapped; + continue ; + } + } + downcoded+=pieces[i]; + } + } + else + { + downcoded = slug; + } + return downcoded; +} + + +function URLify(s, num_chars) { + // changes, e.g., "Petty theft" to "petty_theft" + // remove all these words from the string before urlifying + s = downcode(s); + removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from", + "is", "in", "into", "like", "of", "off", "on", "onto", "per", + "since", "than", "the", "this", "that", "to", "up", "via", + "with"]; + r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi'); + s = s.replace(r, ''); + // if downcode doesn't hit, the char will be stripped here + s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars + s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces + s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens + s = s.toLowerCase(); // convert to lowercase + return s.substring(0, num_chars);// trim to first num_chars chars +} + |