1
0
Fork 0
forked from OpenNeo/impress
impress/app/assets/javascripts/closet_hangers/index.js
Emi Matchu 13b92b30d0 Replace old stickUp dependency with position: sticky
From an era when we didn't have that! Now we do!

(My motivation is that I'm trying to add new JS to this page and errors
in stickUp are crashing the page early, womp womp!)
2024-02-20 18:33:23 -08:00

818 lines
22 KiB
JavaScript

(function () {
var hangersInitCallbacks = [];
function onHangersInit(callback) {
hangersInitCallbacks[hangersInitCallbacks.length] = callback;
}
function hangersInit() {
for (var i = 0; i < hangersInitCallbacks.length; i++) {
hangersInitCallbacks[i]();
}
}
/*
Hanger groups
*/
var hangerGroups = [];
$("div.closet-hangers-group").each(function () {
var el = $(this);
var lists = [];
el.find("div.closet-list").each(function () {
var el = $(this);
var id = el.attr("data-id");
if (id) {
lists[lists.length] = {
id: parseInt(id, 10),
label: el.find("h4").text(),
};
}
});
hangerGroups[hangerGroups.length] = {
label: el.find("h3").text(),
lists: lists,
owned: el.attr("data-owned") == "true",
};
});
$("div.closet-hangers-group span.toggle").live("click", function () {
$(this).closest(".closet-hangers-group").toggleClass("hidden");
});
var hangersElQuery = "#closet-hangers";
var hangersEl = $(hangersElQuery);
/*
Compare with Your Items
*/
$("#toggle-compare").click(function () {
hangersEl.toggleClass("comparing");
});
/*
Hanger forms
*/
var body = $(document.body).addClass("js");
if (!body.hasClass("current-user")) return false;
// 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.
// If there were another way to add hangers, then we'd have to worry about
// that, but, right now, the only way to create a new hanger from this page
// is through the autocompleter, which reinitializes anyway. Geez, this thing
// is begging for a rewrite, but today we're here for performance.
$("#closet-hanger-update-tmpl").template("updateFormTmpl");
$("#closet-hanger-destroy-tmpl").template("destroyFormTmpl");
onHangersInit(function () {
// Super-lame hack to get the user ID from where it already is :/
var currentUserId = itemsSearchForm.data("current-user-id");
$("#closet-hangers div.closet-hangers-group").each(function () {
var groupEl = $(this);
var owned = groupEl.data("owned");
groupEl.find("div.closet-list").each(function () {
var listEl = $(this);
var listId = listEl.data("id");
listEl.find("div.object").each(function () {
var hangerEl = $(this);
var hangerId = hangerEl.data("id");
var quantityEl = hangerEl.find("div.quantity");
var quantity = hangerEl.data("quantity");
// Ooh, this part is weird. We only want the name to be linked, so
// lift everything else out.
var checkboxId = "hanger-selected-" + hangerId;
var label = $("<label />", { for: checkboxId });
var link = hangerEl.children("a");
link.children(":not(.name)").detach().appendTo(label);
link.detach().appendTo(label);
var checkbox = $("<input />", {
type: "checkbox",
id: checkboxId,
}).appendTo(hangerEl);
label.appendTo(hangerEl);
// 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.
link.attr("target", "_blank");
$.tmpl("updateFormTmpl", {
user_id: currentUserId,
closet_hanger_id: hangerId,
quantity: quantity,
list_id: listId,
owned: owned,
}).appendTo(quantityEl);
$.tmpl("destroyFormTmpl", {
user_id: currentUserId,
closet_hanger_id: hangerId,
}).appendTo(hangerEl);
});
});
});
});
$.fn.liveDraggable = function (opts) {
this.live("mouseover", function () {
if (!$(this).data("init")) {
$(this).data("init", true).draggable(opts);
}
});
};
$.fn.disableForms = function () {
return this.data("formsDisabled", true)
.find("input")
.attr("disabled", "disabled")
.end();
};
$.fn.enableForms = function () {
return this.data("formsDisabled", false)
.find("input")
.removeAttr("disabled")
.end();
};
$.fn.hasChanged = function () {
return this.attr("data-previous-value") != this.val();
};
$.fn.revertValue = function () {
return this.each(function () {
var el = $(this);
el.val(el.attr("data-previous-value"));
});
};
$.fn.storeValue = function () {
return this.each(function () {
var el = $(this);
el.attr("data-previous-value", el.val());
});
};
$.fn.insertIntoSortedList = function (list, compare) {
var newChild = this,
inserted = false;
list.children().each(function () {
if (compare(newChild, $(this)) < 1) {
newChild.insertBefore(this);
inserted = true;
return false;
}
});
if (!inserted) newChild.appendTo(list);
return this;
};
function handleSaveError(xhr, action) {
try {
var data = $.parseJSON(xhr.responseText);
} catch (e) {
var data = {};
}
if (typeof data.errors != "undefined") {
$.jGrowl("Error " + action + ": " + data.errors.join(", "));
} else {
$.jGrowl("We had trouble " + action + " just now. Try again?");
}
}
function objectRemoved(objectWrapper) {
objectWrapper.hide(250, function () {
objectWrapper.remove();
updateBulkActions();
});
}
function compareItemsByName(a, b) {
return a.find("span.name").text().localeCompare(b.find("span.name").text());
}
function findList(owned, id, item) {
if (id) {
return $("#closet-list-" + id);
} else {
return $(
"div.closet-hangers-group[data-owned=" +
owned +
"] div.closet-list.unlisted",
);
}
}
function updateListHangersCount(el) {
el.attr("data-hangers-count", el.find("div.object").length);
}
function moveItemToList(item, owned, listId) {
var newList = findList(owned, listId, item);
var oldList = item.closest("div.closet-list");
var hangersWrapper = newList.find("div.closet-list-hangers");
item.insertIntoSortedList(hangersWrapper, compareItemsByName);
updateListHangersCount(oldList);
updateListHangersCount(newList);
}
function submitUpdateForm(form) {
if (form.data("loading")) return false;
var quantityEl = form.children("input[name=closet_hanger[quantity]]");
var ownedEl = form.children("input[name=closet_hanger[owned]]");
var listEl = form.children("input[name=closet_hanger[list_id]]");
var listChanged = ownedEl.hasChanged() || listEl.hasChanged();
if (listChanged || quantityEl.hasChanged()) {
var objectWrapper = form.closest(".object").addClass("loading");
var newQuantity = quantityEl.val();
var quantitySpan = objectWrapper.find(".quantity span").text(newQuantity);
objectWrapper.attr("data-quantity", newQuantity);
var data = form.serialize(); // get data before disabling inputs
objectWrapper.disableForms();
form.data("loading", true);
if (listChanged)
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
$.ajax({
url: form.attr("action") + ".json",
type: "post",
data: data,
dataType: "json",
complete: function (data) {
if (quantityEl.val() == 0) {
objectRemoved(objectWrapper);
} else {
objectWrapper.removeClass("loading").enableForms();
}
form.data("loading", false);
},
success: function () {
// Now that the move was successful, let's merge it with any
// conflicting hangers
var id = objectWrapper.attr("data-item-id");
var conflictingHanger = findList(
ownedEl.val(),
listEl.val(),
objectWrapper,
)
.find("div[data-item-id=" + id + "]")
.not(objectWrapper);
if (conflictingHanger.length) {
var conflictingQuantity = parseInt(
conflictingHanger.attr("data-quantity"),
10,
);
var currentQuantity = parseInt(newQuantity, 10);
var mergedQuantity = conflictingQuantity + currentQuantity;
quantitySpan.text(mergedQuantity);
quantityEl.val(mergedQuantity);
objectWrapper.attr("data-quantity", mergedQuantity);
conflictingHanger.remove();
}
quantityEl.storeValue();
ownedEl.storeValue();
listEl.storeValue();
updateBulkActions();
},
error: function (xhr) {
quantityEl.revertValue();
ownedEl.revertValue();
listEl.revertValue();
if (listChanged)
moveItemToList(objectWrapper, ownedEl.val(), listEl.val());
quantitySpan.text(quantityEl.val());
handleSaveError(xhr, "updating the quantity");
},
});
}
}
$(hangersElQuery + " form.closet-hanger-update").live("submit", function (e) {
e.preventDefault();
submitUpdateForm($(this));
});
function editableInputs() {
return $(hangersElQuery).find(
"input[name=closet_hanger[quantity]], " +
"input[name=closet_hanger[owned]], " +
"input[name=closet_hanger[list_id]]",
);
}
$(hangersElQuery + "input[name=closet_hanger[quantity]]")
.live("change", function () {
submitUpdateForm($(this).parent());
})
.storeValue();
onHangersInit(function () {
editableInputs().storeValue();
});
$(hangersElQuery + " div.object")
.live("mouseleave", function () {
submitUpdateForm($(this).find("form.closet-hanger-update"));
})
.liveDraggable({
appendTo: "#closet-hangers",
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) {
var checkboxes = $(this)
.closest(".closet-list")
.find(".object input[type=checkbox]");
var allChecked = true;
checkboxes.each(function () {
if (!this.checked) {
allChecked = false;
return false;
}
});
checkboxes.attr("checked", !allChecked);
updateBulkActions(); // setting the checked prop doesn't fire change events
});
function getCheckboxes() {
return $(hangersElQuery + " input[type=checkbox]");
}
function getCheckedIds() {
var checkedIds = [];
getCheckboxes()
.filter(":checked")
.each(function () {
if (this.checked) checkedIds.push(this.id);
});
return checkedIds;
}
getCheckboxes().live("change", updateBulkActions);
function updateBulkActions() {
var checkedCount = getCheckboxes().filter(":checked").length;
$(".bulk-actions").attr("data-target-count", checkedCount);
$(".bulk-actions-target-count").text(checkedCount);
}
$(".bulk-actions-move-all").bind("submit", function (e) {
// TODO: DRY
e.preventDefault();
var form = $(this);
var data = form.serializeArray();
data.push({
name: "return_to",
value: window.location.pathname + window.location.search,
});
var checkedBoxes = getCheckboxes().filter(":checked");
checkedBoxes.each(function () {
data.push({
name: "ids[]",
value: $(this).closest(".object").attr("data-id"),
});
});
$.ajax({
url: form.attr("action"),
type: form.attr("method"),
data: data,
success: function (html) {
var doc = $(html);
hangersEl.html(doc.find("#closet-hangers").html());
hangersInit();
updateBulkActions(); // don't want to maintain checked; deselect em all
doc
.find(".flash")
.hide()
.insertBefore(hangersEl)
.show(500)
.delay(5000)
.hide(250);
itemsSearchField.val("");
},
error: function (xhr) {
handleSaveError(xhr, "moving these items");
},
});
});
$(".bulk-actions-remove-all").bind("submit", function (e) {
e.preventDefault();
var form = $(this);
var hangerIds = [];
var checkedBoxes = getCheckboxes().filter(":checked");
var hangerEls = $();
checkedBoxes.each(function () {
hangerEls = hangerEls.add($(this).closest(".object"));
});
hangerEls.each(function () {
hangerIds.push($(this).attr("data-id"));
});
$.ajax({
url: form.attr("action") + ".json?" + $.param({ ids: hangerIds }),
type: "delete",
dataType: "json",
success: function () {
objectRemoved(hangerEls);
},
error: function () {
$.jGrowl("Error removing items. Try again?");
},
});
});
$(".bulk-actions-deselect-all").bind("click", function (e) {
getCheckboxes().filter(":checked").attr("checked", false);
updateBulkActions();
});
function maintainCheckboxes(fn) {
var checkedIds = getCheckedIds();
fn();
checkedIds.forEach(function (id) {
document.getElementById(id).checked = true;
});
updateBulkActions();
}
/*
Search, autocomplete
*/
var itemsSearchForm = $("#closet-hangers-items-search[data-current-user-id]");
var itemsSearchField = itemsSearchForm.children("input[name=q]");
itemsSearchField.autocomplete({
select: function (e, ui) {
if (ui.item.is_item) {
// Let the autocompleter finish up this search before starting a new one
setTimeout(function () {
itemsSearchField.autocomplete("search", ui.item);
}, 0);
} else {
var item = ui.item.item;
var group = ui.item.group;
itemsSearchField.addClass("loading");
var closetHanger = {
owned: group.owned,
list_id: ui.item.list ? ui.item.list.id : "",
};
if (!item.hasHanger) closetHanger.quantity = 1;
$.ajax({
url:
"/user/" +
itemsSearchForm.data("current-user-id") +
"/items/" +
item.id +
"/closet_hangers",
type: "post",
data: {
closet_hanger: closetHanger,
return_to: window.location.pathname + window.location.search,
},
complete: function () {
itemsSearchField.removeClass("loading");
},
success: function (html) {
var doc = $(html);
maintainCheckboxes(function () {
hangersEl.html(doc.find("#closet-hangers").html());
hangersInit();
});
doc
.find(".flash")
.hide()
.insertBefore(hangersEl)
.show(500)
.delay(5000)
.hide(250);
itemsSearchField.val("");
},
error: function (xhr) {
handleSaveError(xhr, "adding the item");
},
});
}
},
source: function (input, callback) {
if (typeof input.term == "string") {
// user-typed query
$.getJSON("/items.json?q=" + input.term, function (data) {
var output = [];
var items = data.items;
for (var i in items) {
items[i].label = items[i].name;
items[i].is_item = true;
output[output.length] = items[i];
}
callback(output);
});
} else {
// item was chosen, now choose a group to insert
var groupInserts = [],
group;
var item = input.term,
itemEl,
occupiedGroups,
hasHanger;
for (var i in hangerGroups) {
group = hangerGroups[i];
itemEl = $(
"div.closet-hangers-group[data-owned=" +
group.owned +
"] div.object[data-item-id=" +
item.id +
"]",
);
occupiedGroups = itemEl.closest(".closet-list");
hasHanger = occupiedGroups.filter(".unlisted").length > 0;
groupInserts[groupInserts.length] = {
group: group,
item: item,
label: item.label,
hasHanger: hasHanger,
};
for (var i = 0; i < group.lists.length; i++) {
hasHanger =
occupiedGroups.filter("[data-id=" + group.lists[i].id + "]")
.length > 0;
groupInserts[groupInserts.length] = {
group: group,
item: item,
label: item.label,
list: group.lists[i],
hasHanger: hasHanger,
};
}
}
callback(groupInserts);
}
},
});
var autocompleter = itemsSearchField.data("autocomplete");
autocompleter._renderItem = function (ul, item) {
var li = $("<li></li>").data("item.autocomplete", item);
if (item.is_item) {
// these are items from the server
$("#autocomplete-item-tmpl").tmpl({ item_name: item.label }).appendTo(li);
} else if (item.list) {
// these are list inserts
var listName = item.list.label;
if (item.hasHanger) {
$("#autocomplete-already-in-collection-tmpl")
.tmpl({ collection_name: listName })
.appendTo(li);
} else {
$("#autocomplete-add-to-list-tmpl")
.tmpl({ list_name: listName })
.appendTo(li);
}
li.addClass("closet-list-autocomplete-item");
} else {
// these are group inserts
var groupName = item.group.label;
if (!item.hasHanger) {
$("#autocomplete-add-to-group-tmpl")
.tmpl({ group_name: groupName.replace(/\s+$/, "") })
.appendTo(li);
} else {
$("#autocomplete-already-in-collection-tmpl")
.tmpl({ collection_name: groupName })
.appendTo(li);
}
li.addClass("closet-hangers-group-autocomplete-item");
}
return li.appendTo(ul);
};
/*
Contact Neopets username form
*/
var contactEl = $("#closet-hangers-contact");
var contactForm = contactEl.children("form");
var contactField = contactForm.children("select");
var contactAddOption = $("<option/>", {
text: contactField.attr("data-new-text"),
value: -1,
});
contactAddOption.appendTo(contactField);
var currentUserId = $("meta[name=current-user-id]").attr("content");
function submitContactForm() {
var data = contactForm.serialize();
contactForm.disableForms();
$.ajax({
url: contactForm.attr("action") + ".json",
type: "post",
data: data,
dataType: "json",
complete: function () {
contactForm.enableForms();
},
error: function (xhr) {
handleSaveError(xhr, "saving Neopets username");
},
});
}
contactField.change(function (e) {
if (contactField.val() < 0) {
var newUsername = $.trim(
prompt(contactField.attr("data-new-prompt"), ""),
);
if (newUsername) {
$.ajax({
url: "/user/" + currentUserId + "/neopets-connections",
type: "POST",
data: { neopets_connection: { neopets_username: newUsername } },
dataType: "json",
success: function (connection) {
var newOption = $("<option/>", {
text: newUsername,
value: connection.id,
});
newOption.insertBefore(contactAddOption);
contactField.val(connection.id);
submitContactForm();
},
});
}
} else {
submitContactForm();
}
});
/*
Hanger list controls
*/
$("input[type=submit][data-confirm]").live("click", function (e) {
if (!confirm(this.getAttribute("data-confirm"))) e.preventDefault();
});
/*
Closet list droppable
*/
onHangersInit(function () {
$("div.closet-list").droppable({
accept: "div.object",
activate: function () {
$(this)
.find(".closet-list-content")
.animate({ opacity: 0, height: 100 }, 250);
},
activeClass: "droppable-active",
deactivate: function () {
$(this)
.find(".closet-list-content")
.css("height", "auto")
.animate({ opacity: 1 }, 250);
},
drop: function (e, ui) {
var form = ui.draggable.find("form.closet-hanger-update");
form
.find("input[name=closet_hanger[list_id]]")
.val(this.getAttribute("data-id"));
form
.find("input[name=closet_hanger[owned]]")
.val($(this).closest(".closet-hangers-group").attr("data-owned"));
submitUpdateForm(form);
},
});
});
/*
Visibility Descriptions
*/
function updateVisibilityDescription() {
var descriptions = $(this)
.closest(".visibility-form")
.find("ul.visibility-descriptions");
descriptions.children("li.current").removeClass("current");
descriptions
.children("li[data-id=" + $(this).val() + "]")
.addClass("current");
}
function visibilitySelects() {
return $("form.visibility-form select");
}
visibilitySelects().live("change", updateVisibilityDescription);
onHangersInit(function () {
visibilitySelects().each(updateVisibilityDescription);
});
/*
Help
*/
$("#toggle-help").click(function () {
$("#closet-hangers-help").toggleClass("hidden");
});
/*
Share URL
*/
$("#closet-hangers-share-box")
.mouseover(function () {
$(this).focus();
})
.mouseout(function () {
$(this).blur();
});
/*
Initialize
*/
hangersInit();
})();