Compare commits

...

10 commits

Author SHA1 Message Date
e991eda308 Fix minor indentation inconsistency
I recently tweaked my editor settings to make stuff like this more
obvious lol, but those are spaces in tab-indented file
2024-02-18 20:41:42 -08:00
31d033013e Delete unused AltStylesHelper 2024-02-18 20:40:55 -08:00
7efe795edb Move JS library files into a new lib folder
Just sorting things a bit cleaner!
2024-02-18 20:40:16 -08:00
1e11db93cc Delete unused modeling.js.jsx file
I think I cleared this from the outfits/new template a while ago, but
never cleaned up this file, because I was too anxious that I was
correctly identifying all its call sites. But now I'm more confident!
2024-02-18 20:38:15 -08:00
df4ea967c6 Remove now-unnecessary polyfill for the placeholder attribute
Long unnecessary, in fact!
2024-02-18 20:36:45 -08:00
95ff69ee9a Run Prettier on some of our JS assets
The motivation is that I'm about to change one of them to remove a
reference to an old placeholder library, so I want that change to be
clear!
2024-02-18 20:34:55 -08:00
0d23412fba Merge pet_query.js into its only call site 2024-02-18 20:32:24 -08:00
496b517e74 Delete unused Javascript libraries
At least, they seem unused to me on a quick audit! The scriptaculous
stuff has long been replaced by jQuery UI equivalents. (Wow, so many
generations of libraries! lol)
2024-02-18 20:30:08 -08:00
d39e7cea81 Move fundraising models into the Fundraising module
This was mostly straightforward it seems, whew!
2024-02-18 20:29:31 -08:00
82be7fe301 Move most fundraising files into a Fundraising module
Mostly this is just me testing out what it would look like to
modularize the app more… I've noticed that some concerns, like
fundraising, are just not relevant to most of the app, and being able
to lock them away inside subfolders feels like it'll help tidy up
long folder lists.

Notably, I haven't touched the models case yet, because I worry that
might be a bit more complex, whereas everything else seems pretty
well-isolated? We'll try it out!
2024-02-18 20:12:14 -08:00
62 changed files with 1012 additions and 9388 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,20 +1,20 @@
(function () { (function () {
var CSRFProtection; var CSRFProtection;
var token = $('meta[name="csrf-token"]').attr('content'); var token = $('meta[name="csrf-token"]').attr("content");
if (token) { if (token) {
CSRFProtection = function(xhr, settings) { CSRFProtection = function (xhr, settings) {
var sendToken = ( var sendToken =
(typeof settings.useCSRFProtection === 'undefined') // default to true typeof settings.useCSRFProtection === "undefined" || // default to true
|| settings.useCSRFProtection); settings.useCSRFProtection;
if (sendToken) { if (sendToken) {
xhr.setRequestHeader('X-CSRF-Token', token); xhr.setRequestHeader("X-CSRF-Token", token);
} }
} };
} else { } else {
CSRFProtection = $.noop; CSRFProtection = $.noop;
} }
$.ajaxSetup({ $.ajaxSetup({
beforeSend: CSRFProtection beforeSend: CSRFProtection,
}); });
})(); })();

View file

@ -1,3 +1,3 @@
document.getElementById('locale').addEventListener('change', function() { document.getElementById("locale").addEventListener("change", function () {
document.getElementById('locale-form').submit(); document.getElementById("locale-form").submit();
}); });

View file

@ -1,2 +0,0 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -6,7 +6,7 @@
} }
function hangersInit() { function hangersInit() {
for(var i = 0; i < hangersInitCallbacks.length; i++) { for (var i = 0; i < hangersInitCallbacks.length; i++) {
hangersInitCallbacks[i](); hangersInitCallbacks[i]();
} }
} }
@ -19,33 +19,33 @@
var hangerGroups = []; var hangerGroups = [];
$('div.closet-hangers-group').each(function () { $("div.closet-hangers-group").each(function () {
var el = $(this); var el = $(this);
var lists = []; var lists = [];
el.find('div.closet-list').each(function () { el.find("div.closet-list").each(function () {
var el = $(this); var el = $(this);
var id = el.attr('data-id'); var id = el.attr("data-id");
if(id) { if (id) {
lists[lists.length] = { lists[lists.length] = {
id: parseInt(id, 10), id: parseInt(id, 10),
label: el.find('h4').text() label: el.find("h4").text(),
} };
} }
}); });
hangerGroups[hangerGroups.length] = { hangerGroups[hangerGroups.length] = {
label: el.find('h3').text(), label: el.find("h3").text(),
lists: lists, lists: lists,
owned: (el.attr('data-owned') == 'true') owned: el.attr("data-owned") == "true",
}; };
}); });
$('div.closet-hangers-group span.toggle').live('click', function () { $("div.closet-hangers-group span.toggle").live("click", function () {
$(this).closest('.closet-hangers-group').toggleClass('hidden'); $(this).closest(".closet-hangers-group").toggleClass("hidden");
}); });
var hangersElQuery = '#closet-hangers'; var hangersElQuery = "#closet-hangers";
var hangersEl = $(hangersElQuery); var hangersEl = $(hangersElQuery);
/* /*
@ -54,8 +54,8 @@
*/ */
$('#toggle-compare').click(function () { $("#toggle-compare").click(function () {
hangersEl.toggleClass('comparing'); hangersEl.toggleClass("comparing");
}); });
/* /*
@ -65,7 +65,7 @@
*/ */
var body = $(document.body).addClass("js"); var body = $(document.body).addClass("js");
if(!body.hasClass("current-user")) return false; if (!body.hasClass("current-user")) return false;
// When we get hangers HTML, add the controls. We do this in JS rather than // When we get hangers HTML, add the controls. We do this in JS rather than
// in the HTML for caching, since otherwise the requests can take forever. // in the HTML for caching, since otherwise the requests can take forever.
@ -94,32 +94,32 @@
// Ooh, this part is weird. We only want the name to be linked, so // Ooh, this part is weird. We only want the name to be linked, so
// lift everything else out. // lift everything else out.
var checkboxId = 'hanger-selected-' + hangerId; var checkboxId = "hanger-selected-" + hangerId;
var label = $('<label />', {'for': checkboxId}); var label = $("<label />", { for: checkboxId });
var link = hangerEl.children('a'); var link = hangerEl.children("a");
link.children(':not(.name)').detach().appendTo(label); link.children(":not(.name)").detach().appendTo(label);
link.detach().appendTo(label); link.detach().appendTo(label);
var checkbox = $('<input />', { var checkbox = $("<input />", {
type: 'checkbox', type: "checkbox",
id: checkboxId id: checkboxId,
}).appendTo(hangerEl); }).appendTo(hangerEl);
label.appendTo(hangerEl); label.appendTo(hangerEl);
// I don't usually like to _blank things, but it's too easy to click // I don't usually like to _blank things, but it's too easy to click
// the text when you didn't mean to and lose your selection work. // the text when you didn't mean to and lose your selection work.
link.attr('target', '_blank'); link.attr("target", "_blank");
$.tmpl("updateFormTmpl", { $.tmpl("updateFormTmpl", {
user_id: currentUserId, user_id: currentUserId,
closet_hanger_id: hangerId, closet_hanger_id: hangerId,
quantity: quantity, quantity: quantity,
list_id: listId, list_id: listId,
owned: owned owned: owned,
}).appendTo(quantityEl); }).appendTo(quantityEl);
$.tmpl("destroyFormTmpl", { $.tmpl("destroyFormTmpl", {
user_id: currentUserId, user_id: currentUserId,
closet_hanger_id: hangerId closet_hanger_id: hangerId,
}).appendTo(hangerEl); }).appendTo(hangerEl);
}); });
}); });
@ -127,7 +127,7 @@
}); });
$.fn.liveDraggable = function (opts) { $.fn.liveDraggable = function (opts) {
this.live("mouseover", function() { this.live("mouseover", function () {
if (!$(this).data("init")) { if (!$(this).data("init")) {
$(this).data("init", true).draggable(opts); $(this).data("init", true).draggable(opts);
} }
@ -135,52 +135,59 @@
}; };
$.fn.disableForms = function () { $.fn.disableForms = function () {
return this.data("formsDisabled", true).find("input").attr("disabled", "disabled").end(); return this.data("formsDisabled", true)
} .find("input")
.attr("disabled", "disabled")
.end();
};
$.fn.enableForms = function () { $.fn.enableForms = function () {
return this.data("formsDisabled", false).find("input").removeAttr("disabled").end(); return this.data("formsDisabled", false)
} .find("input")
.removeAttr("disabled")
.end();
};
$.fn.hasChanged = function () { $.fn.hasChanged = function () {
return this.attr('data-previous-value') != this.val(); return this.attr("data-previous-value") != this.val();
} };
$.fn.revertValue = function () { $.fn.revertValue = function () {
return this.each(function () { return this.each(function () {
var el = $(this); var el = $(this);
el.val(el.attr('data-previous-value')); el.val(el.attr("data-previous-value"));
}); });
} };
$.fn.storeValue = function () { $.fn.storeValue = function () {
return this.each(function () { return this.each(function () {
var el = $(this); var el = $(this);
el.attr('data-previous-value', el.val()); el.attr("data-previous-value", el.val());
}); });
} };
$.fn.insertIntoSortedList = function (list, compare) { $.fn.insertIntoSortedList = function (list, compare) {
var newChild = this, inserted = false; var newChild = this,
inserted = false;
list.children().each(function () { list.children().each(function () {
if(compare(newChild, $(this)) < 1) { if (compare(newChild, $(this)) < 1) {
newChild.insertBefore(this); newChild.insertBefore(this);
inserted = true; inserted = true;
return false; return false;
} }
}); });
if(!inserted) newChild.appendTo(list); if (!inserted) newChild.appendTo(list);
return this; return this;
} };
function handleSaveError(xhr, action) { function handleSaveError(xhr, action) {
try { try {
var data = $.parseJSON(xhr.responseText); var data = $.parseJSON(xhr.responseText);
} catch(e) { } catch (e) {
var data = {}; var data = {};
} }
if(typeof data.errors != 'undefined') { if (typeof data.errors != "undefined") {
$.jGrowl("Error " + action + ": " + data.errors.join(", ")); $.jGrowl("Error " + action + ": " + data.errors.join(", "));
} else { } else {
$.jGrowl("We had trouble " + action + " just now. Try again?"); $.jGrowl("We had trouble " + action + " just now. Try again?");
@ -188,75 +195,85 @@
} }
function objectRemoved(objectWrapper) { function objectRemoved(objectWrapper) {
objectWrapper.hide(250, function() { objectWrapper.hide(250, function () {
objectWrapper.remove(); objectWrapper.remove();
updateBulkActions(); updateBulkActions();
}); });
} }
function compareItemsByName(a, b) { function compareItemsByName(a, b) {
return a.find('span.name').text().localeCompare(b.find('span.name').text()); return a.find("span.name").text().localeCompare(b.find("span.name").text());
} }
function findList(owned, id, item) { function findList(owned, id, item) {
if(id) { if (id) {
return $('#closet-list-' + id); return $("#closet-list-" + id);
} else { } else {
return $("div.closet-hangers-group[data-owned=" + owned + "] div.closet-list.unlisted"); return $(
"div.closet-hangers-group[data-owned=" +
owned +
"] div.closet-list.unlisted",
);
} }
} }
function updateListHangersCount(el) { function updateListHangersCount(el) {
el.attr('data-hangers-count', el.find('div.object').length); el.attr("data-hangers-count", el.find("div.object").length);
} }
function moveItemToList(item, owned, listId) { function moveItemToList(item, owned, listId) {
var newList = findList(owned, listId, item); var newList = findList(owned, listId, item);
var oldList = item.closest('div.closet-list'); var oldList = item.closest("div.closet-list");
var hangersWrapper = newList.find('div.closet-list-hangers'); var hangersWrapper = newList.find("div.closet-list-hangers");
item.insertIntoSortedList(hangersWrapper, compareItemsByName); item.insertIntoSortedList(hangersWrapper, compareItemsByName);
updateListHangersCount(oldList); updateListHangersCount(oldList);
updateListHangersCount(newList); updateListHangersCount(newList);
} }
function submitUpdateForm(form) { function submitUpdateForm(form) {
if(form.data('loading')) return false; if (form.data("loading")) return false;
var quantityEl = form.children("input[name=closet_hanger\[quantity\]]"); var quantityEl = form.children("input[name=closet_hanger[quantity]]");
var ownedEl = form.children("input[name=closet_hanger\[owned\]]"); var ownedEl = form.children("input[name=closet_hanger[owned]]");
var listEl = form.children("input[name=closet_hanger\[list_id\]]"); var listEl = form.children("input[name=closet_hanger[list_id]]");
var listChanged = ownedEl.hasChanged() || listEl.hasChanged(); var listChanged = ownedEl.hasChanged() || listEl.hasChanged();
if(listChanged || quantityEl.hasChanged()) { if (listChanged || quantityEl.hasChanged()) {
var objectWrapper = form.closest(".object").addClass("loading"); var objectWrapper = form.closest(".object").addClass("loading");
var newQuantity = quantityEl.val(); var newQuantity = quantityEl.val();
var quantitySpan = objectWrapper.find(".quantity span").text(newQuantity); var quantitySpan = objectWrapper.find(".quantity span").text(newQuantity);
objectWrapper.attr('data-quantity', newQuantity); objectWrapper.attr("data-quantity", newQuantity);
var data = form.serialize(); // get data before disabling inputs var data = form.serialize(); // get data before disabling inputs
objectWrapper.disableForms(); objectWrapper.disableForms();
form.data('loading', true); form.data("loading", true);
if(listChanged) moveItemToList(objectWrapper, ownedEl.val(), listEl.val()); if (listChanged)
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
$.ajax({ $.ajax({
url: form.attr("action") + ".json", url: form.attr("action") + ".json",
type: "post", type: "post",
data: data, data: data,
dataType: "json", dataType: "json",
complete: function (data) { complete: function (data) {
if(quantityEl.val() == 0) { if (quantityEl.val() == 0) {
objectRemoved(objectWrapper); objectRemoved(objectWrapper);
} else { } else {
objectWrapper.removeClass("loading").enableForms(); objectWrapper.removeClass("loading").enableForms();
} }
form.data('loading', false); form.data("loading", false);
}, },
success: function () { success: function () {
// Now that the move was successful, let's merge it with any // Now that the move was successful, let's merge it with any
// conflicting hangers // conflicting hangers
var id = objectWrapper.attr("data-item-id"); var id = objectWrapper.attr("data-item-id");
var conflictingHanger = findList(ownedEl.val(), listEl.val(), objectWrapper). var conflictingHanger = findList(
find("div[data-item-id=" + id + "]").not(objectWrapper); ownedEl.val(),
if(conflictingHanger.length) { listEl.val(),
objectWrapper,
)
.find("div[data-item-id=" + id + "]")
.not(objectWrapper);
if (conflictingHanger.length) {
var conflictingQuantity = parseInt( var conflictingQuantity = parseInt(
conflictingHanger.attr('data-quantity'), conflictingHanger.attr("data-quantity"),
10 10,
); );
var currentQuantity = parseInt(newQuantity, 10); var currentQuantity = parseInt(newQuantity, 10);
@ -265,7 +282,7 @@
quantitySpan.text(mergedQuantity); quantitySpan.text(mergedQuantity);
quantityEl.val(mergedQuantity); quantityEl.val(mergedQuantity);
objectWrapper.attr('data-quantity', mergedQuantity); objectWrapper.attr("data-quantity", mergedQuantity);
conflictingHanger.remove(); conflictingHanger.remove();
} }
@ -280,84 +297,94 @@
quantityEl.revertValue(); quantityEl.revertValue();
ownedEl.revertValue(); ownedEl.revertValue();
listEl.revertValue(); listEl.revertValue();
if(listChanged) moveItemToList(objectWrapper, ownedEl.val(), listEl.val()); if (listChanged)
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
quantitySpan.text(quantityEl.val()); quantitySpan.text(quantityEl.val());
handleSaveError(xhr, "updating the quantity"); handleSaveError(xhr, "updating the quantity");
} },
}); });
} }
} }
$(hangersElQuery + ' form.closet-hanger-update').live('submit', function (e) { $(hangersElQuery + " form.closet-hanger-update").live("submit", function (e) {
e.preventDefault(); e.preventDefault();
submitUpdateForm($(this)); submitUpdateForm($(this));
}); });
function editableInputs() { function editableInputs() {
return $(hangersElQuery).find( return $(hangersElQuery).find(
'input[name=closet_hanger\[quantity\]], ' + "input[name=closet_hanger[quantity]], " +
'input[name=closet_hanger\[owned\]], ' + "input[name=closet_hanger[owned]], " +
'input[name=closet_hanger\[list_id\]]' "input[name=closet_hanger[list_id]]",
) );
} }
$(hangersElQuery + 'input[name=closet_hanger\[quantity\]]').live('change', function () { $(hangersElQuery + "input[name=closet_hanger[quantity]]")
submitUpdateForm($(this).parent()); .live("change", function () {
}).storeValue(); submitUpdateForm($(this).parent());
})
.storeValue();
onHangersInit(function () { onHangersInit(function () {
editableInputs().storeValue(); editableInputs().storeValue();
}); });
$(hangersElQuery + ' div.object').live('mouseleave', function () { $(hangersElQuery + " div.object")
submitUpdateForm($(this).find('form.closet-hanger-update')); .live("mouseleave", function () {
}).liveDraggable({ submitUpdateForm($(this).find("form.closet-hanger-update"));
appendTo: '#closet-hangers', })
distance: 20, .liveDraggable({
helper: "clone", appendTo: "#closet-hangers",
revert: "invalid" distance: 20,
}); helper: "clone",
revert: "invalid",
$(hangersElQuery + " form.closet-hanger-destroy").live("submit", function (e) {
e.preventDefault();
var form = $(this);
var button = form.children("input[type=submit]").val("Removing…");
var objectWrapper = form.closest(".object").addClass("loading");
var data = form.serialize(); // get data before disabling inputs
objectWrapper.addClass("loading").disableForms();
$.ajax({
url: form.attr("action") + ".json",
type: "post",
data: data,
dataType: "json",
complete: function () {
button.val("Remove");
},
success: function () {
objectRemoved(objectWrapper);
},
error: function () {
objectWrapper.removeClass("loading").enableForms();
$.jGrowl("Error removing item. Try again?");
}
}); });
});
$(hangersElQuery + " .select-all").live("click", function(e) { $(hangersElQuery + " form.closet-hanger-destroy").live(
var checkboxes = $(this).closest(".closet-list").find(".object input[type=checkbox]"); "submit",
function (e) {
e.preventDefault();
var form = $(this);
var button = form.children("input[type=submit]").val("Removing…");
var objectWrapper = form.closest(".object").addClass("loading");
var data = form.serialize(); // get data before disabling inputs
objectWrapper.addClass("loading").disableForms();
$.ajax({
url: form.attr("action") + ".json",
type: "post",
data: data,
dataType: "json",
complete: function () {
button.val("Remove");
},
success: function () {
objectRemoved(objectWrapper);
},
error: function () {
objectWrapper.removeClass("loading").enableForms();
$.jGrowl("Error removing item. Try again?");
},
});
},
);
$(hangersElQuery + " .select-all").live("click", function (e) {
var checkboxes = $(this)
.closest(".closet-list")
.find(".object input[type=checkbox]");
var allChecked = true; var allChecked = true;
checkboxes.each(function() { checkboxes.each(function () {
if (!this.checked) { if (!this.checked) {
allChecked = false; allChecked = false;
return false; return false;
} }
}); });
checkboxes.attr('checked', !allChecked); checkboxes.attr("checked", !allChecked);
updateBulkActions(); // setting the checked prop doesn't fire change events updateBulkActions(); // setting the checked prop doesn't fire change events
}); });
function getCheckboxes() { function getCheckboxes() {
@ -366,30 +393,38 @@
function getCheckedIds() { function getCheckedIds() {
var checkedIds = []; var checkedIds = [];
getCheckboxes().filter(':checked').each(function() { getCheckboxes()
if (this.checked) checkedIds.push(this.id); .filter(":checked")
}); .each(function () {
if (this.checked) checkedIds.push(this.id);
});
return checkedIds; return checkedIds;
} }
getCheckboxes().live("change", updateBulkActions); getCheckboxes().live("change", updateBulkActions);
function updateBulkActions() { function updateBulkActions() {
var checkedCount = getCheckboxes().filter(':checked').length; var checkedCount = getCheckboxes().filter(":checked").length;
$('.bulk-actions').attr('data-target-count', checkedCount); $(".bulk-actions").attr("data-target-count", checkedCount);
$('.bulk-actions-target-count').text(checkedCount); $(".bulk-actions-target-count").text(checkedCount);
} }
$(".bulk-actions-move-all").bind("submit", function(e) { $(".bulk-actions-move-all").bind("submit", function (e) {
// TODO: DRY // TODO: DRY
e.preventDefault(); e.preventDefault();
var form = $(this); var form = $(this);
var data = form.serializeArray(); var data = form.serializeArray();
data.push({name: "return_to", value: window.location.pathname + window.location.search}); data.push({
name: "return_to",
value: window.location.pathname + window.location.search,
});
var checkedBoxes = getCheckboxes().filter(':checked'); var checkedBoxes = getCheckboxes().filter(":checked");
checkedBoxes.each(function() { checkedBoxes.each(function () {
data.push({name: "ids[]", value: $(this).closest('.object').attr('data-id')}); data.push({
name: "ids[]",
value: $(this).closest(".object").attr("data-id"),
});
}); });
$.ajax({ $.ajax({
@ -398,32 +433,38 @@
data: data, data: data,
success: function (html) { success: function (html) {
var doc = $(html); var doc = $(html);
hangersEl.html( doc.find('#closet-hangers').html() ); hangersEl.html(doc.find("#closet-hangers").html());
hangersInit(); hangersInit();
updateBulkActions(); // don't want to maintain checked; deselect em all updateBulkActions(); // don't want to maintain checked; deselect em all
doc.find('.flash').hide().insertBefore(hangersEl).show(500).delay(5000).hide(250); doc
.find(".flash")
.hide()
.insertBefore(hangersEl)
.show(500)
.delay(5000)
.hide(250);
itemsSearchField.val(""); itemsSearchField.val("");
}, },
error: function (xhr) { error: function (xhr) {
handleSaveError(xhr, "moving these items"); handleSaveError(xhr, "moving these items");
} },
}); });
}); });
$(".bulk-actions-remove-all").bind("submit", function(e) { $(".bulk-actions-remove-all").bind("submit", function (e) {
e.preventDefault(); e.preventDefault();
var form = $(this); var form = $(this);
var hangerIds = []; var hangerIds = [];
var checkedBoxes = getCheckboxes().filter(':checked'); var checkedBoxes = getCheckboxes().filter(":checked");
var hangerEls = $(); var hangerEls = $();
checkedBoxes.each(function() { checkedBoxes.each(function () {
hangerEls = hangerEls.add($(this).closest('.object')); hangerEls = hangerEls.add($(this).closest(".object"));
}); });
hangerEls.each(function() { hangerEls.each(function () {
hangerIds.push($(this).attr('data-id')); hangerIds.push($(this).attr("data-id"));
}); });
$.ajax({ $.ajax({
url: form.attr("action") + ".json?" + $.param({ids: hangerIds}), url: form.attr("action") + ".json?" + $.param({ ids: hangerIds }),
type: "delete", type: "delete",
dataType: "json", dataType: "json",
success: function () { success: function () {
@ -431,20 +472,20 @@
}, },
error: function () { error: function () {
$.jGrowl("Error removing items. Try again?"); $.jGrowl("Error removing items. Try again?");
} },
}); });
}); });
$(".bulk-actions-deselect-all").bind("click", function(e) { $(".bulk-actions-deselect-all").bind("click", function (e) {
getCheckboxes().filter(':checked').attr('checked', false); getCheckboxes().filter(":checked").attr("checked", false);
updateBulkActions(); updateBulkActions();
}); });
// hahaha, nasty hacks to make stickUp use our old jQuery // hahaha, nasty hacks to make stickUp use our old jQuery
$.fn.on = $.fn.bind; $.fn.on = $.fn.bind;
$(function() { $(function () {
$('.bulk-actions').stickUp(); $(".bulk-actions").stickUp();
}); });
function maintainCheckboxes(fn) { function maintainCheckboxes(fn) {
@ -452,7 +493,7 @@
fn(); fn();
checkedIds.forEach(function(id) { checkedIds.forEach(function (id) {
document.getElementById(id).checked = true; document.getElementById(id).checked = true;
}); });
updateBulkActions(); updateBulkActions();
@ -464,14 +505,12 @@
*/ */
$('input, textarea').placeholder();
var itemsSearchForm = $("#closet-hangers-items-search[data-current-user-id]"); var itemsSearchForm = $("#closet-hangers-items-search[data-current-user-id]");
var itemsSearchField = itemsSearchForm.children("input[name=q]"); var itemsSearchField = itemsSearchForm.children("input[name=q]");
itemsSearchField.autocomplete({ itemsSearchField.autocomplete({
select: function (e, ui) { select: function (e, ui) {
if(ui.item.is_item) { if (ui.item.is_item) {
// Let the autocompleter finish up this search before starting a new one // Let the autocompleter finish up this search before starting a new one
setTimeout(function () { setTimeout(function () {
itemsSearchField.autocomplete("search", ui.item); itemsSearchField.autocomplete("search", ui.item);
@ -484,107 +523,141 @@
var closetHanger = { var closetHanger = {
owned: group.owned, owned: group.owned,
list_id: ui.item.list ? ui.item.list.id : '' list_id: ui.item.list ? ui.item.list.id : "",
}; };
if(!item.hasHanger) closetHanger.quantity = 1; if (!item.hasHanger) closetHanger.quantity = 1;
$.ajax({ $.ajax({
url: "/user/" + itemsSearchForm.data("current-user-id") + "/items/" + item.id + "/closet_hangers", url:
"/user/" +
itemsSearchForm.data("current-user-id") +
"/items/" +
item.id +
"/closet_hangers",
type: "post", type: "post",
data: {closet_hanger: closetHanger, return_to: window.location.pathname + window.location.search}, data: {
closet_hanger: closetHanger,
return_to: window.location.pathname + window.location.search,
},
complete: function () { complete: function () {
itemsSearchField.removeClass("loading"); itemsSearchField.removeClass("loading");
}, },
success: function (html) { success: function (html) {
var doc = $(html); var doc = $(html);
maintainCheckboxes(function() { maintainCheckboxes(function () {
hangersEl.html( doc.find('#closet-hangers').html() ); hangersEl.html(doc.find("#closet-hangers").html());
hangersInit(); hangersInit();
}); });
doc.find('.flash').hide().insertBefore(hangersEl).show(500).delay(5000).hide(250); doc
.find(".flash")
.hide()
.insertBefore(hangersEl)
.show(500)
.delay(5000)
.hide(250);
itemsSearchField.val(""); itemsSearchField.val("");
}, },
error: function (xhr) { error: function (xhr) {
handleSaveError(xhr, "adding the item"); handleSaveError(xhr, "adding the item");
} },
}); });
} }
}, },
source: function (input, callback) { source: function (input, callback) {
if(typeof input.term == 'string') { // user-typed query if (typeof input.term == "string") {
// user-typed query
$.getJSON("/items.json?q=" + input.term, function (data) { $.getJSON("/items.json?q=" + input.term, function (data) {
var output = []; var output = [];
var items = data.items; var items = data.items;
for(var i in items) { for (var i in items) {
items[i].label = items[i].name; items[i].label = items[i].name;
items[i].is_item = true; items[i].is_item = true;
output[output.length] = items[i]; output[output.length] = items[i];
} }
callback(output); callback(output);
}); });
} else { // item was chosen, now choose a group to insert } else {
var groupInserts = [], group; // item was chosen, now choose a group to insert
var item = input.term, itemEl, occupiedGroups, hasHanger; var groupInserts = [],
for(var i in hangerGroups) { group;
var item = input.term,
itemEl,
occupiedGroups,
hasHanger;
for (var i in hangerGroups) {
group = hangerGroups[i]; group = hangerGroups[i];
itemEl = $('div.closet-hangers-group[data-owned=' + group.owned + '] div.object[data-item-id=' + item.id + ']'); itemEl = $(
occupiedGroups = itemEl.closest('.closet-list'); "div.closet-hangers-group[data-owned=" +
hasHanger = occupiedGroups.filter('.unlisted').length > 0; group.owned +
"] div.object[data-item-id=" +
item.id +
"]",
);
occupiedGroups = itemEl.closest(".closet-list");
hasHanger = occupiedGroups.filter(".unlisted").length > 0;
groupInserts[groupInserts.length] = { groupInserts[groupInserts.length] = {
group: group, group: group,
item: item, item: item,
label: item.label, label: item.label,
hasHanger: hasHanger hasHanger: hasHanger,
} };
for(var i = 0; i < group.lists.length; i++) { for (var i = 0; i < group.lists.length; i++) {
hasHanger = occupiedGroups. hasHanger =
filter("[data-id=" + group.lists[i].id + "]").length > 0; occupiedGroups.filter("[data-id=" + group.lists[i].id + "]")
.length > 0;
groupInserts[groupInserts.length] = { groupInserts[groupInserts.length] = {
group: group, group: group,
item: item, item: item,
label: item.label, label: item.label,
list: group.lists[i], list: group.lists[i],
hasHanger: hasHanger hasHanger: hasHanger,
} };
} }
} }
callback(groupInserts); callback(groupInserts);
} }
} },
}); });
var autocompleter = itemsSearchField.data("autocomplete"); var autocompleter = itemsSearchField.data("autocomplete");
autocompleter._renderItem = function( ul, item ) { autocompleter._renderItem = function (ul, item) {
var li = $("<li></li>").data("item.autocomplete", item); var li = $("<li></li>").data("item.autocomplete", item);
if(item.is_item) { // these are items from the server if (item.is_item) {
$('#autocomplete-item-tmpl').tmpl({item_name: item.label}).appendTo(li); // these are items from the server
} else if(item.list) { // these are list inserts $("#autocomplete-item-tmpl").tmpl({ item_name: item.label }).appendTo(li);
} else if (item.list) {
// these are list inserts
var listName = item.list.label; var listName = item.list.label;
if(item.hasHanger) { if (item.hasHanger) {
$('#autocomplete-already-in-collection-tmpl'). $("#autocomplete-already-in-collection-tmpl")
tmpl({collection_name: listName}).appendTo(li); .tmpl({ collection_name: listName })
.appendTo(li);
} else { } else {
$('#autocomplete-add-to-list-tmpl').tmpl({list_name: listName}). $("#autocomplete-add-to-list-tmpl")
appendTo(li); .tmpl({ list_name: listName })
.appendTo(li);
} }
li.addClass("closet-list-autocomplete-item"); li.addClass("closet-list-autocomplete-item");
} else { // these are group inserts } else {
// these are group inserts
var groupName = item.group.label; var groupName = item.group.label;
if(!item.hasHanger) { if (!item.hasHanger) {
$('#autocomplete-add-to-group-tmpl'). $("#autocomplete-add-to-group-tmpl")
tmpl({group_name: groupName.replace(/\s+$/, '')}).appendTo(li); .tmpl({ group_name: groupName.replace(/\s+$/, "") })
.appendTo(li);
} else { } else {
$('#autocomplete-already-in-collection-tmpl'). $("#autocomplete-already-in-collection-tmpl")
tmpl({collection_name: groupName}).appendTo(li); .tmpl({ collection_name: groupName })
.appendTo(li);
} }
li.addClass('closet-hangers-group-autocomplete-item'); li.addClass("closet-hangers-group-autocomplete-item");
} }
return li.appendTo(ul); return li.appendTo(ul);
} };
/* /*
@ -592,48 +665,54 @@
*/ */
var contactEl = $('#closet-hangers-contact'); var contactEl = $("#closet-hangers-contact");
var contactForm = contactEl.children('form'); var contactForm = contactEl.children("form");
var contactField = contactForm.children('select'); var contactField = contactForm.children("select");
var contactAddOption = $('<option/>', var contactAddOption = $("<option/>", {
{text: contactField.attr('data-new-text'), value: -1}); text: contactField.attr("data-new-text"),
value: -1,
});
contactAddOption.appendTo(contactField); contactAddOption.appendTo(contactField);
var currentUserId = $('meta[name=current-user-id]').attr('content'); var currentUserId = $("meta[name=current-user-id]").attr("content");
function submitContactForm() { function submitContactForm() {
var data = contactForm.serialize(); var data = contactForm.serialize();
contactForm.disableForms(); contactForm.disableForms();
$.ajax({ $.ajax({
url: contactForm.attr('action') + '.json', url: contactForm.attr("action") + ".json",
type: 'post', type: "post",
data: data, data: data,
dataType: 'json', dataType: "json",
complete: function () { complete: function () {
contactForm.enableForms(); contactForm.enableForms();
}, },
error: function (xhr) { error: function (xhr) {
handleSaveError(xhr, 'saving Neopets username'); handleSaveError(xhr, "saving Neopets username");
} },
}); });
} }
contactField.change(function(e) { contactField.change(function (e) {
if (contactField.val() < 0) { if (contactField.val() < 0) {
var newUsername = $.trim(prompt(contactField.attr('data-new-prompt'), '')); var newUsername = $.trim(
prompt(contactField.attr("data-new-prompt"), ""),
);
if (newUsername) { if (newUsername) {
$.ajax({ $.ajax({
url: '/user/' + currentUserId + '/neopets-connections', url: "/user/" + currentUserId + "/neopets-connections",
type: 'POST', type: "POST",
data: {neopets_connection: {neopets_username: newUsername}}, data: { neopets_connection: { neopets_username: newUsername } },
dataType: 'json', dataType: "json",
success: function(connection) { success: function (connection) {
var newOption = $('<option/>', {text: newUsername, var newOption = $("<option/>", {
value: connection.id}) text: newUsername,
value: connection.id,
});
newOption.insertBefore(contactAddOption); newOption.insertBefore(contactAddOption);
contactField.val(connection.id); contactField.val(connection.id);
submitContactForm(); submitContactForm();
} },
}); });
} }
} else { } else {
@ -647,8 +726,8 @@
*/ */
$('input[type=submit][data-confirm]').live('click', function (e) { $("input[type=submit][data-confirm]").live("click", function (e) {
if(!confirm(this.getAttribute('data-confirm'))) e.preventDefault(); if (!confirm(this.getAttribute("data-confirm"))) e.preventDefault();
}); });
/* /*
@ -658,23 +737,30 @@
*/ */
onHangersInit(function () { onHangersInit(function () {
$('div.closet-list').droppable({ $("div.closet-list").droppable({
accept: 'div.object', accept: "div.object",
activate: function () { activate: function () {
$(this).find('.closet-list-content').animate({opacity: 0, height: 100}, 250); $(this)
.find(".closet-list-content")
.animate({ opacity: 0, height: 100 }, 250);
}, },
activeClass: 'droppable-active', activeClass: "droppable-active",
deactivate: function () { deactivate: function () {
$(this).find('.closet-list-content').css('height', 'auto').animate({opacity: 1}, 250); $(this)
.find(".closet-list-content")
.css("height", "auto")
.animate({ opacity: 1 }, 250);
}, },
drop: function (e, ui) { drop: function (e, ui) {
var form = ui.draggable.find('form.closet-hanger-update'); var form = ui.draggable.find("form.closet-hanger-update");
form.find('input[name=closet_hanger\[list_id\]]'). form
val(this.getAttribute('data-id')); .find("input[name=closet_hanger[list_id]]")
form.find('input[name=closet_hanger\[owned\]]'). .val(this.getAttribute("data-id"));
val($(this).closest('.closet-hangers-group').attr('data-owned')); form
.find("input[name=closet_hanger[owned]]")
.val($(this).closest(".closet-hangers-group").attr("data-owned"));
submitUpdateForm(form); submitUpdateForm(form);
} },
}); });
}); });
@ -685,16 +771,21 @@
*/ */
function updateVisibilityDescription() { function updateVisibilityDescription() {
var descriptions = $(this).closest('.visibility-form'). var descriptions = $(this)
find('ul.visibility-descriptions'); .closest(".visibility-form")
.find("ul.visibility-descriptions");
descriptions.children('li.current').removeClass('current'); descriptions.children("li.current").removeClass("current");
descriptions.children('li[data-id=' + $(this).val() + ']').addClass('current'); descriptions
.children("li[data-id=" + $(this).val() + "]")
.addClass("current");
} }
function visibilitySelects() { return $('form.visibility-form select') } function visibilitySelects() {
return $("form.visibility-form select");
}
visibilitySelects().live('change', updateVisibilityDescription); visibilitySelects().live("change", updateVisibilityDescription);
onHangersInit(function () { onHangersInit(function () {
visibilitySelects().each(updateVisibilityDescription); visibilitySelects().each(updateVisibilityDescription);
@ -706,8 +797,8 @@
*/ */
$('#toggle-help').click(function () { $("#toggle-help").click(function () {
$('#closet-hangers-help').toggleClass('hidden'); $("#closet-hangers-help").toggleClass("hidden");
}); });
/* /*
@ -716,11 +807,13 @@
*/ */
$('#closet-hangers-share-box').mouseover(function () { $("#closet-hangers-share-box")
$(this).focus(); .mouseover(function () {
}).mouseout(function () { $(this).focus();
$(this).blur(); })
}); .mouseout(function () {
$(this).blur();
});
/* /*
@ -730,4 +823,3 @@
hangersInit(); hangersInit();
})(); })();

View file

@ -1,8 +1,8 @@
(function () { (function () {
function setChecked() { function setChecked() {
var el = $(this); var el = $(this);
el.closest('li').toggleClass('checked', el.is(':checked')); el.closest("li").toggleClass("checked", el.is(":checked"));
} }
$('#petpage-closet-lists input').click(setChecked).each(setChecked); $("#petpage-closet-lists input").click(setChecked).each(setChecked);
})(); })();

View file

@ -1,965 +0,0 @@
// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = { };
Autocompleter.Base = Class.create({
baseInitialize: function(element, update, options) {
element = $(element);
this.element = element;
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
this.oldElementValue = this.element.value;
if(this.setOptions)
this.setOptions(options);
else
this.options = options || { };
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {
setHeight: false,
offsetTop: element.offsetHeight
});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
// Force carriage returns as token delimiters anyway
if (!this.options.tokens.include('\n'))
this.options.tokens.push('\n');
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(Prototype.Browser.IE) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--;
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++;
else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = $(selectedElement).select('.' + this.options.select) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var bounds = this.getTokenBounds();
if (bounds[0] != -1) {
var newValue = this.element.value.substr(0, bounds[0]);
var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value + this.element.value.substr(bounds[1]);
} else {
this.element.value = value;
}
this.oldElementValue = this.element.value;
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.down());
if(this.update.firstChild && this.update.down().childNodes) {
this.entryCount =
this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
if(this.entryCount==1 && this.options.autoSelect) {
this.selectEntry();
this.hide();
} else {
this.render();
}
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
this.tokenBounds = null;
if(this.getToken().length>=this.options.minChars) {
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
this.oldElementValue = this.element.value;
},
getToken: function() {
var bounds = this.getTokenBounds();
return this.element.value.substring(bounds[0], bounds[1]).strip();
},
getTokenBounds: function() {
if (null != this.tokenBounds) return this.tokenBounds;
var value = this.element.value;
if (value.strip().empty()) return [-1, 0];
var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
var offset = (diff == this.oldElementValue.length ? 1 : 0);
var prevTokenPos = -1, nextTokenPos = value.length;
var tp;
for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
if (tp > prevTokenPos) prevTokenPos = tp;
tp = value.indexOf(this.options.tokens[index], diff + offset);
if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
}
return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
}
});
Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
var boundary = Math.min(newS.length, oldS.length);
for (var index = 0; index < boundary; ++index)
if (newS[index] != oldS[index])
return index;
return boundary;
};
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
this.startIndicator();
var entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create(Autocompleter.Base, {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
return "<ul>" + ret.join('') + "</ul>";
}
}, options || { });
}
});
// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
};
Ajax.InPlaceEditor = Class.create({
initialize: function(element, url, options) {
this.url = url;
this.element = element = $(element);
this.prepareOptions();
this._controls = { };
arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
Object.extend(this.options, options || { });
if (!this.options.formId && this.element.id) {
this.options.formId = this.element.id + '-inplaceeditor';
if ($(this.options.formId))
this.options.formId = '';
}
if (this.options.externalControl)
this.options.externalControl = $(this.options.externalControl);
if (!this.options.externalControl)
this.options.externalControlOnly = false;
this._originalBackground = this.element.getStyle('background-color') || 'transparent';
this.element.title = this.options.clickToEditText;
this._boundCancelHandler = this.handleFormCancellation.bind(this);
this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
this._boundFailureHandler = this.handleAJAXFailure.bind(this);
this._boundSubmitHandler = this.handleFormSubmission.bind(this);
this._boundWrapperHandler = this.wrapUp.bind(this);
this.registerListeners();
},
checkForEscapeOrReturn: function(e) {
if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
if (Event.KEY_ESC == e.keyCode)
this.handleFormCancellation(e);
else if (Event.KEY_RETURN == e.keyCode)
this.handleFormSubmission(e);
},
createControl: function(mode, handler, extraClasses) {
var control = this.options[mode + 'Control'];
var text = this.options[mode + 'Text'];
if ('button' == control) {
var btn = document.createElement('input');
btn.type = 'submit';
btn.value = text;
btn.className = 'editor_' + mode + '_button';
if ('cancel' == mode)
btn.onclick = this._boundCancelHandler;
this._form.appendChild(btn);
this._controls[mode] = btn;
} else if ('link' == control) {
var link = document.createElement('a');
link.href = '#';
link.appendChild(document.createTextNode(text));
link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
link.className = 'editor_' + mode + '_link';
if (extraClasses)
link.className += ' ' + extraClasses;
this._form.appendChild(link);
this._controls[mode] = link;
}
},
createEditField: function() {
var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
var fld;
if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
fld = document.createElement('input');
fld.type = 'text';
var size = this.options.size || this.options.cols || 0;
if (0 < size) fld.size = size;
} else {
fld = document.createElement('textarea');
fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
fld.cols = this.options.cols || 40;
}
fld.name = this.options.paramName;
fld.value = text; // No HTML breaks conversion anymore
fld.className = 'editor_field';
if (this.options.submitOnBlur)
fld.onblur = this._boundSubmitHandler;
this._controls.editor = fld;
if (this.options.loadTextURL)
this.loadExternalText();
this._form.appendChild(this._controls.editor);
},
createForm: function() {
var ipe = this;
function addText(mode, condition) {
var text = ipe.options['text' + mode + 'Controls'];
if (!text || condition === false) return;
ipe._form.appendChild(document.createTextNode(text));
};
this._form = $(document.createElement('form'));
this._form.id = this.options.formId;
this._form.addClassName(this.options.formClassName);
this._form.onsubmit = this._boundSubmitHandler;
this.createEditField();
if ('textarea' == this._controls.editor.tagName.toLowerCase())
this._form.appendChild(document.createElement('br'));
if (this.options.onFormCustomization)
this.options.onFormCustomization(this, this._form);
addText('Before', this.options.okControl || this.options.cancelControl);
this.createControl('ok', this._boundSubmitHandler);
addText('Between', this.options.okControl && this.options.cancelControl);
this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
addText('After', this.options.okControl || this.options.cancelControl);
},
destroy: function() {
if (this._oldInnerHTML)
this.element.innerHTML = this._oldInnerHTML;
this.leaveEditMode();
this.unregisterListeners();
},
enterEditMode: function(e) {
if (this._saving || this._editing) return;
this._editing = true;
this.triggerCallback('onEnterEditMode');
if (this.options.externalControl)
this.options.externalControl.hide();
this.element.hide();
this.createForm();
this.element.parentNode.insertBefore(this._form, this.element);
if (!this.options.loadTextURL)
this.postProcessEditField();
if (e) Event.stop(e);
},
enterHover: function(e) {
if (this.options.hoverClassName)
this.element.addClassName(this.options.hoverClassName);
if (this._saving) return;
this.triggerCallback('onEnterHover');
},
getText: function() {
return this.element.innerHTML.unescapeHTML();
},
handleAJAXFailure: function(transport) {
this.triggerCallback('onFailure', transport);
if (this._oldInnerHTML) {
this.element.innerHTML = this._oldInnerHTML;
this._oldInnerHTML = null;
}
},
handleFormCancellation: function(e) {
this.wrapUp();
if (e) Event.stop(e);
},
handleFormSubmission: function(e) {
var form = this._form;
var value = $F(this._controls.editor);
this.prepareSubmission();
var params = this.options.callback(form, value) || '';
if (Object.isString(params))
params = params.toQueryParams();
params.editorId = this.element.id;
if (this.options.htmlResponse) {
var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Updater({ success: this.element }, this.url, options);
} else {
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Request(this.url, options);
}
if (e) Event.stop(e);
},
leaveEditMode: function() {
this.element.removeClassName(this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this._originalBackground;
this.element.show();
if (this.options.externalControl)
this.options.externalControl.show();
this._saving = false;
this._editing = false;
this._oldInnerHTML = null;
this.triggerCallback('onLeaveEditMode');
},
leaveHover: function(e) {
if (this.options.hoverClassName)
this.element.removeClassName(this.options.hoverClassName);
if (this._saving) return;
this.triggerCallback('onLeaveHover');
},
loadExternalText: function() {
this._form.addClassName(this.options.loadingClassName);
this._controls.editor.disabled = true;
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
this._form.removeClassName(this.options.loadingClassName);
var text = transport.responseText;
if (this.options.stripLoadedTextTags)
text = text.stripTags();
this._controls.editor.value = text;
this._controls.editor.disabled = false;
this.postProcessEditField();
}.bind(this),
onFailure: this._boundFailureHandler
});
new Ajax.Request(this.options.loadTextURL, options);
},
postProcessEditField: function() {
var fpc = this.options.fieldPostCreation;
if (fpc)
$(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
},
prepareOptions: function() {
this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
[this._extraDefaultOptions].flatten().compact().each(function(defs) {
Object.extend(this.options, defs);
}.bind(this));
},
prepareSubmission: function() {
this._saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
registerListeners: function() {
this._listeners = { };
var listener;
$H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
listener = this[pair.value].bind(this);
this._listeners[pair.key] = listener;
if (!this.options.externalControlOnly)
this.element.observe(pair.key, listener);
if (this.options.externalControl)
this.options.externalControl.observe(pair.key, listener);
}.bind(this));
},
removeForm: function() {
if (!this._form) return;
this._form.remove();
this._form = null;
this._controls = { };
},
showSaving: function() {
this._oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
this.element.addClassName(this.options.savingClassName);
this.element.style.backgroundColor = this._originalBackground;
this.element.show();
},
triggerCallback: function(cbName, arg) {
if ('function' == typeof this.options[cbName]) {
this.options[cbName](this, arg);
}
},
unregisterListeners: function() {
$H(this._listeners).each(function(pair) {
if (!this.options.externalControlOnly)
this.element.stopObserving(pair.key, pair.value);
if (this.options.externalControl)
this.options.externalControl.stopObserving(pair.key, pair.value);
}.bind(this));
},
wrapUp: function(transport) {
this.leaveEditMode();
// Can't use triggerCallback due to backward compatibility: requires
// binding + direct element
this._boundComplete(transport, this.element);
}
});
Object.extend(Ajax.InPlaceEditor.prototype, {
dispose: Ajax.InPlaceEditor.prototype.destroy
});
Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
initialize: function($super, element, url, options) {
this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
$super(element, url, options);
},
createEditField: function() {
var list = document.createElement('select');
list.name = this.options.paramName;
list.size = 1;
this._controls.editor = list;
this._collection = this.options.collection || [];
if (this.options.loadCollectionURL)
this.loadCollection();
else
this.checkForExternalText();
this._form.appendChild(this._controls.editor);
},
loadCollection: function() {
this._form.addClassName(this.options.loadingClassName);
this.showLoadingText(this.options.loadingCollectionText);
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
var js = transport.responseText.strip();
if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
throw('Server returned an invalid collection representation.');
this._collection = eval(js);
this.checkForExternalText();
}.bind(this),
onFailure: this.onFailure
});
new Ajax.Request(this.options.loadCollectionURL, options);
},
showLoadingText: function(text) {
this._controls.editor.disabled = true;
var tempOption = this._controls.editor.firstChild;
if (!tempOption) {
tempOption = document.createElement('option');
tempOption.value = '';
this._controls.editor.appendChild(tempOption);
tempOption.selected = true;
}
tempOption.update((text || '').stripScripts().stripTags());
},
checkForExternalText: function() {
this._text = this.getText();
if (this.options.loadTextURL)
this.loadExternalText();
else
this.buildOptionList();
},
loadExternalText: function() {
this.showLoadingText(this.options.loadingText);
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
this._text = transport.responseText.strip();
this.buildOptionList();
}.bind(this),
onFailure: this.onFailure
});
new Ajax.Request(this.options.loadTextURL, options);
},
buildOptionList: function() {
this._form.removeClassName(this.options.loadingClassName);
this._collection = this._collection.map(function(entry) {
return 2 === entry.length ? entry : [entry, entry].flatten();
});
var marker = ('value' in this.options) ? this.options.value : this._text;
var textFound = this._collection.any(function(entry) {
return entry[0] == marker;
}.bind(this));
this._controls.editor.update('');
var option;
this._collection.each(function(entry, index) {
option = document.createElement('option');
option.value = entry[0];
option.selected = textFound ? entry[0] == marker : 0 == index;
option.appendChild(document.createTextNode(entry[1]));
this._controls.editor.appendChild(option);
}.bind(this));
this._controls.editor.disabled = false;
Field.scrollFreeActivate(this._controls.editor);
}
});
//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only exists for a while, in order to let ****
//**** users adapt to the new API. Read up on the new ****
//**** API and convert your code to it ASAP! ****
Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
if (!options) return;
function fallback(name, expr) {
if (name in options || expr === undefined) return;
options[name] = expr;
};
fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
options.cancelLink == options.cancelButton == false ? false : undefined)));
fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
options.okLink == options.okButton == false ? false : undefined)));
fallback('highlightColor', options.highlightcolor);
fallback('highlightEndColor', options.highlightendcolor);
};
Object.extend(Ajax.InPlaceEditor, {
DefaultOptions: {
ajaxOptions: { },
autoRows: 3, // Use when multi-line w/ rows == 1
cancelControl: 'link', // 'link'|'button'|false
cancelText: 'cancel',
clickToEditText: 'Click to edit',
externalControl: null, // id|elt
externalControlOnly: false,
fieldPostCreation: 'activate', // 'activate'|'focus'|false
formClassName: 'inplaceeditor-form',
formId: null, // id|elt
highlightColor: '#ffff99',
highlightEndColor: '#ffffff',
hoverClassName: '',
htmlResponse: true,
loadingClassName: 'inplaceeditor-loading',
loadingText: 'Loading...',
okControl: 'button', // 'link'|'button'|false
okText: 'ok',
paramName: 'value',
rows: 1, // If 1 and multi-line, uses autoRows
savingClassName: 'inplaceeditor-saving',
savingText: 'Saving...',
size: 0,
stripLoadedTextTags: false,
submitOnBlur: false,
textAfterControls: '',
textBeforeControls: '',
textBetweenControls: ''
},
DefaultCallbacks: {
callback: function(form) {
return Form.serialize(form);
},
onComplete: function(transport, element) {
// For backward compatibility, this one is bound to the IPE, and passes
// the element directly. It was too often customized, so we don't break it.
new Effect.Highlight(element, {
startcolor: this.options.highlightColor, keepBackgroundImage: true });
},
onEnterEditMode: null,
onEnterHover: function(ipe) {
ipe.element.style.backgroundColor = ipe.options.highlightColor;
if (ipe._effect)
ipe._effect.cancel();
},
onFailure: function(transport, ipe) {
alert('Error communication with the server: ' + transport.responseText.stripTags());
},
onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
onLeaveEditMode: null,
onLeaveHover: function(ipe) {
ipe._effect = new Effect.Highlight(ipe.element, {
startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
restorecolor: ipe._originalBackground, keepBackgroundImage: true
});
}
},
Listeners: {
click: 'enterEditMode',
keydown: 'checkForEscapeOrReturn',
mouseover: 'enterHover',
mouseout: 'leaveHover'
}
});
Ajax.InPlaceCollectionEditor.DefaultOptions = {
loadingCollectionText: 'Loading options...'
};
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create({
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
});

View file

@ -1,974 +0,0 @@
// script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
if(Object.isUndefined(Effect))
throw("dragdrop.js requires including script.aculo.us' effects.js library");
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null,
tree: false
}, arguments[1] || { });
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if(Object.isArray(containment)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
var drop, affected = [];
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0)
drop = Droppables.findDeepestChild(affected);
if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
if (drop) {
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if (drop != this.last_active) Droppables.activate(drop);
}
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop) {
this.last_active.onDrop(element, this.last_active.element, event);
return true;
}
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
};
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
if(draggable.options.delay) {
this._timeout = setTimeout(function() {
Draggables._timeout = null;
window.focus();
Draggables.activeDraggable = draggable;
}.bind(this), draggable.options.delay);
} else {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
}
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
};
/*--------------------------------------------------------------------------*/
var Draggable = Class.create({
initialize: function(element) {
var defaults = {
handle: false,
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
queue: {scope:'_draggable', position:'end'}
});
},
endeffect: function(element) {
var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
queue: {scope:'_draggable', position:'end'},
afterFinish: function(){
Draggable._dragging[element] = false
}
});
},
zindex: 1000,
revert: false,
quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
delay: 0
};
if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
Object.extend(defaults, {
starteffect: function(element) {
element._opacity = Element.getOpacity(element);
Draggable._dragging[element] = true;
new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
}
});
var options = Object.extend(defaults, arguments[1] || { });
this.element = $(element);
if(options.handle && Object.isString(options.handle))
this.handle = this.element.down('.'+options.handle, 0);
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
options.scroll = $(options.scroll);
this._isScrollChild = Element.childOf(this.element, options.scroll);
}
Element.makePositioned(this.element); // fix IE
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(!Object.isUndefined(Draggable._dragging[this.element]) &&
Draggable._dragging[this.element]) return;
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if((tag_name = src.tagName.toUpperCase()) && (
tag_name=='INPUT' ||
tag_name=='SELECT' ||
tag_name=='OPTION' ||
tag_name=='BUTTON' ||
tag_name=='TEXTAREA')) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = this.element.cumulativeOffset();
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(!this.delta)
this.delta = this.currentDelta();
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
if (!this._originallyAbsolute)
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
this.originalScrollLeft = where.left;
this.originalScrollTop = where.top;
} else {
this.originalScrollLeft = this.options.scroll.scrollLeft;
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
if(!this.options.quiet){
Position.prepare();
Droppables.show(pointer, this.element);
}
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft + Position.deltaX;
p[1] += this.options.scroll.scrollTop + Position.deltaY;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
var speed = [0,0];
if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(Prototype.Browser.WebKit) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.quiet){
Position.prepare();
var pointer = [Event.pointerX(event), Event.pointerY(event)];
Droppables.show(pointer, this.element);
}
if(this.options.ghosting) {
if (!this._originallyAbsolute)
Position.relativize(this.element);
delete this._originallyAbsolute;
Element.remove(this._clone);
this._clone = null;
}
var dropped = false;
if(success) {
dropped = Droppables.fire(event, this.element);
if (!dropped) dropped = false;
}
if(dropped && this.options.onDropped) this.options.onDropped(this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && Object.isFunction(revert)) revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
if (dropped == 0 || revert != 'failure')
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = this.element.cumulativeOffset();
if(this.options.ghosting) {
var r = Position.realOffset(this.element);
pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
}
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(Object.isFunction(this.options.snap)) {
p = this.options.snap(p[0],p[1],this);
} else {
if(Object.isArray(this.options.snap)) {
p = p.map( function(v, i) {
return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
} else {
p = p.map( function(v) {
return (v/this.options.snap).round()*this.options.snap }.bind(this));
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
if(!(speed[0] || speed[1])) return;
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
this.lastScrolled = current;
if(this.options.scroll == window) {
with (this._getWindowScroll(this.options.scroll)) {
if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
var d = delta / 1000;
this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
}
}
} else {
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
if (this._isScrollChild) {
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
}
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
if (w.document.documentElement && documentElement.scrollTop) {
T = documentElement.scrollTop;
L = documentElement.scrollLeft;
} else if (w.document.body) {
T = body.scrollTop;
L = body.scrollLeft;
}
if (w.innerWidth) {
W = w.innerWidth;
H = w.innerHeight;
} else if (w.document.documentElement && documentElement.clientWidth) {
W = documentElement.clientWidth;
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight;
}
}
return { top: T, left: L, width: W, height: H };
}
});
Draggable._dragging = { };
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create({
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
});
var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
sortables: { },
_findRootElement: function(element) {
while (element.tagName.toUpperCase() != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
},
options: function(element) {
element = Sortable._findRootElement($(element));
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
element = $(element);
var s = Sortable.sortables[element.id];
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false,
treeTag: 'ul',
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
delay: 0,
hoverclass: null,
ghosting: false,
quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: this.SERIALIZE_RULE,
// these take arrays of elements or ids and can be
// used for better initialization performance
elements: false,
handles: false,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || { });
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
quiet: options.quiet,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
delay: options.delay,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
};
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
};
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// drop on empty handling
if(options.dropOnEmpty || options.tree) {
Droppables.add(element, options_for_tree);
options.droppables.push(element);
}
(options.elements || this.findElements(element, options) || []).each( function(e,i) {
var handle = options.handles ? $(options.handles[i]) :
(options.handle ? $(e).select('.' + options.handle)[0] : e);
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
e.treeNode = element;
options.droppables.push(e);
});
}
// keep reference
this.sortables[element.identify()] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
},
onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;
if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Sortable._marker.hide();
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker =
($('dropmarker') || Element.extend(document.createElement('DIV'))).
hide().addClassName('dropmarker').setStyle({position:'absolute'});
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = dropon.cumulativeOffset();
Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
else
Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
Sortable._marker.show();
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: [],
position: parent.children.length,
container: $(children[i]).down(options.treeTag)
};
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child);
parent.children.push (child);
}
return parent;
},
tree: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
treeTag: sortableOptions.treeTag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
}, arguments[1] || { });
var root = {
id: null,
parent: null,
children: [],
container: element,
position: 0
};
return Sortable._tree(element, options, root);
},
/* Construct a [i] index for a particular node */
_constructIndex: function(node) {
var index = '';
do {
if (node.id) index = '[' + node.position + ']' + index;
} while ((node = node.parent) != null);
return index;
},
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || { });
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
},
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || { });
var nodeMap = { };
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
n[1].appendChild(n[0]);
delete nodeMap[ident];
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || { });
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "[id]=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
return Sortable.sequence(element, arguments[1]).map( function(item) {
return name + "[]=" + encodeURIComponent(item);
}).join('&');
}
}
};
// Returns true if child is contained within element
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
};
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==tagName &&
(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
elements.push(e);
if(recursive) {
var grandchildren = Element.findChildren(e, only, recursive, tagName);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : []);
};
Element.offsetSize = function (element, type) {
return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
};

File diff suppressed because it is too large Load diff

View file

@ -1,450 +0,0 @@
var Neopia = (function ($, I18n) {
// Console-polyfill. MIT license.
// https://github.com/paulmillr/console-polyfill
// Make it safe to do console.log() always.
var console = (function (con) {
"use strict";
var prop, method;
var empty = {};
var dummy = function () {};
var properties = "memory".split(",");
var methods = (
"assert,count,debug,dir,dirxml,error,exception,group," +
"groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd," +
"time,timeEnd,trace,warn"
).split(",");
while ((prop = properties.pop())) con[prop] = con[prop] || empty;
while ((method = methods.pop())) con[method] = con[method] || dummy;
return con;
})(window.console || {});
var Neopia = {
User: {
get: function (id) {
return $.ajax({
dataType: "json",
url: Neopia.API_URL + "/users/" + id,
useCSRFProtection: false,
}).then(function (response) {
return response.users[0];
});
},
},
Customization: {
request: function (petId, type) {
var data = {};
if (ImpressUser.id) {
data.impress_user = ImpressUser.id;
}
return $.ajax({
dataType: "json",
type: type,
url: Neopia.API_URL + "/pets/" + petId + "/customization",
useCSRFProtection: false,
data: data,
});
},
get: function (petId) {
return this.request(petId, "GET");
},
post: function (petId) {
return this.request(petId, "POST");
},
},
Status: {
get: function () {
return $.ajax({
dataType: "json",
url: Neopia.API_URL + "/status",
useCSRFProtection: false,
});
},
},
init: function () {
var hostEl = $("meta[name=neopia-host]");
if (!hostEl.length) {
throw "missing neopia-host meta tag";
}
var host = hostEl.attr("content");
if (!host) {
throw "neopia-host meta tag exists, but is empty";
}
Neopia.API_URL = "//" + host + "/api/1";
},
};
var ImpressUser = (function () {
var userSignedIn =
$("meta[name=user-signed-in]").attr("content") === "true";
if (userSignedIn) {
var currentUserId = $("meta[name=current-user-id]").attr("content");
return {
addNeopetsUsername: function (username) {
return $.ajax({
url: "/user/" + currentUserId + "/neopets-connections",
type: "POST",
data: { neopets_connection: { neopets_username: username } },
});
},
removeNeopetsUsername: function (username) {
return $.ajax({
url:
"/user/" +
currentUserId +
"/neopets-connections/" +
encodeURIComponent(username),
type: "POST",
data: { _method: "DELETE" },
});
},
getNeopetsUsernames: function () {
return JSON.parse(
$("#modeling-neopets-users").attr("data-usernames")
);
},
id: currentUserId,
};
} else {
return {
_key: "guestNeopetsUsernames",
_setNeopetsUsernames: function (usernames) {
localStorage.setItem(this._key, JSON.stringify(usernames));
},
addNeopetsUsername: function (username) {
this._setNeopetsUsernames(
this.getNeopetsUsernames().concat([username])
);
},
removeNeopetsUsername: function (username) {
this._setNeopetsUsernames(
this.getNeopetsUsernames().filter(function (u) {
return u !== username;
})
);
},
getNeopetsUsernames: function () {
return JSON.parse(localStorage.getItem(this._key)) || [];
},
id: null,
};
}
})();
var Modeling = {
_customizationsByPetId: {},
_customizations: [],
_itemsById: {},
_items: [],
_usersComponent: { setState: function () {} },
_neopetsUsernamesPresenceMap: {},
_addCustomization: function (customization) {
// Set all equipped, interesting items' statuses as success and cross
// them off the list.
var itemsById = this._itemsById;
var equippedByZone = customization.custom_pet.equipped_by_zone;
var closetItems = customization.closet_items;
Object.keys(equippedByZone).forEach(function (zoneId) {
var equippedClosetId = equippedByZone[zoneId].closet_obj_id;
var equippedObjectId = closetItems[equippedClosetId].obj_info_id;
if (itemsById.hasOwnProperty(equippedObjectId)) {
customization.statusByItemId[equippedObjectId] = "success";
itemsById[equippedObjectId].el
.find("span[data-body-id=" + customization.custom_pet.body_id + "]")
.addClass("modeled")
.attr("title", I18n.modeledBodyTitle);
}
});
this._customizationsByPetId[customization.custom_pet.name] =
customization;
this._customizations = this._buildCustomizations();
this._updateCustomizations();
},
_addNewCustomization: function (customization) {
customization.loadingForItemId = null;
customization.statusByItemId = {};
this._addCustomization(customization);
},
_buildCustomizations: function () {
var modelCustomizationsByPetId = this._customizationsByPetId;
return Object.keys(modelCustomizationsByPetId).map(function (petId) {
return modelCustomizationsByPetId[petId];
});
},
_createItems: function ($) {
var itemsById = this._itemsById;
this._items = $("#newest-unmodeled-items li")
.map(function () {
var el = $(this);
var item = {
el: el,
id: el.attr("data-item-id"),
name: el.find("h2").text(),
missingBodyIdsPresenceMap: el
.find("span[data-body-id]")
.toArray()
.reduce(function (map, node) {
map[$(node).attr("data-body-id")] = true;
return map;
}, {}),
};
item.component = React.renderComponent(
<ModelForItem item={item} />,
el.find(".models").get(0)
);
itemsById[item.id] = item;
return item;
})
.toArray();
},
_loadPetCustomization: function (neopiaPetId) {
return Neopia.Customization.get(neopiaPetId)
.done(this._addNewCustomization.bind(this))
.fail(function () {
console.error("couldn't load pet %s", neopiaPetId);
});
},
_loadManyPetsCustomizations: function (neopiaPetIds) {
return neopiaPetIds.map(this._loadPetCustomization.bind(this));
},
_loadUserCustomizations: function (neopiaUserId) {
return Neopia.User.get(neopiaUserId)
.then(function (neopiaUser) {
return neopiaUser.links.pets;
})
.then(this._loadManyPetsCustomizations.bind(this))
.fail(function () {
console.error("couldn't load user %s's customizations", neopiaUserId);
});
},
_startLoading: function (neopiaPetId, itemId) {
var customization = this._customizationsByPetId[neopiaPetId];
customization.loadingForItemId = itemId;
customization.statusByItemId[itemId] = "loading";
this._updateCustomizations();
},
_stopLoading: function (neopiaPetId, itemId, status) {
var customization = this._customizationsByPetId[neopiaPetId];
customization.loadingForItemId = null;
customization.statusByItemId[itemId] = status;
this._updateCustomizations();
},
_updateCustomizations: function () {
var neopetsUsernamesPresenceMap = this._neopetsUsernamesPresenceMap;
var liveCustomizations = this._customizations.filter(function (c) {
return neopetsUsernamesPresenceMap[c.custom_pet.owner];
});
this._items.forEach(function (item) {
var filteredCustomizations = liveCustomizations.filter(function (c) {
return item.missingBodyIdsPresenceMap[c.custom_pet.body_id];
});
item.component.setState({ customizations: filteredCustomizations });
});
},
_updateUsernames: function () {
var usernames = Object.keys(this._neopetsUsernamesPresenceMap);
this._usersComponent.setState({ usernames: usernames });
},
init: function ($) {
Neopia.init();
this._createItems($);
var usersEl = $("#modeling-neopets-users");
if (usersEl.length) {
this._usersComponent = React.renderComponent(
<NeopetsUsernamesForm />,
usersEl.get(0)
);
var usernames = ImpressUser.getNeopetsUsernames();
usernames.forEach(this._registerUsername.bind(this));
this._updateUsernames();
}
},
model: function (neopiaPetId, itemId) {
var oldCustomization = this._customizationsByPetId[neopiaPetId];
var itemsById = this._itemsById;
this._startLoading(neopiaPetId, itemId);
return Neopia.Customization.post(neopiaPetId)
.done(function (newCustomization) {
// Add this field as null for consistency.
newCustomization.loadingForItemId = null;
// Copy previous statuses.
newCustomization.statusByItemId = oldCustomization.statusByItemId;
// Set the attempted item's status as unworn (to possibly be
// overridden by the upcoming loop in _addCustomization).
newCustomization.statusByItemId[itemId] = "unworn";
// Now, finally, let's overwrite the old customization with the new.
Modeling._addCustomization(newCustomization);
})
.fail(function () {
Modeling._stopLoading(neopiaPetId, itemId, "error");
});
},
_registerUsername: function (username) {
this._neopetsUsernamesPresenceMap[username] = true;
this._loadUserCustomizations(username);
this._updateUsernames();
},
addUsername: function (username) {
if (typeof this._neopetsUsernamesPresenceMap[username] === "undefined") {
ImpressUser.addNeopetsUsername(username);
this._registerUsername(username);
}
},
removeUsername: function (username) {
if (this._neopetsUsernamesPresenceMap[username]) {
ImpressUser.removeNeopetsUsername(username);
delete this._neopetsUsernamesPresenceMap[username];
this._updateCustomizations();
this._updateUsernames();
}
},
};
var ModelForItem = React.createClass({
getInitialState: function () {
return { customizations: [] };
},
render: function () {
var item = this.props.item;
function createModelPet(customization) {
return (
<ModelPet
customization={customization}
item={item}
key={customization.custom_pet.name}
/>
);
}
var sortedCustomizations = this.state.customizations
.slice(0)
.sort(function (a, b) {
var aName = a.custom_pet.name.toLowerCase();
var bName = b.custom_pet.name.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
});
return <ul>{sortedCustomizations.map(createModelPet)}</ul>;
},
});
var ModelPet = React.createClass({
render: function () {
var petName = this.props.customization.custom_pet.name;
var status = this.props.customization.statusByItemId[this.props.item.id];
var loadingForItemId = this.props.customization.loadingForItemId;
var disabled = status === "loading" || status === "success";
if (
loadingForItemId !== null &&
loadingForItemId !== this.props.item.id
) {
disabled = true;
}
var itemName = this.props.item.name;
var imageSrc =
"https://pets.neopets.com/cpn/" +
petName +
"/1/1.png?" +
this.appearanceQuery();
var title = I18n.pet.title
.replace(/%{pet}/g, petName)
.replace(/%{item}/g, itemName);
var statusMessage = I18n.pet.status[status] || "";
return (
<li data-status={status}>
<button onClick={this.handleClick} title={title} disabled={disabled}>
<img src={imageSrc} />
<div>
<span className="pet-name">{petName}</span>
<span className="message">{statusMessage}</span>
</div>
</button>
</li>
);
},
handleClick: function (e) {
Modeling.model(
this.props.customization.custom_pet.name,
this.props.item.id
);
},
appearanceQuery: function () {
// By appending this string to the image URL, we update it when and only
// when the pet's appearance has changed.
var assetIdByZone = {};
var biologyByZone = this.props.customization.custom_pet.biology_by_zone;
var biologyPartIds = Object.keys(biologyByZone).forEach(function (zone) {
assetIdByZone[zone] = biologyByZone[zone].part_id;
});
var equippedByZone = this.props.customization.custom_pet.equipped_by_zone;
var equippedAssetIds = Object.keys(equippedByZone).forEach(function (
zone
) {
assetIdByZone[zone] = equippedByZone[zone].asset_id;
});
// Sort the zones, so the string (which should match exactly when the
// appearance matches) isn't dependent on iteration order.
return Object.keys(assetIdByZone)
.sort()
.map(function (zone) {
return "zone[" + zone + "]=" + assetIdByZone[zone];
})
.join("&");
},
});
var NeopetsUsernamesForm = React.createClass({
getInitialState: function () {
return { usernames: [], newUsername: "" };
},
render: function () {
function buildUsernameItem(username) {
return <NeopetsUsernameItem username={username} key={username} />;
}
return (
<div>
<ul>{this.state.usernames.slice(0).sort().map(buildUsernameItem)}</ul>
<form onSubmit={this.handleSubmit}>
<input
type="text"
placeholder={I18n.neopetsUsernamesForm.label}
onChange={this.handleChange}
value={this.state.newUsername}
/>
<button type="submit">{I18n.neopetsUsernamesForm.submit}</button>
</form>
</div>
);
},
handleChange: function (e) {
this.setState({ newUsername: e.target.value });
},
handleSubmit: function (e) {
e.preventDefault();
this.state.newUsername = $.trim(this.state.newUsername);
if (this.state.newUsername.length) {
Modeling.addUsername(this.state.newUsername);
this.setState({ newUsername: "" });
}
},
});
var NeopetsUsernameItem = React.createClass({
render: function () {
return (
<li>
{this.props.username} <button onClick={this.handleClick}>×</button>
</li>
);
},
handleClick: function (e) {
Modeling.removeUsername(this.props.username);
},
});
Modeling.init($);
return Neopia;
})(jQuery, ModelingI18n);

View file

@ -1,4 +1,3 @@
$('form.button_to input[type=submit]').click(function (e) { $("form.button_to input[type=submit]").click(function (e) {
if(!confirm(this.getAttribute('data-confirm'))) e.preventDefault(); if (!confirm(this.getAttribute("data-confirm"))) e.preventDefault();
}); });

View file

@ -1,5 +1,28 @@
(function () { (function () {
// don't need to export anything in here function petImage(id, size) {
return "https://pets.neopets.com/" + id + "/1/" + size + ".png";
}
var PetQuery = {},
query_string = document.location.hash || document.location.search;
$.each(query_string.substr(1).split("&"), function () {
var split_piece = this.split("=");
if (split_piece.length == 2) {
PetQuery[split_piece[0]] = split_piece[1];
}
});
if (PetQuery.name) {
if (PetQuery.species && PetQuery.color) {
$("#pet-query-notice-template")
.tmpl({
pet_name: PetQuery.name,
pet_image_url: petImage("cpn/" + PetQuery.name, 1),
})
.prependTo("#container");
}
}
var preview_el = $("#pet-preview"), var preview_el = $("#pet-preview"),
img_el = preview_el.find("img"), img_el = preview_el.find("img"),

View file

@ -1,22 +0,0 @@
function petImage(id, size) {
return 'https://pets.neopets.com/' + id + '/1/' + size + '.png';
}
var PetQuery = {},
query_string = document.location.hash || document.location.search;
$.each(query_string.substr(1).split('&'), function () {
var split_piece = this.split('=');
if(split_piece.length == 2) {
PetQuery[split_piece[0]] = split_piece[1];
}
});
if(PetQuery.name) {
if(PetQuery.species && PetQuery.color) {
$('#pet-query-notice-template').tmpl({
pet_name: PetQuery.name,
pet_image_url: petImage('cpn/' + PetQuery.name, 1)
}).prependTo('#container');
}
}

View file

@ -1,16 +1,16 @@
var DEBUG = (document.location.search.substr(0, 6) == '?debug'); var DEBUG = document.location.search.substr(0, 6) == "?debug";
/* Needed items form */ /* Needed items form */
(function () { (function () {
var UI = {}; var UI = {};
UI.form = $('#needed-items-form'); UI.form = $("#needed-items-form");
UI.alert = $('#needed-items-alert'); UI.alert = $("#needed-items-alert");
UI.pet_name_field = $('#needed-items-pet-name-field'); UI.pet_name_field = $("#needed-items-pet-name-field");
UI.pet_thumbnail = $('#needed-items-pet-thumbnail'); UI.pet_thumbnail = $("#needed-items-pet-thumbnail");
UI.pet_header = $('#needed-items-pet-header'); UI.pet_header = $("#needed-items-pet-header");
UI.reload = $('#needed-items-reload'); UI.reload = $("#needed-items-reload");
UI.pet_items = $('#needed-items-pet-items'); UI.pet_items = $("#needed-items-pet-items");
UI.item_template = $('#item-template'); UI.item_template = $("#item-template");
var current_request = { abort: function () {} }; var current_request = { abort: function () {} };
function sendRequest(options) { function sendRequest(options) {
@ -18,7 +18,7 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
} }
function cancelRequest() { function cancelRequest() {
if(DEBUG) console.log("Canceling request", current_request); if (DEBUG) console.log("Canceling request", current_request);
current_request.abort(); current_request.abort();
} }
@ -33,53 +33,56 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
cancelRequest(); cancelRequest();
sendRequest({ sendRequest({
url: UI.form.attr('action') + '.json', url: UI.form.attr("action") + ".json",
dataType: 'json', dataType: "json",
data: {name: pet_name}, data: { name: pet_name },
error: petError, error: petError,
success: function (data) { petSuccess(data, pet_name) }, success: function (data) {
complete: petComplete petSuccess(data, pet_name);
},
complete: petComplete,
}); });
UI.form.removeClass('failed').addClass('loading-pet'); UI.form.removeClass("failed").addClass("loading-pet");
} }
function petComplete() { function petComplete() {
UI.form.removeClass('loading-pet'); UI.form.removeClass("loading-pet");
} }
function petError(xhr) { function petError(xhr) {
UI.alert.text(xhr.responseText); UI.alert.text(xhr.responseText);
UI.form.addClass('failed'); UI.form.addClass("failed");
} }
function petSuccess(data, pet_name) { function petSuccess(data, pet_name) {
last_successful_pet_name = pet_name; last_successful_pet_name = pet_name;
UI.pet_thumbnail.attr('src', petThumbnailUrl(pet_name)); UI.pet_thumbnail.attr("src", petThumbnailUrl(pet_name));
UI.pet_header.empty(); UI.pet_header.empty();
$('#needed-items-pet-header-template').tmpl({pet_name: pet_name}). $("#needed-items-pet-header-template")
appendTo(UI.pet_header); .tmpl({ pet_name: pet_name })
.appendTo(UI.pet_header);
loadItems(data.query); loadItems(data.query);
} }
function petThumbnailUrl(pet_name) { function petThumbnailUrl(pet_name) {
return 'https://pets.neopets.com/cpn/' + pet_name + '/1/1.png'; return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
} }
/* Items */ /* Items */
function loadItems(query) { function loadItems(query) {
UI.form.addClass('loading-items'); UI.form.addClass("loading-items");
sendRequest({ sendRequest({
url: '/items/needed.json', url: "/items/needed.json",
dataType: 'json', dataType: "json",
data: query, data: query,
success: itemsSuccess success: itemsSuccess,
}); });
} }
function itemsSuccess(items) { function itemsSuccess(items) {
if(DEBUG) { if (DEBUG) {
// The dev server is missing lots of data, so sends me 2000+ needed // The dev server is missing lots of data, so sends me 2000+ needed
// items. We don't need that many for styling, so limit it to 100 to make // items. We don't need that many for styling, so limit it to 100 to make
// my browser happier. // my browser happier.
@ -89,7 +92,7 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
UI.pet_items.empty(); UI.pet_items.empty();
UI.item_template.tmpl(items).appendTo(UI.pet_items); UI.item_template.tmpl(items).appendTo(UI.pet_items);
UI.form.removeClass('loading-items').addClass('loaded'); UI.form.removeClass("loading-items").addClass("loaded");
} }
UI.form.submit(function (e) { UI.form.submit(function (e) {
@ -105,28 +108,31 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
/* Bulk pets form */ /* Bulk pets form */
(function () { (function () {
var form = $('#bulk-pets-form'), var form = $("#bulk-pets-form"),
queue_el = form.find('ul'), queue_el = form.find("ul"),
names_el = form.find('textarea'), names_el = form.find("textarea"),
add_el = $('#bulk-pets-form-add'), add_el = $("#bulk-pets-form-add"),
clear_el = $('#bulk-pets-form-clear'), clear_el = $("#bulk-pets-form-clear"),
bulk_load_queue; bulk_load_queue;
$(document.body).addClass('js'); $(document.body).addClass("js");
bulk_load_queue = new (function BulkLoadQueue() { bulk_load_queue = new (function BulkLoadQueue() {
var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30; var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30;
var RECENTLY_SENT_MAX = 3; var RECENTLY_SENT_MAX = 3;
var pets = [], url = form.attr('action') + '.json', var pets = [],
recently_sent_count = 0, loading = false; url = form.attr("action") + ".json",
recently_sent_count = 0,
loading = false;
function Pet(name) { function Pet(name) {
var el = $('#bulk-pets-submission-template').tmpl({pet_name: name}). var el = $("#bulk-pets-submission-template")
appendTo(queue_el); .tmpl({ pet_name: name })
.appendTo(queue_el);
this.load = function () { this.load = function () {
el.removeClass('waiting').addClass('loading'); el.removeClass("waiting").addClass("loading");
var response_el = el.find('span.response'); var response_el = el.find("span.response");
pets.shift(); pets.shift();
loading = true; loading = true;
$.ajax({ $.ajax({
@ -134,20 +140,21 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
loading = false; loading = false;
loadNextIfReady(); loadNextIfReady();
}, },
data: {name: name}, data: { name: name },
dataType: 'json', dataType: "json",
error: function (xhr) { error: function (xhr) {
el.removeClass('loading').addClass('failed'); el.removeClass("loading").addClass("failed");
response_el.text(xhr.responseText); response_el.text(xhr.responseText);
}, },
success: function (data) { success: function (data) {
var points = data.points; var points = data.points;
el.removeClass('loading').addClass('loaded'); el.removeClass("loading").addClass("loaded");
$('#bulk-pets-submission-success-template').tmpl({points: points}). $("#bulk-pets-submission-success-template")
appendTo(response_el); .tmpl({ points: points })
.appendTo(response_el);
}, },
type: 'post', type: "post",
url: url url: url,
}); });
recently_sent_count++; recently_sent_count++;
@ -155,39 +162,42 @@ var DEBUG = (document.location.search.substr(0, 6) == '?debug');
recently_sent_count--; recently_sent_count--;
loadNextIfReady(); loadNextIfReady();
}, RECENTLY_SENT_INTERVAL_IN_SECONDS * 1000); }, RECENTLY_SENT_INTERVAL_IN_SECONDS * 1000);
} };
} }
this.add = function (name) { this.add = function (name) {
name = name.replace(/^\s+|\s+$/g, ''); name = name.replace(/^\s+|\s+$/g, "");
if(name.length) { if (name.length) {
var pet = new Pet(name); var pet = new Pet(name);
pets.push(pet); pets.push(pet);
if(pets.length == 1) loadNextIfReady(); if (pets.length == 1) loadNextIfReady();
} }
} };
function loadNextIfReady() { function loadNextIfReady() {
if(!loading && recently_sent_count < RECENTLY_SENT_MAX && pets.length) { if (!loading && recently_sent_count < RECENTLY_SENT_MAX && pets.length) {
pets[0].load(); pets[0].load();
} }
} }
})(); })();
names_el.keyup(function () { names_el.keyup(function () {
var names = this.value.split('\n'), x = names.length - 1, i, name; var names = this.value.split("\n"),
for(i = 0; i < x; i++) { x = names.length - 1,
i,
name;
for (i = 0; i < x; i++) {
bulk_load_queue.add(names[i]); bulk_load_queue.add(names[i]);
} }
this.value = (x >= 0) ? names[x] : ''; this.value = x >= 0 ? names[x] : "";
}); });
add_el.click(function () { add_el.click(function () {
bulk_load_queue.add(names_el.val()); bulk_load_queue.add(names_el.val());
names_el.val(''); names_el.val("");
}); });
clear_el.click(function () { clear_el.click(function () {
queue_el.children('li.loaded, li.failed').remove(); queue_el.children("li.loaded, li.failed").remove();
}); });
})(); })();

View file

@ -1,3 +0,0 @@
/*! http://mths.be/placeholder v1.8.4 by @mathias */
(function($){var e='placeholder' in document.createElement('input'),a='placeholder' in document.createElement('textarea');if(e&&a){$.fn.placeholder=function(){return this};$.fn.placeholder.input=$.fn.placeholder.textarea=true}else{$.fn.placeholder=function(){return this.filter((e?'textarea':':input')+'[placeholder]').bind('focus.placeholder',b).bind('blur.placeholder',d).trigger('blur.placeholder').end()};$.fn.placeholder.input=e;$.fn.placeholder.textarea=a;$(function(){$('form').bind('submit.placeholder',function(){var f=$('.placeholder',this).each(b);setTimeout(function(){f.each(d)},10)})});$(window).bind('unload.placeholder',function(){$('.placeholder').val('')})}function c(g){var f={},h=/^jQuery\d+$/;$.each(g.attributes,function(k,j){if(j.specified&&!h.test(j.name)){f[j.name]=j.value}});return f}function b(){var f=$(this);if(f.val()===f.attr('placeholder')&&f.hasClass('placeholder')){if(f.data('placeholder-password')){f.hide().next().attr('id',f.removeAttr('id').data('placeholder-id')).show().focus()}else{f.val('').removeClass('placeholder')}}}function d(){var j,i=$(this),f=i,h=this.id;if(i.val()===''){if(i.is(':password')){if(!i.data('placeholder-textinput')){try{j=i.clone().attr({type:'text'})}catch(g){j=$('<input>').attr($.extend(c(this),{type:'text'}))}j.removeAttr('name').data('placeholder-password',true).data('placeholder-id',h).bind('focus.placeholder',b);i.data('placeholder-textinput',j).data('placeholder-id',h).before(j)}i=i.removeAttr('id').hide().prev().attr('id',h).show()}i.addClass('placeholder').val(i.attr('placeholder'))}else{i.removeClass('placeholder')}}}(jQuery));

File diff suppressed because it is too large Load diff

View file

@ -1,109 +0,0 @@
document.observe("dom:loaded", function() {
var authToken = $$('meta[name=csrf-token]').first().readAttribute('content'),
authParam = $$('meta[name=csrf-param]').first().readAttribute('content'),
formTemplate = '<form method="#{method}" action="#{action}">\
#{realmethod}<input name="#{param}" value="#{token}" type="hidden">\
</form>',
realmethodTemplate = '<input name="_method" value="#{method}" type="hidden">';
function handleRemote(element) {
var method, url, params;
if (element.tagName.toLowerCase() == 'form') {
method = element.readAttribute('method') || 'post';
url = element.readAttribute('action');
params = element.serialize(true);
} else {
method = element.readAttribute('data-method') || 'get';
url = element.readAttribute('href');
params = {};
}
var event = element.fire("ajax:before");
if (event.stopped) return false;
new Ajax.Request(url, {
method: method,
parameters: params,
asynchronous: true,
evalScripts: true,
onLoading: function(request) { element.fire("ajax:loading", {request: request}); },
onLoaded: function(request) { element.fire("ajax:loaded", {request: request}); },
onInteractive: function(request) { element.fire("ajax:interactive", {request: request}); },
onComplete: function(request) { element.fire("ajax:complete", {request: request}); },
onSuccess: function(request) { element.fire("ajax:success", {request: request}); },
onFailure: function(request) { element.fire("ajax:failure", {request: request}); }
});
element.fire("ajax:after");
}
$(document.body).observe("click", function(event) {
var message = event.element().readAttribute('data-confirm');
if (message && !confirm(message)) {
event.stop();
return false;
}
var element = event.findElement("a[data-remote=true]");
if (element) {
handleRemote(element);
event.stop();
}
var element = event.findElement("a[data-method]");
if (element && element.readAttribute('data-remote') != 'true') {
var method = element.readAttribute('data-method'),
piggyback = method.toLowerCase() != 'post',
formHTML = formTemplate.interpolate({
method: 'POST',
realmethod: piggyback ? realmethodTemplate.interpolate({ method: method }) : '',
action: element.readAttribute('href'),
token: authToken,
param: authParam
});
var form = new Element('div').update(formHTML).down().hide();
this.insert({ bottom: form });
form.submit();
event.stop();
}
});
// TODO: I don't think submit bubbles in IE
$(document.body).observe("submit", function(event) {
var message = event.element().readAttribute('data-confirm');
if (message && !confirm(message)) {
event.stop();
return false;
}
var inputs = event.element().select("input[type=submit][data-disable-with]");
inputs.each(function(input) {
input.disabled = true;
input.writeAttribute('data-original-value', input.value);
input.value = input.readAttribute('data-disable-with');
});
var element = event.findElement("form[data-remote=true]");
if (element) {
handleRemote(element);
event.stop();
}
});
$(document.body).observe("ajax:complete", function(event) {
var element = event.element();
if (element.tagName.toLowerCase() == 'form') {
var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
inputs.each(function(input) {
input.value = input.readAttribute('data-original-value');
input.writeAttribute('data-original-value', null);
input.disabled = false;
});
}
});
});

View file

@ -8,11 +8,9 @@
@import partials/jquery.jgrowl @import partials/jquery.jgrowl
@import alt_styles/index @import alt_styles/index
@import campaigns/show
@import closet_hangers/index @import closet_hangers/index
@import closet_hangers/petpage @import closet_hangers/petpage
@import closet_lists/form @import closet_lists/form
@import donations/show
@import neopets_page_import_tasks/new @import neopets_page_import_tasks/new
@import contributions/index @import contributions/index
@import items @import items

View file

@ -1,227 +0,0 @@
@import "partials/clean/constants"
@import "partials/campaign-progress"
@import "partials/outfit"
/* TODO: redundant with outfits/index; why is it not in the partial? */
$outfit-inner-height: 150px
$outfit-inner-width: 150px
$outfit-banner-h-padding: 4px
$outfit-banner-v-padding: 2px
$outfit-banner-inner-width: $outfit-inner-width - (2 * $outfit-banner-h-padding)
body.campaigns-show, body.campaigns-current
+campaign-progress
color: $campaign-text-color
a
color: $campaign-text-color + #222 !important
#home-link:hover
background-color: $campaign-background-color
#userbar, #footer
color: $text-color
a
color: $link-color
#home-link
color: $link-color
#title
display: none
#donation-form
+module
background: $campaign-background-color
border-color: $campaign-border-color
display: flex
flex-direction: row
margin-top: 1em
margin-bottom: 1.5em
padding-bottom: 32px
padding-left: 24px
padding-right: 24px
padding-top: 32px
position: relative
// We ignore the theme attribute on campaigns now, and just do purple.
&::after
background:
image: url(image_path("campaigns/purple.png"))
repeat: no-repeat
bottom: 0
content: " "
height: 123px
position: absolute
right: 4px
width: 150px
header, #donation-fields
flex: 1
#donation-form-title
font-size: 125%
font-weight: bold
margin-bottom: .25em
margin-top: 0
p
font-family: $main-font
font-size: 85%
margin-bottom: 0
margin-top: .5em
#donation-fields
margin-left: 20px
padding-top: 7px
#amount-header
font-size: 85%
font-weight: bold
line-height: 1
#amount-choices
$amount-choices-border-color: desaturate(lighten($campaign-border-color, 30%), 30%)
display: flex
flex-direction: row
margin-bottom: .75em
margin-top: .5em
li
border: 1px solid $amount-choices-border-color
border-radius: 5px
display: block
flex: 1
list-style: none
overflow: hidden
text-align: center
&:not(:last-of-type)
margin-right: .75em
input[type=radio]
height: 0
margin: 0
padding: 0
opacity: 0
position: absolute
width: 0
label
border: 1px solid transparent
box-sizing: border-box
display: block
padding: .5em .5em
width: 100%
input[type=radio]:checked ~ label
background: lighten($amount-choices-border-color, 15%)
font-weight: bold
input[type=radio]:focus ~ label
border-color: white
border-radius: 5px
#amount-custom-fields
display: none
input[type=text]
font-family: inherit
font-size: inherit
line-height: 1
padding: 0
text-align: center
#amount-custom:checked ~ #amount-custom-fields
display: block
#amount-custom:checked ~ label[for=amount-custom]
display: none
input[type=text]
border-color: #cce
color: $campaign-text-color
width: 3em
#donation-controls
button
+awesome-button-color(#004)
font-size: 120%
#campaign-text[data-campaign-complete]
#description
display: none
&[data-show]
display: block
#success-thanks
border: 1px dashed $module-border-color
margin-bottom: 1em
padding: 1em
position: relative
p:last-child
margin-bottom: 0
#success-thanks-toggle-description
position: absolute
bottom: 1em
font-style: italic
right: 1em
#outfits
+outfits-list
text-align: center
> li
+outfit
height: $outfit-inner-height
margin: 2px
width: $outfit-inner-width
header, footer
font-size: 85%
padding: $outfit-banner-v-padding $outfit-banner-h-padding
width: $outfit-banner-inner-width
img
height: $outfit-inner-height
width: $outfit-inner-width
&.banner
background-image: url(https://images.neopets.com/themes/004_bir_a2e60/footer_bg.png)
background-position: 0 -60px
border: 2px solid #006
color: white
height: 100px
line-height: 100px
margin: 4px 0
text-shadow: #335 2px 2px 1px
width: 800px - 4px
span
+inline-block
font-size: 32px
font-weight: bold
line-height: 1.5
vertical-align: middle
#last-years-donors
font-weight: bold
margin-top: 1em
text-align: center
#outfits-header > *
display: inline-block
#all-campaigns-list
li
display: inline-block
list-style: none
margin-left: 1em
#fine-print
font-size: 85%
margin-top: 2em

View file

@ -1,29 +0,0 @@
@import "../partials/clean/constants"
@import "../partials/clean/mixins"
body.donations-show
#thank-you
border: 3px solid $module-border-color
display: block
margin: 0 auto 1em
#edit-donation
ul
list-style: none
label
+inline-block
width: 16em
.name input[type=text]
width: 10em
.feature input[type=text]
width: 23em
.choose-outfit
font-size: 85%
margin-left: 1em
select
width: 10em

View file

@ -0,0 +1,227 @@
@import "../../partials/clean/constants"
@import "../../partials/campaign-progress"
@import "../../partials/outfit"
/* TODO: redundant with outfits/index; why is it not in the partial? */
$outfit-inner-height: 150px
$outfit-inner-width: 150px
$outfit-banner-h-padding: 4px
$outfit-banner-v-padding: 2px
$outfit-banner-inner-width: $outfit-inner-width - (2 * $outfit-banner-h-padding)
body.fundraising-campaigns
+campaign-progress
color: $campaign-text-color
a
color: $campaign-text-color + #222 !important
#home-link:hover
background-color: $campaign-background-color
#userbar, #footer
color: $text-color
a
color: $link-color
#home-link
color: $link-color
#title
display: none
#donation-form
+module
background: $campaign-background-color
border-color: $campaign-border-color
display: flex
flex-direction: row
margin-top: 1em
margin-bottom: 1.5em
padding-bottom: 32px
padding-left: 24px
padding-right: 24px
padding-top: 32px
position: relative
// We ignore the theme attribute on campaigns now, and just do purple.
&::after
background:
image: url(image_path("fundraising/purple.png"))
repeat: no-repeat
bottom: 0
content: " "
height: 123px
position: absolute
right: 4px
width: 150px
header, #donation-fields
flex: 1
#donation-form-title
font-size: 125%
font-weight: bold
margin-bottom: .25em
margin-top: 0
p
font-family: $main-font
font-size: 85%
margin-bottom: 0
margin-top: .5em
#donation-fields
margin-left: 20px
padding-top: 7px
#amount-header
font-size: 85%
font-weight: bold
line-height: 1
#amount-choices
$amount-choices-border-color: desaturate(lighten($campaign-border-color, 30%), 30%)
display: flex
flex-direction: row
margin-bottom: .75em
margin-top: .5em
li
border: 1px solid $amount-choices-border-color
border-radius: 5px
display: block
flex: 1
list-style: none
overflow: hidden
text-align: center
&:not(:last-of-type)
margin-right: .75em
input[type=radio]
height: 0
margin: 0
padding: 0
opacity: 0
position: absolute
width: 0
label
border: 1px solid transparent
box-sizing: border-box
display: block
padding: .5em .5em
width: 100%
input[type=radio]:checked ~ label
background: lighten($amount-choices-border-color, 15%)
font-weight: bold
input[type=radio]:focus ~ label
border-color: white
border-radius: 5px
#amount-custom-fields
display: none
input[type=text]
font-family: inherit
font-size: inherit
line-height: 1
padding: 0
text-align: center
#amount-custom:checked ~ #amount-custom-fields
display: block
#amount-custom:checked ~ label[for=amount-custom]
display: none
input[type=text]
border-color: #cce
color: $campaign-text-color
width: 3em
#donation-controls
button
+awesome-button-color(#004)
font-size: 120%
#campaign-text[data-campaign-complete]
#description
display: none
&[data-show]
display: block
#success-thanks
border: 1px dashed $module-border-color
margin-bottom: 1em
padding: 1em
position: relative
p:last-child
margin-bottom: 0
#success-thanks-toggle-description
position: absolute
bottom: 1em
font-style: italic
right: 1em
#outfits
+outfits-list
text-align: center
> li
+outfit
height: $outfit-inner-height
margin: 2px
width: $outfit-inner-width
header, footer
font-size: 85%
padding: $outfit-banner-v-padding $outfit-banner-h-padding
width: $outfit-banner-inner-width
img
height: $outfit-inner-height
width: $outfit-inner-width
&.banner
background-image: url(https://images.neopets.com/themes/004_bir_a2e60/footer_bg.png)
background-position: 0 -60px
border: 2px solid #006
color: white
height: 100px
line-height: 100px
margin: 4px 0
text-shadow: #335 2px 2px 1px
width: 800px - 4px
span
+inline-block
font-size: 32px
font-weight: bold
line-height: 1.5
vertical-align: middle
#last-years-donors
font-weight: bold
margin-top: 1em
text-align: center
#outfits-header > *
display: inline-block
#all-campaigns-list
li
display: inline-block
list-style: none
margin-left: 1em
#fine-print
font-size: 85%
margin-top: 2em

View file

@ -0,0 +1,28 @@
@import "../../partials/clean/constants"
@import "../../partials/clean/mixins"
#thank-you
border: 3px solid $module-border-color
display: block
margin: 0 auto 1em
#edit-donation
ul
list-style: none
label
+inline-block
width: 16em
.name input[type=text]
width: 10em
.feature input[type=text]
width: 23em
.choose-outfit
font-size: 85%
margin-left: 1em
select
width: 10em

View file

@ -1,27 +0,0 @@
class CampaignsController < ApplicationController
def show
@campaign = Campaign.find(params[:id])
redirect_to(action: :current) if @campaign.active?
@current_campaign = Campaign.current
@donations = find_donations
@all_campaigns = find_all_campaigns
end
def current
@campaign = Campaign.current
@current_campaign = @campaign
@donations = find_donations
@all_campaigns = find_all_campaigns
render action: :show
end
private
def find_all_campaigns
@all_campaigns = Campaign.order('created_at DESC').all
end
def find_donations
@campaign.donations.includes(features: :outfit).order('created_at DESC')
end
end

View file

@ -65,7 +65,7 @@ class ClosetHangersController < ApplicationController
current_user.assign_closeted_to_items!(items) current_user.assign_closeted_to_items!(items)
end end
@campaign = Campaign.current @campaign = Fundraising::Campaign.current
end end
def petpage def petpage

View file

@ -1,8 +0,0 @@
class DonationFeaturesController < ApplicationController
def index
# TODO: scope by campaign?
@features = DonationFeature.includes(:donation).includes(:outfit).
where('outfit_id IS NOT NULL')
render json: @features
end
end

View file

@ -1,56 +0,0 @@
class DonationsController < ApplicationController
def create
@campaign = Campaign.current
begin
@donation = Donation.create_from_charge(
@campaign, current_user, params[:donation])
rescue Stripe::CardError => e
flash[:alert] = "We couldn't process your donation: #{e.message}"
redirect_to :donate
rescue => e
flash[:alert] =
"We couldn't process your donation: #{e.message} " +
"Please try again later!"
redirect_to :donate
else
redirect_to @donation
end
end
def show
@donation = Donation.from_param(params[:id])
@features = @donation.features
@outfits = current_user.outfits.wardrobe_order if user_signed_in?
end
def update
@donation = Donation.from_param(params[:id])
@donation.attributes = donation_params
feature_params = params[:feature] || {}
@features = @donation.features.find(feature_params.keys)
@features.each do |feature|
feature.outfit_url = feature_params[feature.id.to_s][:outfit_url]
end
begin
Donation.transaction do
@donation.save!
@features.each(&:save!)
end
rescue ActiveRecord::RecordInvalid
flash[:alert] = "Couldn't save donation details. Do those outfits exist?"
redirect_to @donation
else
flash[:notice] = 'Donation details saved! ' +
'Also, have we thanked you yet today? Thank you!'
redirect_to @donation
end
end
private
def donation_params
params.require(:donation).permit(:donor_name)
end
end

View file

@ -0,0 +1,29 @@
module Fundraising
class CampaignsController < ApplicationController
def show
@campaign = Campaign.find(params[:id])
redirect_to(action: :current) if @campaign.active?
@current_campaign = Campaign.current
@donations = find_donations
@all_campaigns = find_all_campaigns
end
def current
@campaign = Campaign.current
@current_campaign = @campaign
@donations = find_donations
@all_campaigns = find_all_campaigns
render action: :show
end
private
def find_all_campaigns
@all_campaigns = Campaign.order('created_at DESC').all
end
def find_donations
@campaign.donations.includes(features: :outfit).order('created_at DESC')
end
end
end

View file

@ -0,0 +1,10 @@
module Fundraising
class DonationFeaturesController < ApplicationController
def index
# TODO: scope by campaign?
@features = DonationFeature.includes(:donation).includes(:outfit).
where('outfit_id IS NOT NULL')
render json: @features
end
end
end

View file

@ -0,0 +1,58 @@
module Fundraising
class DonationsController < ApplicationController
def create
@campaign = Campaign.current
begin
@donation = Donation.create_from_charge(
@campaign, current_user, params[:donation])
rescue Stripe::CardError => e
flash[:alert] = "We couldn't process your donation: #{e.message}"
redirect_to :donate
rescue => e
flash[:alert] =
"We couldn't process your donation: #{e.message} " +
"Please try again later!"
redirect_to :donate
else
redirect_to @donation
end
end
def show
@donation = Donation.from_param(params[:id])
@features = @donation.features
@outfits = current_user.outfits.wardrobe_order if user_signed_in?
end
def update
@donation = Donation.from_param(params[:id])
@donation.attributes = donation_params
feature_params = params[:feature] || {}
@features = @donation.features.find(feature_params.keys)
@features.each do |feature|
feature.outfit_url = feature_params[feature.id.to_s][:outfit_url]
end
begin
Donation.transaction do
@donation.save!
@features.each(&:save!)
end
rescue ActiveRecord::RecordInvalid
flash[:alert] = "Couldn't save donation details. Do those outfits exist?"
redirect_to @donation
else
flash[:notice] = 'Donation details saved! ' +
'Also, have we thanked you yet today? Thank you!'
redirect_to @donation
end
end
private
def donation_params
params.require(:donation).permit(:donor_name)
end
end
end

View file

@ -18,7 +18,7 @@ class ItemsController < ApplicationController
assign_closeted! assign_closeted!
respond_to do |format| respond_to do |format|
format.html { format.html {
@campaign = Campaign.current rescue nil @campaign = Fundraising::Campaign.current rescue nil
if @items.count == 1 if @items.count == 1
redirect_to @items.first redirect_to @items.first
else else
@ -45,7 +45,7 @@ class ItemsController < ApplicationController
else else
respond_to do |format| respond_to do |format|
format.html { format.html {
@campaign = Campaign.current rescue nil @campaign = Fundraising::Campaign.current rescue nil
@newest_items = Item.newest.includes(:translations).limit(18) @newest_items = Item.newest.includes(:translations).limit(18)
} }
format.js { render json: {error: '$q required'}} format.js { render json: {error: '$q required'}}

View file

@ -76,7 +76,7 @@ class OutfitsController < ApplicationController
@neopets_usernames = user_signed_in? ? current_user.neopets_usernames : [] @neopets_usernames = user_signed_in? ? current_user.neopets_usernames : []
@campaign = Campaign.current rescue nil @campaign = Fundraising::Campaign.current rescue nil
end end
def show def show

View file

@ -1,2 +0,0 @@
module AltStylesHelper
end

View file

@ -15,7 +15,8 @@ module ApplicationHelper
end end
def body_class def body_class
"#{params[:controller]} #{params[:controller]}-#{params[:action]}".tap do |output| controller = params[:controller].gsub("/", "-")
"#{controller} #{controller}-#{params[:action]}".tap do |output|
output << @body_class if @body_class output << @body_class if @body_class
end end
end end

View file

@ -1,25 +0,0 @@
module DonationsHelper
THANK_YOU_GREETINGS = [
'https://images.neopets.com/new_greetings/1368.gif',
'https://images.neopets.com/new_greetings/466.gif',
'https://images.neopets.com/new_greetings/48.gif',
'https://images.neopets.com/new_greetings/49.gif',
'https://images.neopets.com/new_greetings/64.gif',
'https://images.neopets.com/new_greetings/65.gif',
'https://images.neopets.com/new_greetings/66.gif',
'https://images.neopets.com/new_greetings/67.gif',
'https://images.neopets.com/new_greetings/69.gif',
'https://images.neopets.com/new_greetings/71.gif',
'https://images.neopets.com/new_greetings/72.gif',
'https://images.neopets.com/new_greetings/103.gif',
'https://images.neopets.com/new_greetings/420.gif'
]
def thank_you_greeting_url
THANK_YOU_GREETINGS.sample
end
def feature_outfit_url(outfit_id)
outfit_url(outfit_id) if outfit_id
end
end

View file

@ -0,0 +1,11 @@
module Fundraising
module CampaignsHelper
def large_donation?(amount)
amount > 100_00
end
def outfit_image?(outfit)
outfit.present? && outfit.image?
end
end
end

View file

@ -0,0 +1,27 @@
module Fundraising
module DonationsHelper
THANK_YOU_GREETINGS = [
'https://images.neopets.com/new_greetings/1368.gif',
'https://images.neopets.com/new_greetings/466.gif',
'https://images.neopets.com/new_greetings/48.gif',
'https://images.neopets.com/new_greetings/49.gif',
'https://images.neopets.com/new_greetings/64.gif',
'https://images.neopets.com/new_greetings/65.gif',
'https://images.neopets.com/new_greetings/66.gif',
'https://images.neopets.com/new_greetings/67.gif',
'https://images.neopets.com/new_greetings/69.gif',
'https://images.neopets.com/new_greetings/71.gif',
'https://images.neopets.com/new_greetings/72.gif',
'https://images.neopets.com/new_greetings/103.gif',
'https://images.neopets.com/new_greetings/420.gif'
]
def thank_you_greeting_url
THANK_YOU_GREETINGS.sample
end
def feature_outfit_url(outfit_id)
outfit_url(outfit_id) if outfit_id
end
end
end

View file

@ -1,9 +0,0 @@
module StaticHelper
def large_donation?(amount)
amount > 100_00
end
def outfit_image?(outfit)
outfit.present? && outfit.image?
end
end

View file

@ -1,8 +0,0 @@
class DonationMailer < ActionMailer::Base
default from: "matchu@openneo.net"
def thank_you_email(donation, recipient)
@donation = donation
mail(to: recipient, subject: 'Thanks for donating to Dress to Impress!')
end
end

View file

@ -0,0 +1,10 @@
module Fundraising
class DonationMailer < ActionMailer::Base
default from: "matchu@openneo.net"
def thank_you_email(donation, recipient)
@donation = donation
mail(to: recipient, subject: 'Thanks for donating to Dress to Impress!')
end
end
end

View file

@ -1,20 +0,0 @@
class Campaign < ApplicationRecord
has_many :donations
def progress_percent
[(progress.to_f / goal) * 100, 100].min
end
def remaining
goal - progress
end
def complete?
progress >= goal
end
def self.current
self.where(active: true).first or
raise ActiveRecord::RecordNotFound.new("no current campaign")
end
end

View file

@ -1,69 +0,0 @@
class Donation < ApplicationRecord
FEATURE_COST = 500 # in cents = $5.00
belongs_to :campaign
belongs_to :user, optional: true
has_many :features, class_name: 'DonationFeature'
def to_param
"#{id}-#{secret}"
end
def self.create_from_charge(campaign, user, params)
amount = (BigDecimal.new(params[:amount]) * 100).floor
campaign.progress += amount
charge_params = {
amount: amount,
description: 'Donation (thank you!)',
currency: 'usd'
}
if params[:stripe_token_type] == 'card'
customer = Stripe::Customer.create(
card: params[:stripe_token]
)
charge_params[:customer] = customer.id
elsif params[:stripe_token_type] == 'bitcoin_receiver'
charge_params[:card] = params[:stripe_token]
else
raise ArgumentError, "unexpected stripe token type #{params[:stripe_token_type]}"
end
charge = Stripe::Charge.create(charge_params)
donation = campaign.donations.build
donation.amount = amount
donation.charge_id = charge.id
donation.user = user
donation.donor_name = user.try(:name)
donation.donor_email = params[:donor_email]
donation.secret = new_secret
num_features = amount / FEATURE_COST
features = []
num_features.times do
features << donation.features.new
end
Donation.transaction do
campaign.save!
donation.save!
features.each(&:save!)
end
DonationMailer.thank_you_email(donation, donation.donor_email).deliver
donation
end
def self.new_secret
SecureRandom.urlsafe_base64 8
end
def self.from_param(param)
id, secret = param.split('-', 2)
self.where(secret: secret).find(id)
end
end

View file

@ -1,24 +0,0 @@
class DonationFeature < ApplicationRecord
belongs_to :donation
belongs_to :outfit, optional: true
validates :outfit, presence: true, if: :outfit_id_present?
delegate :donor_name, to: :donation
def as_json(options={})
{donor_name: donor_name, outfit_image_url: outfit_image_url}
end
def outfit_url=(outfit_url)
self.outfit_id = outfit_url.split('/').last rescue nil
end
def outfit_id_present?
outfit_id.present?
end
def outfit_image_url
outfit && outfit.image ? outfit.image.medium.url : nil
end
end

View file

@ -0,0 +1,22 @@
module Fundraising
class Campaign < ApplicationRecord
has_many :donations
def progress_percent
[(progress.to_f / goal) * 100, 100].min
end
def remaining
goal - progress
end
def complete?
progress >= goal
end
def self.current
self.where(active: true).first or
raise ActiveRecord::RecordNotFound.new("no current campaign")
end
end
end

View file

@ -0,0 +1,71 @@
module Fundraising
class Donation < ApplicationRecord
FEATURE_COST = 500 # in cents = $5.00
belongs_to :campaign
belongs_to :user, optional: true
has_many :features, class_name: 'DonationFeature'
def to_param
"#{id}-#{secret}"
end
def self.create_from_charge(campaign, user, params)
amount = (BigDecimal.new(params[:amount]) * 100).floor
campaign.progress += amount
charge_params = {
amount: amount,
description: 'Donation (thank you!)',
currency: 'usd'
}
if params[:stripe_token_type] == 'card'
customer = Stripe::Customer.create(
card: params[:stripe_token]
)
charge_params[:customer] = customer.id
elsif params[:stripe_token_type] == 'bitcoin_receiver'
charge_params[:card] = params[:stripe_token]
else
raise ArgumentError, "unexpected stripe token type #{params[:stripe_token_type]}"
end
charge = Stripe::Charge.create(charge_params)
donation = campaign.donations.build
donation.amount = amount
donation.charge_id = charge.id
donation.user = user
donation.donor_name = user.try(:name)
donation.donor_email = params[:donor_email]
donation.secret = new_secret
num_features = amount / FEATURE_COST
features = []
num_features.times do
features << donation.features.new
end
Donation.transaction do
campaign.save!
donation.save!
features.each(&:save!)
end
DonationMailer.thank_you_email(donation, donation.donor_email).deliver
donation
end
def self.new_secret
SecureRandom.urlsafe_base64 8
end
def self.from_param(param)
id, secret = param.split('-', 2)
self.where(secret: secret).find(id)
end
end
end

View file

@ -0,0 +1,26 @@
module Fundraising
class DonationFeature < ApplicationRecord
belongs_to :donation
belongs_to :outfit, optional: true
validates :outfit, presence: true, if: :outfit_id_present?
delegate :donor_name, to: :donation
def as_json(options={})
{donor_name: donor_name, outfit_image_url: outfit_image_url}
end
def outfit_url=(outfit_url)
self.outfit_id = outfit_url.split('/').last rescue nil
end
def outfit_id_present?
outfit_id.present?
end
def outfit_image_url
outfit && outfit.image ? outfit.image.medium.url : nil
end
end
end

View file

@ -2,8 +2,8 @@ module OwlsValueGuide
include HTTParty include HTTParty
ITEMDATA_URL_TEMPLATE = Addressable::Template.new( ITEMDATA_URL_TEMPLATE = Addressable::Template.new(
"https://neo-owls.net/itemdata/{item_name}" "https://neo-owls.net/itemdata/{item_name}"
) )
def self.find_by_name(item_name) def self.find_by_name(item_name)
# Load the itemdata, pulling from the Rails cache if possible. # Load the itemdata, pulling from the Rails cache if possible.

View file

@ -153,5 +153,5 @@
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery, :jquery_tmpl = include_javascript_libraries :jquery, :jquery_tmpl
= javascript_include_tag 'ajax_auth', 'jquery.ui', 'jquery.jgrowl', = javascript_include_tag 'ajax_auth', 'lib/jquery.ui', 'lib/jquery.jgrowl',
'placeholder', 'stickUp.min', 'closet_hangers/index' 'lib/stickUp.min', 'closet_hangers/index'

View file

@ -125,4 +125,7 @@
- content_for :javascripts do - content_for :javascripts do
= javascript_include_tag 'https://checkout.stripe.com/checkout.js', = javascript_include_tag 'https://checkout.stripe.com/checkout.js',
'static/donate.js' 'fundraising/campaigns/show'
- content_for :stylesheets do
= stylesheet_link_tag 'fundraising/campaigns/show'

View file

@ -43,4 +43,7 @@
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery = include_javascript_libraries :jquery
= javascript_include_tag 'donations/show.js' = javascript_include_tag 'fundraising/donations/show'
- content_for :stylesheets do
= stylesheet_link_tag 'fundraising/donations/show'

View file

@ -117,4 +117,4 @@
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery20, :jquery_tmpl = include_javascript_libraries :jquery20, :jquery_tmpl
= javascript_include_tag 'ajax_auth', 'react', 'jquery.timeago', 'pet_query', 'outfits/new' = javascript_include_tag 'ajax_auth', 'lib/react', 'lib/jquery.timeago', 'outfits/new'

View file

@ -79,13 +79,15 @@ OpenneoImpressItems::Application.routes.draw do
end end
# Donation campaign stuff! # Donation campaign stuff!
resources :donations, only: [:create, :show, :update] do scope module: "fundraising", as: "fundraising" do
collection do resources :donations, only: [:create, :show, :update] do
resources :donation_features, path: 'features', only: [:index] collection do
resources :donation_features, path: 'features', only: [:index]
end
end end
resources :campaigns, only: [:show], path: '/donate/campaigns'
end end
resources :campaigns, only: [:show], path: '/donate/campaigns' get '/donate' => 'fundraising/campaigns#current', as: :donate
get '/donate' => 'campaigns#current', as: :donate
# Static pages! # Static pages!
get '/terms', as: :terms, get '/terms', as: :terms,

View file

@ -209,4 +209,4 @@ Zone.create(:id => 52, :label => "Foreground", :plain_label => "foreground", :de
# NOTE: Creating an AuthUser automatically creates a User, too. # NOTE: Creating an AuthUser automatically creates a User, too.
AuthUser.create(name: "test", password: "test123", email: "test@gmail.example") AuthUser.create(name: "test", password: "test123", email: "test@gmail.example")
Campaign.create(goal: 100_00, active: true, advertised: false, description: "") Fundraising::Campaign.create(goal: 100_00, active: true, advertised: false, description: "")