/* global django */ // IE<9 lacks Array.prototype.indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (needle) { for (var i = 0, l = this.length; i < l; ++i) { if (this[i] === needle) return i } return -1 } } // https://github.com/jquery/jquery-ui/blob/master/ui/disable-selection.js django.jQuery.fn.extend({ disableSelection: (function () { var eventType = "onselectstart" in document.createElement("div") ? "selectstart" : "mousedown" return function () { return this.on(eventType + ".ui-disableSelection", function (event) { event.preventDefault() }) } })(), enableSelection: function () { return this.off(".ui-disableSelection") }, }) django.jQuery(function ($) { // We are not on a changelist it seems. if (!document.getElementById("result_list")) return var DraggableMPTTAdmin = null function isExpandedNode(id) { return DraggableMPTTAdmin.collapsedNodes.indexOf(id) == -1 } function markNodeAsExpanded(id) { // remove itemId from array of collapsed nodes var idx = DraggableMPTTAdmin.collapsedNodes.indexOf(id) if (idx >= 0) DraggableMPTTAdmin.collapsedNodes.splice(idx, 1) } function markNodeAsCollapsed(id) { if (isExpandedNode(id)) DraggableMPTTAdmin.collapsedNodes.push(id) } function treeNode(pk) { return $('.tree-node[data-pk="' + pk + '"]') } // toggle children function doToggle(id, show) { var children = DraggableMPTTAdmin.treeStructure[id] || [] for (var i = 0; i < children.length; ++i) { var childId = children[i] if (show) { treeNode(childId).closest("tr").show() // only reveal children if current node is not collapsed if (isExpandedNode(childId)) { doToggle(childId, show) } } else { treeNode(childId).closest("tr").hide() // always recursively hide children doToggle(childId, show) } } } function rowLevel($row) { try { return $row.find(".tree-node").data("level") || 0 } catch (e) { return 0 } } /* * FeinCMS Drag-n-drop tree reordering. * Based upon code by bright4 for Radiant CMS, rewritten for * FeinCMS by Bjorn Post. * * September 2010 */ $.extend( ($.fn.feinTree = function () { $.each(DraggableMPTTAdmin.treeStructure, function (key, _value) { treeNode(key).addClass("children") }) $("div.drag-handle").bind("mousedown", function (event) { var BEFORE = "before" var AFTER = "after" var CHILD = "child" var CHILD_PAD = DraggableMPTTAdmin.levelIndent var originalRow = $(event.target).closest("tr") var rowHeight = originalRow.height() var moveTo = new Object() var resultListWidth = $("#result_list").width() $("body") .addClass("dragging") .disableSelection() .bind("mousemove", function (event) { // Remove focus originalRow.blur() // attach dragged item to mouse var cloned = originalRow.html() if ($("#ghost").length == 0) { $('
').appendTo("body") } $("#ghost") .html(cloned) .css({ opacity: 0.8, position: "absolute", top: event.pageY, left: event.pageX - 30, width: 600, }) // check on edge of screen if ( event.pageY + 100 > $(window).height() + $(window).scrollTop() ) { $("html,body") .stop() .animate({ scrollTop: $(window).scrollTop() + 250 }, 500) } else if (event.pageY - 50 < $(window).scrollTop()) { $("html,body") .stop() .animate({ scrollTop: $(window).scrollTop() - 250 }, 500) } // check if drag-line element already exists, else append if ($("#drag-line").length < 1) { $("body").append('
') } // loop trough all rows $("tr", originalRow.parent()).each(function (index, el) { var element = $(el), top = element.offset().top, next // check if mouse is over a row if (event.pageY >= top && event.pageY < top + rowHeight) { var targetRow = null, targetLoc = null, elementLevel = rowLevel(element) if (event.pageY >= top && event.pageY < top + rowHeight / 3) { targetRow = element targetLoc = BEFORE } else if ( event.pageY >= top + rowHeight / 3 && event.pageY < top + (rowHeight * 2) / 3 ) { next = element.next() // there's no point in allowing adding children when there are some already // better move the items to the correct place right away if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element targetLoc = CHILD } } else if ( event.pageY >= top + (rowHeight * 2) / 3 && event.pageY < top + rowHeight ) { next = element.next() if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element targetLoc = AFTER } } if (targetRow) { // Positioning relative to cell containing the link var offset = targetRow.find("th").offset() var left = offset.left + rowLevel(targetRow) * CHILD_PAD + (targetLoc == CHILD ? CHILD_PAD : 0) + 5 // Center of the circle aligns with start of link text (cell padding!) $("#drag-line") .css({ width: resultListWidth - left, left: left, top: offset.top + (targetLoc == BEFORE ? 0 : rowHeight), }) .find("span") .text(DraggableMPTTAdmin.messages[targetLoc] || "") // Store the found row and options moveTo.hovering = element moveTo.relativeTo = targetRow moveTo.side = targetLoc return true } } }) }) $("body").keydown(function (event) { if (event.which == "27") { $("#drag-line").remove() $("#ghost").remove() $("body") .removeClass("dragging") .enableSelection() .unbind("mousemove") .unbind("mouseup") event.preventDefault() } }) $("body").bind("mouseup", function () { if (moveTo.relativeTo) { var cutItem = originalRow.find(".tree-node").data("pk") var pastedOn = moveTo.relativeTo.find(".tree-node").data("pk") // get out early if items are the same if (cutItem != pastedOn) { var isParent = rowLevel(moveTo.relativeTo.next()) > rowLevel(moveTo.relativeTo) var position = "" // determine position if (moveTo.side == CHILD && !isParent) { position = "last-child" } else if (moveTo.side == BEFORE) { position = "left" } else { position = "right" } $.ajax({ complete: function () { window.location.reload() }, data: { cmd: "move_node", position: position, cut_item: cutItem, pasted_on: pastedOn, }, headers: { "X-CSRFToken": $( "input[type=hidden][name=csrfmiddlewaretoken]" ).val(), }, method: "POST", }) } else { $("#drag-line").remove() $("#ghost").remove() } $("body") .removeClass("dragging") .enableSelection() .unbind("mousemove") .unbind("mouseup") } }) }) return this }) ) /* Every time the user expands or collapses a part of the tree, we remember the current state of the tree so we can restore it on a reload. */ function storeCollapsedNodes(nodes) { window.localStorage && window.localStorage.setItem( DraggableMPTTAdmin.storageName, JSON.stringify(nodes) ) } function retrieveCollapsedNodes() { try { return JSON.parse( window.localStorage.getItem(DraggableMPTTAdmin.storageName) ) } catch (e) { return null } } function expandOrCollapseNode(item) { var show = true if (!item.hasClass("children")) return var itemId = item.data("pk") if (!isExpandedNode(itemId)) { item.removeClass("closed") markNodeAsExpanded(itemId) } else { item.addClass("closed") show = false markNodeAsCollapsed(itemId) } storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes) doToggle(itemId, show) } function collapseTree() { var rlist = $("#result_list") rlist.hide() $("tbody tr", rlist).each(function (i, el) { var marker = $(".tree-node", el) if (marker.hasClass("children")) { var itemId = marker.data("pk") doToggle(itemId, false) marker.addClass("closed") markNodeAsCollapsed(itemId) } }) storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes) rlist.show() return false } function expandTree() { var rlist = $("#result_list") rlist.hide() $("tbody tr", rlist).each(function (i, el) { var marker = $(".tree-node", el) if (marker.hasClass("children")) { var itemId = $(".tree-node", el).data("pk") doToggle(itemId, true) marker.removeClass("closed") markNodeAsExpanded(itemId) } }) storeCollapsedNodes([]) rlist.show() return false } var changelistTab = function (elem, event, direction) { event.preventDefault() elem = $(elem) var ne = direction > 0 ? elem.nextAll(":visible:first") : elem.prevAll(":visible:first") if (ne) { elem.attr("tabindex", -1) ne.attr("tabindex", "0") ne.focus() } } function keyboardNavigationHandler(event) { // On form element? Ignore. if (/textarea|select|input/i.test(event.target.nodeName)) return // console.log('keydown', this, event.keyCode); switch (event.keyCode) { case 40: // down changelistTab(this, event, 1) break case 38: // up changelistTab(this, event, -1) break case 37: // left case 39: // right expandOrCollapseNode($(this).find(".tree-node")) break case 13: // return document.location = $("a", this).attr("href") break default: break } } function addObjectTool(title, handler) { var $a = $("") $a.click(handler) $a.text(title) $a.prependTo(".object-tools").wrap("
  • ") } // Some old browsers do not support JSON.parse (the only thing we require) var jsonParse = JSON.parse || function jsonParse(sJSON) { return eval("(" + sJSON + ")") } DraggableMPTTAdmin = jsonParse( document .getElementById("draggable-admin-context") .getAttribute("data-context") ) addObjectTool(DraggableMPTTAdmin.messages.collapseTree, collapseTree) addObjectTool(DraggableMPTTAdmin.messages.expandTree, expandTree) // fire! var rlist = $("#result_list"), rlist_tbody = rlist.find("tbody") if ($("tbody tr", rlist).length > 1) { rlist_tbody.feinTree() rlist.find(".tree-node").on("click", function (event) { event.preventDefault() event.stopPropagation() expandOrCollapseNode($(this)) }) /* Enable focussing, put focus on first result, add handler for keyboard navigation */ $("tr", rlist).attr("tabindex", -1) $("tbody tr:first", rlist).attr("tabindex", 0).focus() $("tr", rlist).keydown(keyboardNavigationHandler) DraggableMPTTAdmin.collapsedNodes = [] var storedNodes = retrieveCollapsedNodes() if (storedNodes) { for (var i = 0; i < storedNodes.length; i++) { expandOrCollapseNode(treeNode(storedNodes[i])) } } else { if (!DraggableMPTTAdmin.expandTreeByDefault) { collapseTree() } } } })