/* contentfilters.js Provides the various functions that we use on the content filters management page to enable easy filter management. Authors: Mark Smith Copyright (c) 2009 by Dreamwidth Studios, LLC. This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself. For a copy of the license, please reference 'perldoc perlartistic' or 'perldoc perlgpl'. */ /* data structures... I was going nuts not having this referencable, so I have broken down all of the globals and what's in them. cfSubs = { userid => { showbydefault => 0/1, fgcolor => '#000000', bgcolor => '#ffffff', groupmask => 2394829347, journaltype => 'P', username => 'test3', }, userid => ..., userid => ..., }; cfFilters = { filterid => { id => 1, name => 'filter', public => 1/0, sortorder => 234, // populated only when a filter is clicked on, initially null members => { userid => { user => 'username', // all of these are optional and may not be present adult_content => enum('any', 'nonexplicit', 'sfw'), poster_type => enum('any', 'maintainer', 'moderator'), tags_mode => enum('any_of', 'all_of', 'none_of'), tags => [ 1, 3, 59, 23, ... ], }, userid => ..., userid => ..., }, }, filterid => ..., filterid => ..., }; cfTags = { userid => { tagid => { name => 'tagname', uses => 13, }, tagid => ..., tagid => ..., }, userid => ..., userid => ..., }; */ var cfSelectedFilterId = null, cfCurrentUserid = null; var cfSubs = {}, cfTags = {}, cfFilters = {}; var cfTypeFilter = ''; var cfSubsSorted = []; // [ total count, selected ] var cfTagCount = [ 0, 0 ]; // current save timer var cfTimerId = null, cfSaveTicksLeft = 0; function cfShowTypes( newtype ) { if ( cfTypeFilter == newtype ) return; cfTypeFilter = newtype; cfPopulateLists(); } function cfPopulateLists(selectValue) { // whenever we repopulate the lists, we lose what is selected cfHideOptions(); // short circuit, if we have no filter, just empty both if ( ! cfSelectedFilterId ) { $('#cf-in-list, #cf-notin-list').empty(); $('#cf-rename, #cf-delete, #cf-view, #cf-edit').hide(); $('#cf-intro').show(); return; } // show our rename button $('#cf-rename, #cf-delete, #cf-view, #cf-edit').show(); $('#cf-intro').hide(); var filt = cfFilters[cfSelectedFilterId]; // creates a sorted list of userids cfSubsSorted = []; for ( i in cfSubs ) if ( cfTypeFilter == '' || cfSubs[i].journaltype == cfTypeFilter ) cfSubsSorted.push( i ); cfSubsSorted.sort( function( a, b ) { return ( cfSubs[a].username < cfSubs[b].username ) ? -1 : ( cfSubs[a].username > cfSubs[b].username ) ? 1 : 0; } ); var inOpts = '', outOpts = ''; for ( idx in cfSubsSorted ) { var i = cfSubsSorted[idx]; var isIn = false; for ( j in filt.members ) { if ( filt.members[j].user == cfSubs[i].username ) isIn = true; } if ( isIn ) inOpts += ''; else outOpts += ''; } $('#cf-in-list').html( inOpts ); $('#cf-notin-list').html( outOpts ).val( selectValue ); if ($.browser.msie && parseInt($.browser.version, 10) <= 8) $('#cf-in-list, #cf-notin-list').css('width', 'auto').css('width', '100%'); // set #cf-in-list and #cf-notin-list width (IE n..7 bug) } function cfPopulateOptions( ) { $('#cf-public').val( cfFilters[cfSelectedFilterId]['public'] ); $('#cf-sortorder').val( String( cfFilters[cfSelectedFilterId]['sortorder'] ) ); $('#cf-foname').text( cfFilters[cfSelectedFilterId]['name'] ); $('#cf-filtopts').show(); } function cfSelectedFilter() { // filter options are not implemented yet, so don't show that box :) cfPopulateLists(); cfPopulateOptions(); } function cfSelectFilter( filtid ) { // do nothing case if ( filtid < 1 ) filtid = null; if ( cfSelectedFilterId != null && cfSelectedFilterId == filtid ) return; // store this for usefulness cfSelectedFilterId = filtid; // have to hide the options now, as we're not sure what the user is doing cfHideOptions(); // if they've chosen nothing... if ( filtid == null ) return cfPopulateLists(); // if this filter already has loaded members, just return if ( cfFilters[filtid].members != null ) return cfSelectedFilter(); // get the members of this filter $.getJSON( '/__rpc_contentfilters?mode=list_members&user=' + DW.currentUser + '&filterid=' + filtid, function( data ) { cfFilters[filtid].members = data.members; cfSelectedFilter(); } ); } function cfUpdateTags( data ) { // cfTags[cfCurrentUser] = data.tags var member = cfFilters[cfSelectedFilterId].members[cfCurrentUserid]; if ( ! member ) return; // initialize our tag structure if ( ! member.tags ) member.tags = {}; // reset the global tag counts cfTagCount = [ 0, 0 ]; var html = '', htmlin = ''; //sort tags alphabetically function sorttags( tag_a, tag_b ) { var name_a = data.tags[tag_a].name; var name_b = data.tags[tag_b].name; if ( name_a < name_b ) { return -1; } else if ( name_a > name_b ) { return 1; } else { return 0; } } var sorted_tags = []; for ( id in data.tags ) { sorted_tags.push( id ); } sorted_tags = sorted_tags.sort( sorttags ); // go through tag list alphabetically for(var i=0; i' + data.tags[id].name + '[' + data.tags[id].uses + '] '; } else { html += '' + data.tags[id].name + '[' + data.tags[id].uses + '] '; } } // do some default stuff if nothing is selected $('#cf-notagsavail').toggle( html == '' ); $('#cf-notagssel').toggle( htmlin == '' ); // and now show/hide the other boxes $('#cf-t-box2').toggle( html != '' ); $('#cf-t-box3').toggle( htmlin != '' ); // now pass this into the page for the user to view $('#cf-t-box2').html( html ); $('#cf-t-box3').html( htmlin ); // now, all of these tags need an onclick handler $('span.cf-tag').bind( 'click', function( e ) { cfClickTag( $(this).attr( 'id' ) ); } ); } function cfClickTag( id ) { var obj = $('span.cf-tag#' + id); var filt = cfFilters[cfSelectedFilterId]; var member = filt.members[cfCurrentUserid]; if ( !obj || !filt || !member || !DW.userIsPaid ) return; // first, let's toggle the class that boldens the tag obj.toggleClass( 'cf-tag-on' ); // now make sure we have a tags array... if ( ! member.tags ) member.tags = {}; // now, if the class is ON, we need to move this tag to the bucket if ( obj.hasClass( 'cf-tag-on') ) { $('#cf-t-box3').append(' '); // thanks Janine! obj.appendTo('#cf-t-box3'); member.tags[id] = true; cfTagCount[1]++; // and if it's off, remove it } else { $('#cf-t-box2').append(' '); // damn splits... obj.appendTo('#cf-t-box2'); delete member.tags[id]; cfTagCount[1]--; } // now show/hide our UI markers $('#cf-notagssel').toggle( cfTagCount[1] == 0 ); $('#cf-notagsavail').toggle( cfTagCount[0] == 0 || ( cfTagCount[0] - cfTagCount[1] == 0 ) ); // and now... tag contents $('#cf-t-box3').toggle( cfTagCount[1] > 0 ); $('#cf-t-box2').toggle( cfTagCount[0] > 0 && ( cfTagCount[0] - cfTagCount[1] > 0 ) ); // kick off a save cfSaveChanges(); } function cfSaveChanges() { // if we have a save timer, nuke it if ( cfTimerId ) clearTimeout( cfTimerId ); // set a timer... then try to save, which actually sets a new timer // for us to use. cfSaveTicksLeft = 6; cfTrySave(); } function cfTrySave() { // our timer has fired cfTimerId = null; // now, if we're out of ticks just save if ( --cfSaveTicksLeft <= 0 ) return cfDoSave(); // okay, wait another second cfTimerId = setTimeout( cfTrySave, 1000 ); // now update the text $('#cf-unsaved').html( 'Saving in ' + cfSaveTicksLeft + ' seconds...' ); $('#cf-unsaved, #cf-hourglass').show(); } function cfDoSave() { // this actually posts the save $.post( '/__rpc_contentfilters?mode=save_filters&user=' + DW.currentUser, { 'json': JSON.stringify( cfFilters ) }, function( data ) { // FIXME: error handling... if ( !data.ok ) return; // we're saved cfTimerId = null; $('#cf-unsaved, #cf-hourglass').hide(); }, 'json' ); } function cfSelectMember( sel ) { // if we have selected more than one (or less than one) thing, then hide the // options box and call it good if ( sel.length < 1 || sel.length > 1 ) { cfCurrentUserid = null; return cfHideOptions(); } // some variables we're going to use later, in particular cfCurrentUserid is // used in many places so we know who we're editing var userid = sel[0]; user = cfSubs[userid]; cfCurrentUserid = userid; // these have to be true, or we have serious issues var filt = cfFilters[cfSelectedFilterId]; var member = filt.members[cfCurrentUserid]; if ( !filt || !member ) return; // FIXME: don't always reget the tags $.getJSON( '/__rpc_general?mode=list_tags&user=' + user.username, cfUpdateTags ); // clear out both of the tag lists $('#cf-t-box2, #cf-t-box3').empty(); // if this is a comm show the extra community options $('#cf-pt-box').toggle( user.journaltype == 'C' ); // default the member to a few options... if ( ! member.adultcontent ) member.adultcontent = 'any'; if ( ! member.postertype ) member.postertype = 'any'; if ( ! member.tagmode ) member.tagmode = 'any_of'; // now fill the options in $('#cf-adultcontent').val( member.adultcontent ); $('#cf-postertype').val( member.postertype ); $('#cf-tagmode').val( member.tagmode ); // and now show the actual options box cfShowOptions(); } function cfHideOptions() { $('#cf-options').hide(); } function cfShowOptions() { // if the user is not paid, make sure we disable things, etc if ( ! DW.userIsPaid ) { $('#cf-adultcontent, #cf-postertype, #cf-tagmode').attr( 'disabled', 'disabled' ); $('#cf-free-warning').show(); } $('#cf-options').show(); } function cfAddMembers() { var members = $('#cf-notin-list').val(); var filt = cfFilters[cfSelectedFilterId]; if ( !filt || members.length <= 0 ) return; // simply create a new row in the filter members list for this person for ( i in members ) { var userid = members[i]; filt.members[userid] = { 'user': cfSubs[userid].username }; } // kick off a save event cfSaveChanges(); var $opt = $("#cf-notin-list option"); var lastsel = $opt.filter(":selected:last").index(); var at_end = $opt.length - 1 - lastsel == 0; var $newsel = $opt.filter((at_end?":lt(":":gt(") + lastsel+")").filter(":not(:selected)" + (at_end?":last":":first")) // and then redisplay our lists cfPopulateLists( $newsel.val() ); } function cfRemoveMembers() { var members = $('#cf-in-list').val(); var filt = cfFilters[cfSelectedFilterId]; if ( ! filt || members.length <= 0 ) return; // simply delete the row in the filter members list for this person for ( i in members ) { var userid = members[i]; delete filt.members[userid]; } // kick off a save event cfSaveChanges(); // and then redisplay our lists cfPopulateLists(); } function cfChangedTagMode() { var tagmode = $('#cf-tagmode').val(); var filt = cfFilters[cfSelectedFilterId]; var member = filt.members[cfCurrentUserid]; if ( !tagmode || !filt || !member ) return; member.tagmode = tagmode; cfSaveChanges(); } function cfChangedAdultContent() { var adultcontent = $('#cf-adultcontent').val(); var filt = cfFilters[cfSelectedFilterId]; var member = filt.members[cfCurrentUserid]; if ( !adultcontent || !filt || !member ) return; member.adultcontent = adultcontent; cfSaveChanges(); } function cfChangedPosterType() { var postertype = $('#cf-postertype').val(); var filt = cfFilters[cfSelectedFilterId]; var member = filt.members[cfCurrentUserid]; if ( !postertype || !filt || !member ) return; member.postertype = postertype; cfSaveChanges(); } function cfNewFilter() { // prompt the user for a filter name... var name = prompt( 'New filter name:', '' ); if ( ! name ) return; // now that we have a name, kick off a request to make a new filter $.getJSON( '/__rpc_contentfilters?mode=create_filter&user=' + DW.currentUser + '&name=' + name, function( data ) { // no id means some sort of failure // FIXME: error handling so the user knows what's up if ( !data.id || !data.name ) return; // save a roundtrip, we don't have to hit the server since it just gave us // the filter information cfFilters[data.id] = { 'id': data.id, 'name': data.name, 'public': data["public"], 'sortorder': data.sortorder }; // we have to do this first, before the update of the filter select, due to the // way the code is structured cfSelectFilter( data.id ); // happens last cfUpdateFilterSelect(); } ); } function cfRenameFilter() { var filt = cfFilters[cfSelectedFilterId]; if ( !filt ) return; // FIXME: don't think dialogs are accessible at all var renamed = prompt( 'Rename filter to:', filt.name ); if ( renamed != null ) filt.name = renamed; // and now update the select dialog cfUpdateFilterSelect(); // and any labeling $('#cf-foname').text( cfFilters[cfSelectedFilterId]['name'] ); // kick off a saaaaaaaave! cfSaveChanges(); } function cfSortOrder() { cfFilters[cfSelectedFilterId]['sortorder'] = parseInt( $('#cf-sortorder').val(), 10 ); cfSaveChanges(); cfUpdateFilterSelect(); } function cfPublic( sel ) { cfFilters[cfSelectedFilterId]['public'] = sel === "1" ? 1 : 0; cfSaveChanges(); } function cfViewFilter() { var filt = cfFilters[cfSelectedFilterId]; $.getJSON( '/__rpc_contentfilters?mode=view_filter&user=' + DW.currentUser + '&name=' + filt.name, function( data ) { if ( !data.url ) return; window.open(data.url); } ); } // function used for sorting filters -- compares based on sort order, then name function compareFilters( a, b ) { if ( a.sortorder == b.sortorder ) { return a.name > b.name ? 1 : a.name < b.name ? -1 : 0; } return a.sortorder > b.sortorder ? 1 : -1; } function cfUpdateFilterSelect() { // regenerate HTML for the Filter: dropdown var options = ''; // sort by sortorder, then name var sortedFilters = []; for ( i in cfFilters ) { sortedFilters.push(cfFilters[i]); } sortedFilters.sort(compareFilters); for ( var i = 0; i < sortedFilters.length; i++ ) { var id = sortedFilters[i]['id']; cfFilters[id]['members'] = null; options += ''; } $('#cf-filters').html( options ); // and if we have a current filter id, reselect if ( cfSelectedFilterId ) $('#cf-filters').val( cfSelectedFilterId ); } function cfRefreshFilterList() { // in a function because we call from multiple places $.getJSON( '/__rpc_contentfilters?mode=list_filters&user=' + DW.currentUser, function( data ) { cfFilters = data.filters; cfUpdateFilterSelect(); } ); } function cfDeleteFilter() { var filt = cfFilters[cfSelectedFilterId]; if ( !filt ) return; // confirm! if ( ! confirm( 'Really delete this content filter? There is no turning back if you say yes.' ) ) return; $.getJSON( '/__rpc_contentfilters?mode=delete_filter&user=' + DW.currentUser + '&id=' + filt.id, function( data ) { // FIXME: error handling ... if ( !data.ok ) return; // the filter is gone, so nuke from some of our stuff delete cfFilters[filt.id]; // and update the UI, again, the order of these two calls matters cfSelectFilter( null ); cfUpdateFilterSelect(); } ); } jQuery( function($) { // load the current filters into the box cfRefreshFilterList(); // and get who this person is subscribed to, we're going to need this later $.getJSON( '/__rpc_general?mode=list_subscriptions&user=' + DW.currentUser, function( data ) { cfSubs = {}; for ( i in data.subs ) { cfSubs[i] = data.subs[i]; } cfPopulateLists(); } ); // setup our click handlers $('#cf-filters').bind( 'change', function(e) { cfSelectFilter( $(e.target).val() ); } ); $('#cf-in-list').bind( 'change', function(e) { cfSelectMember( $(e.target).val() ); } ); $('#cf-add-btn').bind( 'click', function(e) { cfAddMembers(); } ); $('#cf-del-btn').bind( 'click', function(e) { cfRemoveMembers(); } ); $('#cf-new').bind( 'click', function(e) { cfNewFilter(); } ); $('#cf-rename').bind( 'click', function(e) { cfRenameFilter(); } ); $('#cf-view').bind( 'click', function(e) { cfViewFilter(); } ); $('#cf-delete').bind( 'click', function(e) { cfDeleteFilter(); } ); $('#cf-showtypes').bind( 'change', function(e) { cfShowTypes( $(e.target).val() ); } ); $('#cf-public').bind( 'change', function(e) { cfPublic( $(e.target).val() ); } ); // not working on the input element? put the function in directly as onChange //$('#cf-sortorder').bind( 'change', function(e) { cfSortOrder( $(e.target).val() ); } ); // if the user is paid, we bind these. note that even if someone goes through the // trouble of hacking up the form and submitting data, the server won't actually give // you an advanced filter. so don't waste your time! if ( DW.userIsPaid ) { $('#cf-adultcontent').bind( 'change', function(e) { cfChangedAdultContent(); } ); $('#cf-postertype').bind( 'change', function(e) { cfChangedPosterType(); } ); $('#cf-tagmode').bind( 'change', function(e) { cfChangedTagMode(); } ); } } );