(function () { function addCSRFToken(xhr) { const token = document .querySelector('meta[name="csrf-token"]') ?.getAttribute("content"); xhr.setRequestHeader("X-CSRF-Token", token); } var hangersInitCallbacks = []; function onHangersInit(callback) { hangersInitCallbacks[hangersInitCallbacks.length] = callback; } function hangersInit() { for (var i = 0; i < hangersInitCallbacks.length; i++) { try { hangersInitCallbacks[i](); } catch (error) { console.error(error); } } } /* Hanger groups */ var hangerGroups = []; $(".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", }; }); $(".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"); }); // Read the item IDs of trade matches from the meta tags. const ownedIds = document .querySelector("meta[name=trade-matches-owns]") ?.getAttribute("value") ?.split(",") ?? []; const wantedIds = document .querySelector("meta[name=trade-matches-wants]") ?.getAttribute("value") ?.split(",") ?? []; // Apply the `user-owns` and `user-wants` classes to the relevant entries. // This both provides immediate visual feedback, and sets up "Compare with // Your Items" to toggle to just them! // // NOTE: The motivation here is caching: this allows us to share a cache of // the closet list contents across all users, without `user-owns` or // `user-wants` classes for one specific user getting cached and reused. const hangerEls = document.querySelectorAll("#closet-hangers .object"); for (const hangerEl of hangerEls) { const itemId = hangerEl.getAttribute("data-item-id"); if (ownedIds.includes(itemId)) { hangerEl.classList.add("user-owns"); } if (wantedIds.includes(itemId)) { hangerEl.classList.add("user-wants"); } } /* 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 .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 = $("", { for: checkboxId }); var link = hangerEl.children("a"); link.children(":not(.name)").detach().appendTo(label); link.detach().appendTo(label); var checkbox = $("", { 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 $( ".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", beforeSend: addCSRFToken, 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", beforeSend: addCSRFToken, 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, beforeSend: addCSRFToken, 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", beforeSend: addCSRFToken, 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, }, beforeSend: addCSRFToken, 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 = $( ".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 = $("
").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 = $("", { 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", beforeSend: addCSRFToken, 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", beforeSend: addCSRFToken, success: function (connection) { var newOption = $("", { text: newUsername, value: connection.id, }); newOption.insertBefore(contactAddOption); contactField.val(connection.id); submitContactForm(); }, }); } } else { submitContactForm(); } }); /* 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(); })();