amazonのほしい物リストの並び替えGreasemonkeyスクリプト


  • アイテムを消去しておいて同じ内容のアイテムを追加するという方法になっております
  • 最初は他のリストに移動してまた移動し直すという方法だったのですが、移動するにもアイテムに付けられたコメントなどの情報を引数として渡す必要があること、そして移動は「消去済みフラグのON + 同じ内容のアイテムを別のリストへ新規投稿」という実装になっていることに気がついて、それなら他のリストを経由しない方が簡便だと気づきました。
  • amazonのJSがきれいに書かれていなくて操作を行うための引数を構築する部分のコードをコピペすることになりました…
  • Q. Autopagerizeなどで追加された2ページ目以降のアイテムもうまく扱えるようにできるか? A. 試していないが、おそらく「なんでも欲しいものリスト」で追加されたアイテムは情報が欠落してしまうと予想する
  • あっ、自分のほしい物リストの一覧は最初は消しておいたのだけど、リロードを押したときに表示されてしまっている>< あまり見ないでください
  • (2013/5/2追記) 必要な個数だけしか追加し直しをしないようにしました
  • (2013/8/29追記) 開いているページのURLにリストのIDが含まれない場合にうまく動作しなかった問題を直しました
// ==UserScript==
// @name        sort amazon wishlist
// @namespace   http://example.com/
// @include     http://www.amazon.co.jp/wishlist/*
// @include     http://www.amazon.co.jp/*/wishlist/*
// @version     1
// ==/UserScript==

var $ = unsafeWindow.jQuery;

$.when(
	$.getScript("http://code.jquery.com/ui/1.10.2/jquery-ui.js"),
	$.getScript("https://raw.github.com/cho45/jsdeferred/master/jsdeferred.jquery.js")
).then(function() {
	$.JSDeferred.define($);
	SortableUI.setup();
	Sorter.setup();
}).fail(function (x) {
	alert(x);
});

var Sorter = {
	origItems: null,
	setup: function () {
		Sorter.origItems = Sorter.getItems();
		console.log(Sorter.origItems.length);
	},
	run: function () {
		var items = Sorter.itemsToReadd();
		var thisListID = unsafeWindow.Registry.view.rid;

		return $.loop(items.length, function(i) {
			var item = $(items[i]);
			Widget.echo("sorting: "+i+" / "+items.length);
			// ウェイトを入れないとうまく並び替わらない
			// 追加された日時は秒単位で記録されるのかもしれない
			return $.wait(1).next(function() {
				return Helper.deleteItem(item);
			}).next(function() {
				return Helper.moveToList(item, thisListID);
			})
		}).next(function() {
			Widget.echo("done.");
		}).error(function (x) {
			alert(x);
		});
	},
	itemsToReadd: function () {
		var items = Sorter.getItems();
		var origItems = Sorter.origItems;
		if (origItems && items.length == origItems.length) {
			return Sorter.selectItemsToReadd(items, origItems).reverse();
		} else {
			return items.reverse();
		}
	},
	selectItemsToReadd: function (items, orig) {
		var i = Sorter.minInteger(items.length, function (i) items[i] != orig[i]);
		if (i != null) {
			var head = items.slice(0, i+1);
			var itemsp = items.slice(i+1);
			var origp = orig.filter(function (e) head.indexOf(e) < 0);
			return head.concat(Sorter.selectItemsToReadd(itemsp, origp));
		} else {
			return [];
		}
	},
	minInteger: function (n, fn) {
		for (var i = 0; i < n; i ++) {
			if (fn(i)) return i;
		}
		return null;
	},
	getItems: function () {
		return $(".itemWrapper").toArray();
	}

};

var SortableUI = {
	SELECTOR: ".list-items table.wlrdZeroTable",
	setup: function() {
		$("<style>").text(".placeholder td { background-color: #afeeee; height: 10px; }").appendTo("head");
		SortableUI.sortable($(SortableUI.SELECTOR));
		SortableUI.setupAutoPagerize();

	},
	setupAutoPagerize: function () {
		// 次ページに含まれるなんでも欲しいものリストで追加されたアイテムはうまく動かないであろう
		// 次ページが追加される前に1ページ目で要素の並び替えをすでに行っている場合を未考慮
		document.body.addEventListener("AutoPagerize_DOMNodeInserted", function(evt) {
			console.log("inserted");
			var node = evt.target;
			SortableUI.sortable($(node));
			Sorter.setup();
		}, false);
	},
	sortable: function(x) {
		x.sortable({
			connectWith: SortableUI.SELECTOR,
			// デフォルトのplaceholderはtbodyをうまく扱ってくれないので自前で用意
			placeholder: {
				element: function(currentItem) {
					return $("<tbody class=placeholder><tr><td colspan=7></td></tr></tbody>");
				},
				update: function() {}
			},
			update: function (event, ui) {
				Widget.setup();
			}
		});
	}
}

var Widget = {
	$widget: null,
	setup: function () {
		if (Widget.$widget) return;
		Widget.$widget = $("<div/>").css({
			backgroundColor: "rgba(0,0,0,0.5)",
			color: "white",
			width: "200px",
			height: "50px",
			padding: "5px",
			textAlign: "center",
			display: "table-cell",
			verticalAlign: "middle"
		});
		$("<div/>").css({
			position: "fixed",
			top: "10px",
			right: "10px",
			zIndex: "91",
		}).append(Widget.$widget).appendTo("body");

		$("<button/>").text("並び替えを確定する").click(Sorter.run).appendTo(Widget.$widget);
	},
	echo: function (msg) {
		Widget.$widget.empty().text(msg);
	}
};

var Helper = {
	deleteItem: function (container) {
		var deferred = $.JSDeferred();
		var arg = Helper.buildArgumentToMove(container, "");
		console.log(arg);
		unsafeWindow.Registry.update("deleteItem", {itemID: arg.itemID}, function (res) {
			console.log(res);
			if (res && res.ok) {
				deferred.call(res);
			} else {
				deferred.fail(res);
			}
		});
		return deferred;
	},
	moveToList: function (container, toListID) {
		var deferred = $.JSDeferred();
		var arg = Helper.buildArgumentToMove(container, toListID);
		console.log(arg);
		unsafeWindow.Registry.update('movetolist', arg, function (res) {
			console.log(res);
			if (res && res.ok) {
				deferred.call(res);
			} else {
				deferred.fail(res);
			}
		});
		return deferred;
	},
	// 以下のコードはcreateMovePopover()から拝借して改変
	buildArgumentToMove: function (container, toListID) {
		var containerNameParts = container.attr('name').split(".");
		if( containerNameParts.length < 3 ) {
			return true;
		}
		var itemID = containerNameParts[2];
		var asin = $.trim(containerNameParts[3]);
		var currencyCode, savedPrice, productUrl, imageUrl, purchasedQty, requestedQty, itemComments, priority, purchaseDate, wfaItem, matchedAsin;
		var name = getProductTitleText(container);
		purchaseDate = container.find("input[name='purchaseDate." + itemID + "']").val();
		requestedQty = container.find("input[name^='requestedQty.']").val();
		purchasedQty = container.find("input[name^='purchasedQty']").val();
		itemComment = $.trim(container.find("input[name^='itemComment.']").val());
		priority = container.find("select[name^='priority.']").val();

		if (asin == undefined || asin == null || asin == '') {
			currencyCode = $("#hidItemData").find("input[name='currencyCode." + itemID + "']").val();
			savedPrice = $("#hidItemData").find("input[name='savedPrice." + itemID + "']").val();
			var image = container.find(".productImage").children("a");
			productUrl = image.attr('href');
			imageUrl = $("#hidItemData").find("input[name='imageUrl." + itemID + "']").val();
			if (container.find(".wfaItemTitle").length > 0) {
				wfaItem = 1;
			} else {
				wfaItem = 0;
			}
			matchedAsin = $("#hidItemData").find("input[name='uwlAsinMatchAvailable." + itemID + "']").val(); 
			asin = undefined;
		}
		var data = { 
			itemID      : itemID, 
			toListID    : toListID,
			requestedQty: requestedQty,
			purchasedQty: purchasedQty,
			priority :    priority,
			ref_ : 'cm_wl_js_move'
		};

		if (itemComment) { data.itemComment = itemComment; }

		if (asin) { 
			data.asin = asin; 
		} else {
			if (name) { data.name = name; }
			if (savedPrice) { data.savedPrice = savedPrice; }
			if (currencyCode) { data.currencyCode = currencyCode; }
			if (productUrl) { data.productUrl = productUrl; }
			if (imageUrl) { data.imageUrl = imageUrl; }
			if (wfaItem) {
				data.wfaItem = wfaItem;
				data.name = getProductTitleText(container);
			}
			if (matchedAsin) {
				data.matchedAsin = matchedAsin;
				data.matchedAsinBoxDismissed = $("#hidItemData").find("input[name='uwlAsinMatchBoxDismissed." + itemID + "']").val();
			}
		}

		if (purchaseDate) {
			data.purchaseDate = purchaseDate;
		}

		return data;

		function getProductTitleText(container){
			var productTitleClone = container.find(".productTitle").clone();
			productTitleClone.find(".s_extLink").remove();
			return $.trim(productTitleClone.text());
		}
	}
};
筆者: oupo (連絡先: oupo.nejiki@gmail.com)