View file esoTalk-1.0.0g4/core/js/search.js

File size: 15.61Kb
// Search (conversation list) JavaScript.

var ETSearch = {

// The current search details.
currentSearch: "",
currentChannels: [],

// References to search form elements.
form: null,
formInput: null,
formReset: null,

updateInterval: null,

// Initialize the search page.
init: function() {

	// Set the current channel and search query.
	if (ET.currentChannels) ETSearch.currentChannels = ET.currentChannels;
	if (ET.currentSearch) ETSearch.currentSearch = ET.currentSearch;


	// INITIALIZE THE SEARCH FORM.

	// Get the search form elements.
	ETSearch.form = $("#search");
	ETSearch.formInput = $("#search .text");
	ETSearch.formReset = $("#search .control-reset");

	new ETAutoCompletePopup(ETSearch.formInput, "author:");
	new ETAutoCompletePopup(ETSearch.formInput, "contributor:");

	// Make the controls into a popup button.
	if ($("#searchControls").length) {
		$("#search fieldset").append($("#searchControls").popup({alignment: "right"}));
		$("#search").addClass("hasControls");
	}

	// Add an onclick handler to the search button to perform a search.
	ETSearch.form.submit(function(e) {
		ETSearch.search(ETSearch.formInput.val());
		e.preventDefault();
	});

	// Add a key press handler to clear the search input when escape is pressed.
	ETSearch.formInput.keydown(function(e) {
		if (e.which != 27) return;

		// If the value isn't empty, clear it and focus on the input.
		if (ETSearch.formInput.val() != "") {
			ETSearch.search("");
			ETSearch.formInput.focus();
		}
		// If it is already empty, unfocus from the input.
		else ETSearch.formInput.blur();
		e.preventDefault();
	})

	// Add a key press handler to make the 'x' button visible if text has been entered or have previously been entered.
	.keyup(function(e) {
		ETSearch.formReset.css("visibility", (ETSearch.formInput.val() != "" || ETSearch.currentSearch != "") ? "" : "hidden");
	})

	// Add a handler to show the gambits section when the search input is active.
	.focus(function() {
		var input = $("#search input.text");
		$("#gambits").addClass("popup").css({
			position: "absolute",
			top: input.offset().top + input.outerHeight() + 5,
			left: input.offset().left
		}).fadeIn("fast");
	});

	// If the search input is blank, hide the reset 'x' button.
	if (!ETSearch.currentSearch) ETSearch.formReset.css("visibility", "hidden");

	// Add a click handler to the reset 'x' button.
	ETSearch.formReset.click(function(e) {
		ETSearch.search("");
		ETSearch.formInput.focus();
		e.preventDefault();
	});


	// INITIALIZE THE GAMBITS.

	// Hide the gambits area.
	$("#gambits").hide();

	// The gambits area should hide when the search input loses focus.
	ETSearch.formInput.blur(function() {
		$("#gambits").fadeOut("fast");
	});
	// However, prevent the search input from losing focus if a click takes place on the gambits popup.
	$("#gambits").mousedown(function(e) {
		e.preventDefault();
	});

	// Add click and double click handlers to all the gambits.
	$("#gambits a").click(function(e) {
		e.preventDefault();
		ETSearch.gambit(desanitize($(this).data("gambit")), e.shiftKey);
		ETSearch.formInput.keyup();
	}).dblclick(function(e) {
		e.preventDefault();
		ETSearch.search((e.shiftKey ? "!" : "") + "#" + desanitize($(this).data("gambit")));
		ETSearch.formInput.blur().keyup();
	})

	// Prevent the search field from being unfocussed when a gambit is clicked.
	.bind("mousedown", function(e) {
		e.preventDefault();
	});


	// INITIALIZE THE REST OF THE PAGE.

	// Run a callback that will update search results every so often.
	ETSearch.updateInterval = new ETIntervalCallback(ETSearch.update, ET.searchUpdateInterval);

	// Add tooltips to the channels, and give them click handlers.
	$("#channels a:not(.channel-list)").tooltip({alignment: "left", delay: 250, offset: [0, 0], className: "withArrow withArrowBottom"});
	$("#channels a.channel-list").tooltip();

	// When the hash in the URL changes, update the search interface.
	$(document).bind("statechange", function(event, hash) {

		// We'll get the hash in the format "conversations/channel-slug?search=whatever".
		var parts = hash.split("?");
		var channelParts = parts[0].split("/");
		if (!channelParts[1]) channelParts[1] = "all";
		if (!parts[1]) parts[1] = "";
		var newChannel = decodeURIComponent(channelParts[1]);
		var newSearch = decodeURIComponent(parts[1].replace("search=", ""));
		var oldChannel = ETSearch.getCurrentChannelSlugs().join("+");

		// If either the search or the channel has changed, update accordingly.
        if (ETSearch.currentSearch != newSearch || oldChannel != newChannel) {
			if (oldChannel != newChannel) ETSearch.changeChannel(newChannel);
			else ETSearch.search(newSearch);
		}
	});

	// Save the scroll position and the conversation ID whenever a conversation link is clicked on.
	$("#conversations a").live("click", function() {
		$.cookie("scrollTop", $(document).scrollTop(), {path: "/"});
		$.cookie("cid", ETSearch.getConversationIdForElement(this), {path: "/"});
	});

	// When the page loads, scroll to a position saved in a cookie (if any), and highlight the conversation that was just visited.
	setTimeout(function() {
		var scrollTop = $.cookie("scrollTop"),
			cid = $.cookie("cid");
		if (scrollTop) $.scrollTo(scrollTop);
		if (cid) $("#c" + cid).addClass("justVisited");
		$.cookie("scrollTop", null, {path: "/"});
		$.cookie("cid", null, {path: "/"});
	}, 1);

	// Add click handlers to the unread indicators.
	$("#conversations .unreadIndicator").live("click", function(e) {
		e.preventDefault();
		ETSearch.markAsRead(ETSearch.getConversationIdForElement(this));
		$.hideToolTip();
	});

	// Add click handlers to the channels.
	$("#conversations .channel").live("click", function(e) {
		ETSearch.changeChannel($(this).data("channel"));
		e.preventDefault();
	});

	// Initialize the search results.
	ETSearch.initSearchResults();

	// Add a click handler to the mark all as read button.
	$("#control-markAllAsRead").live("click", function(e) {
		e.preventDefault();
		ETSearch.currentSearch = "";
		ETSearch.changeChannel("all", false, true);
	});
	$("#control-markListedAsRead").live("click", function(e) {
		e.preventDefault();
		ETSearch.search(ETSearch.currentSearch, "markAsRead");
	});

	// Add a click handler to the view more button.
	$("#conversations .viewMore a").live("click", function(e) {
		e.preventDefault();
		ETSearch.search(decodeURIComponent($(this).attr("href").split("?search=")[1].replace(/\+/g, ' ')));
	});

	// Add click handlers to the channels.
	$("#channels a:not(.channel-list)").live("click", function(e) {
		if (e.metaKey || e.ctrlKey) return;
		e.preventDefault();
		ETSearch.changeChannel($(this).data("channel"), e.shiftKey);
	});
},

// Given an element within a conversation row, get the ID of its parent conversation.
getConversationIdForElement: function(elm) {
	elm = $(elm);
	var id = elm.is("li") ? elm.attr("id") : elm.parents("li").attr("id");
	return id ? id.substr(1) : null;
},

// Initialize the search results.
initSearchResults: function() {

	// Make all "private" labels show a list of members allowed when they are moused over.
	ETMembersAllowedTooltip.init($("#conversations .label.label-private"), function(elm) {return ETSearch.getConversationIdForElement(elm)});
	ETMembersAllowedTooltip.showDelay = 500;

	$("#conversations .starButton").tooltip();
	$("#conversations .unreadIndicator").tooltip();
	$("#conversations .label").tooltip();

},

// Mark a single conversation as read, hiding its unread indicator.
markAsRead: function(conversationId) {
	$.ETAjax({
		url: "conversation/read.json/" + conversationId,
		global: true,
		success: function(data) {
			var row = $("#c" + conversationId);
			$(row).removeClass("unread");
			$(".unreadIndicator", row).remove();
		}
	});
},

// Change the channel.
changeChannel: function(channel, addChannel, markAllAsRead) {

	// Hide the tooltip and unselect all channels in the list.
	$.hideToolTip();
	$("#channels li:not(.pathItem)").removeClass("selected").find("a").removeClass("channel");

	// Find the channel ID that corresponds to the provided slug.
	var newChannel = null;
	for (var i in ET.channels) {
		if (ET.channels[i] == channel) {
			newChannel = i;
			break;
		}
	}

	// If we're adding this channel to the selection...
	if (addChannel) {

		// If we're not in "multi-select mode" (where the first channel is blank), make it so we are.
		if (ETSearch.currentChannels[0] != "") ETSearch.currentChannels = [""];

		// If this channel is already selected, we want to remove it from the selection.
		var k = ETSearch.currentChannels.indexOf(newChannel);
		if (k != -1) ETSearch.currentChannels.splice(k, 1);

		// Otherwise, add it.
		else ETSearch.currentChannels.push(newChannel);

	}

	// If we found a channel ID, change the selected channels to just this one.
	else if (newChannel) ETSearch.currentChannels = [newChannel];

	// Otherwise, we can assume "all channels" was clicked, in which case we clear the selected channels.
	else ETSearch.currentChannels = [];

	// If one or more channels are selected, highlight them in the channel breadcrumb area.
	if (ETSearch.currentChannels.length) {
		for (var i in ETSearch.currentChannels) {
			$("#channels .channel-"+ETSearch.currentChannels[i]).parent().addClass("selected").not(".pathItem").find("a").addClass("channel");
		}
	}

	// Perform the search.
	ETSearch.search(ETSearch.currentSearch, markAllAsRead ? "markAllAsRead" : "");
},

// Get a list of slugs of the currently selected channels.
getCurrentChannelSlugs: function() {
	var slugs = [];
	if (ETSearch.currentChannels.length) {
		for (var i in ETSearch.currentChannels) {
			if (ET.channels[ETSearch.currentChannels[i]]) slugs.push(encodeURIComponent(ET.channels[ETSearch.currentChannels[i]]));
			else slugs.push("");
		}
	}
	else slugs = ["all"];

	return slugs;
},

// Perform a search.
search: function(query, customMethod) {

	// Hide the gambits popup.
	$("#gambits").fadeOut("fast");

	// Set the current search and the form input value.
	ETSearch.currentSearch = ETSearch.formInput.val(query).val();

	// If the search input is blank, hide the reset 'x' button.
	ETSearch.formReset.css("visibility", ETSearch.currentSearch ? "visible" : "hidden");

	// Get the channel slugs and join them together so we can put them in a URL.
	var channelString = ETSearch.getCurrentChannelSlugs().join("+");

	// Create a history entry so we can use the back button even though we're making an AJAX request.
	$.history.load("conversations/"+channelString+(query ? "?search="+encodeURIComponent(query) : ""), true);

	// Clear the results update timeout.
	ETSearch.updateInterval.reset();

	// Make the request.
	$.ETAjax({
		id: "search",
		url: "conversations/"+(customMethod ? customMethod+".ajax" : "index.ajax")+"/"+channelString,
		type: "post",
		global: false,
		data: {search: query},
		success: function(data) {

			// If messages were returned, don't update the results.
			if (data.messages) return;

			// Display the new results.
			$("#conversations").html(data.view);

			// Update the channels and re-initialize everything.
			ETSearch.updateChannels(data.channels);
			ETSearch.initSearchResults();
			ETMessages.hideMessage("search");

		},
		beforeSend: function() {
			createLoadingOverlay("conversations", "conversations");
		},
		complete: function() {
			hideLoadingOverlay("conversations", false);
		}
	});
},

// Update the channel breadcrumb area, animating the old channels to their new positions.
updateChannels: function(newChannels) {

	// Save the positional coordinates of all <a> tags.
	var positions = {};
	$("#channels a").each(function() {
		var classes = $(this).prop("className").split(" ");
		for (var i in classes) {
			if (classes[i].indexOf("channel-") != -1) {
				positions[classes[i]] = $(this).offset().left;
				return;
			}
		}
	});

	// Remove all of the channels (short of the channel list icon,) and add the new ones.
	$("#channels li:not(:first-child)").remove();
	$("#channels").append(newChannels);

	// Restore the old positional coordinates for all of the <a> tags, and then animate them to their new positions.
	$("#channels a").each(function() {

		// Read the channel's className to find the old position of the same channel.
		var classes = $(this).prop("className").split(" ");
		for (var i in classes) {

			// If this class is a "channel-x" class and we have a position saved for it...
			if (typeof classes[i] == "string" && classes[i].indexOf("channel-") != -1 && positions[classes[i]]) {
				var newPos = $(this).offset().left;
				$(this).css("position", "relative").css("left", -newPos + positions[classes[i]]).animate({left: 0}, "fast");
				return;
			}
		}

		// If we didn't find any matching positions, animate it moving in from the left and fading in.
		$(this).css("position", "relative").css("left", -100).css("opacity", 0).animate({left: 0, opacity: 1}, "fast");
	});
},

// Update the current search results with new post counts, last post times, etc.
update: function() {

	// Construct a list of conversation IDs for which to get updated details.
	var conversationIds = "";
	var count = Math.min($("#conversations li").length, 20);
	$("#conversations li").each(function(i, row) {
		if (i > count) return false;
		conversationIds += ETSearch.getConversationIdForElement(row) + ",";
	});

	// Get the channel slugs and join them together so we can put them in a URL.
	var channelString = ETSearch.getCurrentChannelSlugs().join("+");

	// Make an ajax request.
	$.ETAjax({
		url: "conversations/update.ajax/"+channelString+"/"+encodeURIComponent(ETSearch.currentSearch),
		type: "post",
		global: false,
		data: {conversationIds: conversationIds},
		success: function(data) {
			if (!data.conversations) return;

			// For each of the conversation rows returned, replace them in the results table.
			for (var i in data.conversations) {
				if (!$("#c"+i).length) continue;
				$("#c"+i).replaceWith(data.conversations[i]);
			}
			ETSearch.initSearchResults();
		}
	});
},

// Show new activity - an alias for reperforming the current search.
showNewActivity: function() {
	ETSearch.search(ETSearch.currentSearch);
	ETMessages.hideMessage("newSearchResults");
},

// Add (or take away) a gambit from the search input.
gambit: function(gambit, negative) {

	// Prepend a hashtag to the gambit.
	gambit = "#"+gambit;

	// Get the initial length of the search text.
	var initialLength = $.trim(ETSearch.formInput.val()).length;

	// Make a regular expression to find any instances of the gambit already in there.
	var safe = gambit.replace(/([?^():\[\]])/g, "\\$1");
	var regexp = new RegExp(negative
		? "( ?(- *|!)" + safe + " *$|^ *!" + safe + " *\\+ ?| ?(- *|!)" + safe + "|^ *!" + safe + " *$)"
		: "( ?\\+ *" + safe + " *$|^ *" + safe + " *\\+ ?| ?\\+ *" + safe + "|^ *" + safe + " *$)"
	, "i");

	// If there is an instance, take it out.
	if (ETSearch.formInput.val().match(regexp)) ETSearch.formInput.val(ETSearch.formInput.val().replace(regexp, ""));

	// Otherwise, insert the gambit with a +, -, or ! before it.
	else {
		var insert = (initialLength ? (negative ? " - " : " + ") : (negative ? "!" : "")) + gambit;
		ETSearch.formInput.focus();
		ETSearch.formInput.val(ETSearch.formInput.val() + insert);

		// If there is an instance of "?" or ":member" or ">10" in the gambit, we want to select it so the user can type over it.
		var placeholderIndex, placeholder;
		if (insert.indexOf("?") != -1) {
			placeholderIndex = insert.indexOf("?");
			placeholder = "?";
		} else if (insert.indexOf(">10") != -1) {
				placeholderIndex = insert.indexOf(">10");
				placeholder = ">10";
		} else if (insert.indexOf(":" + T("gambit.member")) != -1) {
			placeholderIndex = insert.indexOf(":" + T("gambit.member")) + 1;
			placeholder = T("gambit.member");
		}
		if (placeholderIndex) {
			ETSearch.formInput.selectRange(initialLength + placeholderIndex, initialLength + placeholderIndex + placeholder.length);
		}
	}
}

};

$(function() {
	ETSearch.init();
});