From 15e407b9f4455f9bcc4ad5b091735ce4342db0db Mon Sep 17 00:00:00 2001 From: Scott Collins Date: Thu, 4 Sep 2008 11:07:41 -0400 Subject: [PATCH] Move over new files/unit-tests from my work branch --- plugins/Ajax/htdocs/images/firehose.tagui.js | 370 ++++++ plugins/Ajax/htdocs/images/responder.tagui.js | 39 - plugins/Ajax/htdocs/images/slash.tagui.js | 1162 +++++++++++++++++ plugins/Ajax/htdocs/images/slash.util.js | 483 +++++++ .../images/t/{api.html => slash.util.html} | 221 +++- .../htdocs/images/t/tagui_broadcaster.html | 89 ++ .../Ajax/htdocs/images/t/tagui_display.html | 89 ++ ...ponder.tagui.html => tagui_responder.html} | 30 +- 8 files changed, 2402 insertions(+), 81 deletions(-) create mode 100644 plugins/Ajax/htdocs/images/firehose.tagui.js delete mode 100644 plugins/Ajax/htdocs/images/responder.tagui.js create mode 100644 plugins/Ajax/htdocs/images/slash.tagui.js create mode 100644 plugins/Ajax/htdocs/images/slash.util.js rename plugins/Ajax/htdocs/images/t/{api.html => slash.util.html} (50%) create mode 100644 plugins/Ajax/htdocs/images/t/tagui_broadcaster.html create mode 100644 plugins/Ajax/htdocs/images/t/tagui_display.html rename plugins/Ajax/htdocs/images/t/{responder.tagui.html => tagui_responder.html} (58%) diff --git a/plugins/Ajax/htdocs/images/firehose.tagui.js b/plugins/Ajax/htdocs/images/firehose.tagui.js new file mode 100644 index 000000000..5dbe992b6 --- /dev/null +++ b/plugins/Ajax/htdocs/images/firehose.tagui.js @@ -0,0 +1,370 @@ +(function($){ +/*jslint evil:true */ +eval(Slash.Util.Package.with_packages('Slash.Util')); +/*jslint evil:false */ + +// public API +Package({ named: 'Slash.Firehose.TagUI', + api: { + click_handler: firehose_click_tag, + init_entries: firehose_init_tagui, + toggle: firehose_toggle_tagui, + toggle_to: firehose_toggle_tagui_to, + form_submit: form_submit_tags, + before_update: before_update, + after_update: after_update + } +}); + +var Firehose = Slash.Firehose; + + +function before_update(){ + return { + selection: new $.TextSelection(gFocusedText), + $menu: $('.ac_results:visible') + }; +} + +function after_update( $new_entries, state ){ + firehose_init_tagui($new_entries); + state.selection.restore().focus(); + state.$menu.show(); +} + + + +// Slash.Firehose.TagUI private implementation details + +function firehose_toggle_tagui_to( if_expanded, selector ){ + var $entry = $(selector).nearest_parent('[tag-server]'), + $widget = $entry.find('.tag-widget.body-widget'), + id = $entry.attr('tag-server'); + + Firehose.set_action(); + $entry.find('.tag-widget').each(function(){ this.set_context(); }); + + $widget.toggleClassTo('expanded', if_expanded); + + var toggle_button={}, toggle_div={}; + if ( if_expanded ){ + $entry.each(function(){ this.tag_server.fetch_tags(); }); + if ( fh_is_admin ) { + firehose_get_admin_extras(id); + } + $widget.find('.tag-entry:visible:first').each(function(){ this.focus(); }); + + toggle_button['+'] = (toggle_button.collapse = 'expand'); + toggle_div['+'] = (toggle_div.tagshide = 'tagbody'); + } else { + toggle_button['+'] = (toggle_button.expand = 'collapse'); + toggle_div['+'] = (toggle_div.tagbody = 'tagshide'); + } + + $widget.find('a.edit-toggle .button').mapClass(toggle_button); + $entry.find('#toggletags-body-'+id).mapClass(toggle_div); +} + +function firehose_toggle_tagui( toggle ) { + firehose_toggle_tagui_to( ! $(toggle.parentNode).hasClass('expanded'), toggle ); +} + +var $related_trigger = $().filter(); + +function form_submit_tags( form, options ){ + var $input = $('.tag-entry:input', form); + $related_trigger = $input; + $(form).nearest_parent('[tag-server]'). + each(function(){ + var tag_cmds = $input.val(); + $input.val(''); + this.tagui_server.submit_tags(tag_cmds, options); + }); +} + +function firehose_click_tag( event ) { + var $target = $(event.target); + var command=''; + + $related_trigger = $target; + + if ( $target.is('a.up') ) { + command = 'nod'; + } else if ( $target.is('a.down') ) { + command = 'nix'; + } else if ( $target.is('.tag') ) { + command = $target.text(); + } else if ( $target.nearest_parent('.tmenu').length ) { + var op = $target.text(); + var $tag = $target.nearest_parent(':has(span.tag)').find('.tag'); + $related_trigger = $tag; + + var tag = $tag.text(); + command = Slash.TagUI.Command.normalize_tag_menu_command(tag, op); + } else { + $related_target = $().filter(); + } + + if ( command ) { + Firehose.set_action(); + var $s_elem = $target.nearest_parent('[tag-server]'); + + if ( event.shiftKey ) { + // if the shift key is down, append the tag to the edit field + $s_elem.find('.tag-entry:text:visible:first').each(function(){ + if ( this.value ) { + var last_char = this.value[ this.value.length-1 ]; + if ( '-^#!)_ '.indexOf(last_char) == -1 ) { + this.value += ' '; + } + } + this.value += command; + this.focus(); + }); + } else { + // otherwise, send it the server to be processed + $s_elem.each(function(){ + this.tagui_server.submit_tags(command, { fade_remove: 400, order: 'prepend', classes: 'not-saved'}); + }); + } + return false; + } + + return true; +} + +var context_triggers = qw.as_set('submission journal bookmark feed story vendor misc comment discussion project'); + + +function firehose_handle_context_triggers( commands ){ + var context; + commands = $.map(commands, function(cmd){ + if ( cmd in context_triggers ) { + context = cmd; + cmd = null; + } + return cmd; + }); + + $('.tag-widget:not(.nod-nix-reasons)', this). + each(function(){ + this.set_context(context); + }); + + return commands; +} + + +function firehose_handle_nodnix( commands ){ + if ( commands.length ) { + var $reasons = $('.nod-nix-reasons', this); + + var context_not_set = true; + var nodnix_context = function( ctx ){ + $reasons.each(function(){ + this.tagui_widget.set_context(ctx); + }); + context_not_set = false; + }; + + var tagui_server=this, context_not_set=true; + $.each(commands.slice(0).reverse(), function(i, cmd){ + if ( cmd=='nod' || cmd=='nix' ) { + nodnix_context(cmd); + return false; + } + }); + + if ( context_not_set ) { + nodnix_context(undefined); + } + } + + return commands; +} + +function firehose_handle_comment_nodnix( commands ){ + if ( commands.length ) { + var voted=false; + commands = $.map(commands, function( cmd ){ + var match = /^([\-!]*)(nod|nix)$/.exec(cmd); + if ( match ) { + var modifier = match[1], vote = match[2]; + cmd = modifier + 'meta' + vote; + if ( ! modifier ) { + voted = true; + } + } + return cmd; + }); + + var $entry = $(this); + if ( voted ) { + Firehose.collapse_entries($entry); + } + $entry.find('.nod-nix-reasons').each(function(){ + this.set_context(undefined); + }); + } + + return commands; +} + +function firehose_tag_feedback( signal, data ){ + var tr = this.tagui_responder; + var tags; + + function if_have( k ){ return k in tags || 'meta'+k in tags; } + function if_busy( depth ){ return depth>0; } + + var $entry = tr._$entry || (tr._$entry = $(this).nearest_parent('[tag-server]')); + + var depth, was_busy=if_busy(depth = tr._busy_depth || 0); + switch ( signal ) { + case 'user': // fix the nod/nix capsule, data => user tags + tags = qw.as_set(data); + var nod = if_have('nod'), nix = if_have('nix'); + (tr._$updown || (tr._$updown = $entry.find('#updown-' + $entry.attr('tag-server')))). + setClass(nod==nix && 'vote' || nod && 'votedup' || 'voteddown'); + break; + case 'ajaxSuccess': // new tags are all in place, refresh "computed styles" + Slash.TagUI.Markup.refresh_styles($entry); + break; + case 'ajaxStart': // start the spinner + ++depth; + break; + case 'ajaxComplete': // stop the spinner + --depth; + break; + } + if ( was_busy != if_busy(tr._busy_depth = depth) ) { + var $spinner = $(this); + if ( was_busy ) { + $spinner.removeAttr('style'); + } else { + $spinner.show(); + } + } +} + +function firehose_click_nodnix_reason( event ) { + Firehose.set_action(); + var $entry = $(event.target).nearest_parent('[tag-server]'); + var id = $entry.attr('tag-server'); + + if ( (fh_is_admin || firehose_settings.metamod) && ($('#updown-'+id).hasClass('voteddown') || $entry.is('[type=comment]')) ) { + Firehose.collapse_entries($entry); + } + + return true; +} + + +function firehose_init_tagui( $new_entries ){ + if ( ! $new_entries || ! $new_entries.length ) { + var $firehoselist = $('#firehoselist'); + if ( $firehoselist.length ) { + $new_entries = $firehoselist.children('[id^=firehose-][class*=article]'); + } else { + $new_entries = $('[id^=firehose-][class*=article]'); + } + } + $new_entries = $new_entries.filter(':not([tag-server])'); + + var pipeline = [ firehose_handle_context_triggers ]; + if ( fh_is_admin ) { + pipeline.unshift(firehose_handle_admin_commands); + } + + $new_entries. + tagui_server(firehose_id_of, pipeline, { request_data: { reskey: reskey_static } }). + each(function(){ + this.tagui_server.command_pipeline.push( + ($(this).attr('type') == 'comment') ? + firehose_handle_comment_nodnix : + firehose_handle_nodnix ); + }). + find('.title'). + append('
' + + '
' + + '
'). + find('.tag-display-stub'). + click(firehose_click_nodnix_reason); + + Slash.Firehose.TagUI.init($new_entries); + + if ( fh_is_admin ) { + $new_entries. + find('.body-widget'). + each(function(){ + this.tagui_widget.modify_context = firehose_admin_context; + }); + } + + $new_entries. + find('.tag-server-busy'). + tagui_responder(firehose_tag_feedback, 'user ajaxStart ajaxSuccess ajaxComplete'); + + $new_entries. + find('.tag-entry'). + focus(function(event){ + gFocusedText = this; + }). + blur(function(event){ + if ( gFocusedText === this ) { + gFocusedText = null; + } + }). + keypress(function(event){ + var ESC=27, SPACE=32; + + var $this = $(this); + switch ( event.which || event.keyCode ) { + case ESC: + $this.val(''); + return false; + case SPACE: + var $form = $this.parent(); + setTimeout(function(){ + $form.trigger("onsubmit"); + }, 0); + return true; + default: + return true; + } + }). + autocomplete('/ajax.pl', { + loadingClass: 'working', + minChars: 3, + autoFill: true, + max: 25, + extraParams: { + op: 'tags_list_tagnames' + } + }). + result(function(){ + $(this).parent().trigger("onsubmit"); + }); + }); +} + + +$(function(){ + var add_style_triggers = Slash.TagUI.Markup.add_style_triggers; + + + add_style_triggers(YAHOO.slashdot.sectionTags, 's1'); + add_style_triggers(YAHOO.slashdot.topicTags, 't2'); + add_style_triggers(['nod', 'metanod'], 'y p'); + add_style_triggers(['nix', 'metanix'], 'x p', ); + add_style_triggers(qw('submission journal bookmark feed story vendor misc comment discussion project'), 'd'); + + if ( fh_is_admin ) { + add_style_triggers(['signed', 'unsigned', 'signoff'], 'w p'); + Slash.TagUI.Display.defaults.menu = 'x ! # ## _ )'; + } +}); + + + +})(jQuery); diff --git a/plugins/Ajax/htdocs/images/responder.tagui.js b/plugins/Ajax/htdocs/images/responder.tagui.js deleted file mode 100644 index 54e3258ac..000000000 --- a/plugins/Ajax/htdocs/images/responder.tagui.js +++ /dev/null @@ -1,39 +0,0 @@ -(function($){ - -if ( window.TagUI === undefined ) { - window.TagUI = {}; -} - -window.TagUI.tag_responder = new API({ - name: 'tag_responder', - element_api: { - ready: function( r_elem, if_ready ){ - var $r_elem = $(r_elem), ready_class = 'ready'; - if ( if_ready === undefined ) { - return $(r_elem).hasClass(ready_class); - } - $(r_elem).toggleClassTo(ready_class, if_ready); - return r_elem; - }, - bind: function( r_elem, fn, signals ){ - r_elem.tag_responder.handle_signal = fn; - $(r_elem).attr('signal', list_as_string(signals)); - return r_elem; - }, - handle: function( r_elem, signals, data, options ){ - var fn = r_elem.tag_responder && r_elem.tag_responder.handle_signal; - if ( fn ) { - fn.apply(r_elem, [signals, data, options]); - } - return r_elem; - } - }, - element_constructor: function( r_elem, fn, signals, if_ready ){ - r_elem. - tag_responder.bind(fn, signals). - tag_responder.ready(if_ready===undefined?true:if_ready); - }, - extend_jquery_wrapper: true -}); - -})(jQuery); diff --git a/plugins/Ajax/htdocs/images/slash.tagui.js b/plugins/Ajax/htdocs/images/slash.tagui.js new file mode 100644 index 000000000..1a2db53f0 --- /dev/null +++ b/plugins/Ajax/htdocs/images/slash.tagui.js @@ -0,0 +1,1162 @@ +(function($){ +/*jslint evil:true */ +// bring names from Slash.Util, etc., into scope, e.g., Package +eval(Slash.Util.Package.with_packages('Slash.Util', 'Slash.Util.Algorithm')); +/*jslint evil:false */ + +/* Note: If you're reading this in BBEdit or any other browser that supports "folds", you may want + to start by collapsing all folds so you just see the eight top-level components. + + Requires: jquery, slash.util.js; non-Slashdot installations will want tagui.core.css + + + + Description: + + This file implements the bulk of the _generic_ tag ui. For Slashdot, specifics are to be found + in firehose.tagui.js. For anyone else, this file (and its prerequisites) should be enough to + have a working tag ui (modulo, as of this writing, actually getting tags from a server). + + + + Design: + + The "element-based" components, Responder, Broadcaster, Server, Display, and Widget, are meant to + be installed onto existing DOM elements. Their individual functions can be called through the + package, e.g., + + Slash.TagUI.Broadcaster.broadcast(from_elem, signal, data, options) + + or, once installed, through the element itself, e.g., + + from_elem.tagui_broadcaster.broadcast(signal, data, options) + + The package itself is the element constructor. So installing onto from_elem usually looks like + this: + + Slash.TagUI.Broadcaster(from_elem, options) + + The element-based components install their API into jQuery selections as well, so the previous + examples could also be rendered as: + + var $broadcasters = $('.i-want-to-broadcast').tagui_broadcaster(construction_options); + $broadcasters.tagui_broadcaster__broadcast(signal, data, broadcast_options); + + The element itself is extended only by a single member (per interface), e.g., a display element + will have: + + elem.tagui_display + elem.tagui_responder // because displays are also responders + + Functions of the Display component hang from the tagui_display "stem", as well as any element + specific data needed by Display. Unfortunately, we can't play the same proxy games with jQuery, + so the stem name is rolled into the function names: + + $(elem).tagui_display__set_tags(tags, options); // vs. + elem.tagui_display.set_tags(tags, options); + + The jQuery versions of the functions apply to every selected element that has the interface + installed, or to every element in the case of the constructors or a "free API" call, e.g., + + $(expr).tagui__tags() + + ...is part of the free (non-element-bound) API, and so applies to every element in $(expr). + + In general, when a package declares an element_api, any function of that API will be available in + several forms, e.g., for the 'bind' function of a responder: + + // via the package + Slash.TagUI.Responder.bind(r_elem, fn, signals); // TagUI.Responder.bind + + // via an (already "constructed") element + r_elem.tagui_responder.bind(fn, signals); // tagui_responder.bind + + // ...and for packages that, like Responder, enable jQuery + // via the jQuery global + $.tagui_responder.bind(r_elem, fn, signals); // tagui_responder.bind + + // via a jQuery selection, for every eligible element it contains + $(expr).tagui_responder__bind(fn, signals); // tagui_responder__bind + + // constructor forms + Slash.TagUI.Responder(r_elem, options); // TagUI.Responder + $.tagui_responder(r_elem, options); // tagui_responder + $(expr).tagui_responder(options); // tagui_responder + + "Free" API calls are available: + + // via the package + Slash.TagUI.Markup.add_style_triggers(tags, styles); // TagUI.Markup.add_style_triggers + + // ...and for packages that, like Markup, enable jQuery + // via the jQuery global + $.tagui_markup.add_style_triggers(tags, styles); // tagui_markup.add_style_triggers + + + + Now, onto the bigger picture. + + ............................................................Servers, Broadcasters, and Responders + + In a view listing a number of entries, e.g., a Firehose view of articles, where tags within an + entry are connected to that entry --- here's how the tag ui is implemented: the entry itself + becomes a tagui_server. When it fetches or submits tags, it notifies components beneath it (DOM + descendants) via the tagui_broadcaster methods. Those descendants will include displays, which + will update their contained tags at this notification, and other custom responders, e.g., + something to start and stop a "busy spinner", or, in the case of the Firehose, to update the vote + displayed by the nod/nix "capsule". + + Any code can submit new tags or tag-commands by finding the tagui_server and calling its methods + directly, e.g., + + $(this).nearest_parent('[tag-server]').tagui_server__submit_tags('slownewsday'); + + Typically, you will do this from a custom click handler (see Slash.Firehose.TagUI.click_handler). + You might also do it from a form or text input. + + ..............................................................Command, and the "command pipeline" + + Before actually submitting any commands across AJAX, the tagui_server first sends the commands + through a pipeline of filters, which can add, remove, or alter those commands. That pipeline is + empty by default. You fill it with whatever you want. Some filters are provided (see Command). + The Firehose uses this mechanism, for example, to notice commands masquerading as tags, e.g, + 'neverdisplay', act on them locally, and delete them from the stream of commands to be sent. + + ...................................................................................CSS and Markup + + Tags are marked-up (or more specifically, the li elements _containing_ tags are marked-up) in two + ways. First with "static styles", that is, css classes that can be determined soley by looking + at the tag itself, e.g., 'apple' is a section name, so it gets the 's1' class. Second, with + "computed styles", that is, css classes determined by examining all the tags under an entry and + in which displays they appear, e.g., if the tag 'democrat' appears in both the top tags display + and the user tags display, then _every_ occurance of that tag anywhere within the entry will have + the css classes 't u' (for top and user, respectively). + + Static styles are set up by calling Markup.add_style_triggers, and are applied automatically when + tags are created or changed. The default set of static styles is empty. The Firehose installs a + set of static styles (including 's1' for sections); you can do the same, or not, as needed. The + computed styles are computed and applied by the Markup call refresh_styles. This function is not + called automatically, so if you don't want computed styles, you don't have to have them. The + Firehose calls this via a Responder that listens for the ajaxSuccess signal. It's the computed + styles that let us do things in CSS such as: hide top tags if they already appear in the system + tags display; hide user tags if they already appear in the system or top tags displays. + + .........................................................................................Displays + + Displays additionally support per-tag menus and supply some defaults. You can disable the menus + entirely; supply your own; and easily control them on a per-display basis or (less easily) on a + tag-by-tag basis. The "meaning" of the menus is up to your code. The Firehose default (via its + click handler) is to submit a command based on the menu label and the underlying tag, e.g., the + 'x' menu item on the tag 'apple' asks the corresponding tagui_server to submit_tags('-apple'). + This simple rule means the Firehose only needs a single click handler for the entire page ... at + least for submitting tag-commands. The appearance and behavior of the menus is entirely defined + by CSS. + + In the near future, Displays will also support "drag and drop" of tags for ordering within a + display or for moving tags between displays (and possibly new components). As with menus, the + "meaning" of a drag and drop action would be up to the client code. + + ..........................................................................................Widgets + + Finally, a Widget is a container that manages a "context" --- hiding, showing, positioning, and + animating a designated display filled with context-specific tags. Your custom machinery + determines when and how contexts are set. One way of doing that is by installing a handler in + the command pipeline, and setting a context based on what tags were just submitted. Your click + handler is another likely place. Essentially, a Widget is a control for a context sensitive menu + in the form of a tag display. For example, the Firehose uses this mechanism to bring up the + secondary choices after a nod or a nix, that is (for nod) 'fresh funny insightful interesting + maybe'. Widgets support "timing out" the context sensitive display. + + .......................................................................applications of the tag ui + + The simplest scenario for the tagui is that you've set up the static styles to your liking, and + then produce a single read-only display populated with tags at template time, and calling the + tagui__init function, from jQuery, on the new entries as they are added to the window. The next + step up is adding a server to each entry, allowing the display to be filled dynamically, upon + request. Then adding a click handler to act on the tags and/or a text field to add new tags. + Then widgets to supply additional context-sensitive commands; and command handlers to separate + out and act on those that require local handling. + + Some Final Notes: + + The tagui components that are actually attached to elements automatically benefit from the loose + coupling afforded by the DOM. A single server serves only those elements beneath it. Any + element can just "look up" (with nearest_parent('[tag-server]')) to find its server. Responders + (for instance, displays) can be added or removed at any time. All such components support + specifying most of their behavior right in the HTML. Slash.Util.Package ensures that actual + attachments have a minimal footprint, both in the namespace and code size---adapting a single + element function to be called stand-alone, with the element as its first argument; as a method + of an element; or as method of a jQuery selection, to be applied to every eligible element. + */ + + +var Responder, Broadcaster, Server, Markup, Display, Command, Fx, Widget; + +// Slash.TagUI.Util +(function(){ var TagUI = + +// public API +new Package({ named: 'Slash.TagUI', + api: { + tags: function( selector ){ + return $(selector).tagui__tags(); + }, + cached_user_tags: function( selector ){ + return $(selector).tagui__cached_user_tags(); + }, + bare_tag: function( tag ){ + try { + // XXX what are the real requirements for a tag? + return /[a-z][a-z0-9]*/.exec(tag.toLowerCase())[0]; + } catch (e) { + // I can't do anything with it; I guess you must know what you're doing + return tag; + } + }, + init: function( $new_entries, options ){ + options = options || {}; + $new_entries.find('.tag-display-stub').tagui_display(options.for_display); + $new_entries.find('.tag-widget-stub').tagui_widget(options.for_widget); + Markup.refresh_styles($new_entries); + } + }, + jquery: { + element_api: { + tags: function(){ + var tags = {}; + this.find('span.tag').each(function(){ + tags[ $(this).text() ] = true; + }); + return qw(tags); + }, + cached_user_tags: function(){ + return this.find('.tag-display.ready[signal=user]').tagui__tags(); + } + } + }, + exports: 'tags cached_user_tags bare_tag' +}); + +})(); +/*jslint evil:true */ +eval(Package.with_packages('Slash.TagUI')); +/*jslint evil:false */ + +// Slash.TagUI.Responder: "observer" +(function(){ Responder = + +// public API +new Package({ named: 'Slash.TagUI.Responder', + element_api: { + ready: function( r_elem, if_ready ){ + var $r_elem = $(r_elem), ready_class = 'ready'; + if ( if_ready === undefined ) { + return $(r_elem).hasClass(ready_class); + } + $(r_elem).toggleClassTo(ready_class, if_ready); + return r_elem; + }, + bind: function( r_elem, fn, signals ){ + r_elem.tagui_responder.handle_signal = fn; + $(r_elem).attr('signal', qw.as_string(signals)); + return r_elem; + }, + handle: function( r_elem, signals, data, options ){ + var fn = r_elem.tagui_responder && r_elem.tagui_responder.handle_signal; + if ( fn ) { + fn.apply(r_elem, [signals, data, options]); + } + return r_elem; + } + }, + stem_function: function( r_elem, o ){ + r_elem. + tagui_responder.bind(o.fn, o.signals). + tagui_responder.ready(!if_defined_false(o.if_ready)); + return o.defaults ? { defaults: o.defaults } : undefined; + }, + jquery: true +}); + +})(); + +// Slash.TagUI.Broadcaster: "observable" +(function(){ Broadcaster = + +// public API +new Package({ named: 'Slash.TagUI.Broadcaster', + element_api: { + broadcast: function( b_elem, signal, data, options ){ + var M = /^\?$/.exec(signal); + if ( M ) { + signal = M[1]; + var selector = '.ready[signal*='+M[2]+']'; + var $r_list = arguments[4]; // list of responders + $r_list = $r_list && $r_list.filter(selector) || $(selector, b_elem); + $r_list.tagui_responder__handle(signal, data, options); + } + return b_elem; + }, + broadcast_sequence: function( b_elem, sequence, options ){ + // slice to remove bogus empty before first "separator" + var tuples = sequence.split(/\n?<([\w:]*)>/).slice(1); + + if ( tuples && tuples.length >= 2 ) { + // XXX consider caching + var $responders = $('.ready[signal]', b_elem); + + while ( tuples.length >= 2 ) { + var data = tuples.pop(); + Broadcaster.broadcast(b_elem, tuples.pop(), data, options, $responders); + } + } + return b_elem; + } + }, + jquery: true +}); + +})(); + +// Slash.TagUI.Server: ajax service and Broadcaster reporting results and events +(function(){ Server = + +// public API +new Package({ named: 'Slash.TagUI.Server', + api: { + defaults: { + command_feedback: { + order: 'append', + classes: 'not-saved' + }, + success_feedback: { + order: 'append' + }, + request_data: { + op: 'tags_setget_combined' + }, + ajax: { + url: '/ajax.pl', + type: 'POST', + dataType: 'text' + } + } + }, + element_api: { + ajax: function( s_elem, options ){ + return ajax(s_elem, null, options); + }, + fetch_tags: function( s_elem, options ){ + return ajax(s_elem, null, options); + }, + submit_tags: function( s_elem, commands, options ){ + return ajax(s_elem, commands, options); + } + }, + element_constructor: function( s_elem, options ){ + options = options || {}; + Broadcaster(s_elem); + + var id = $.isFunction(options.id) ? + options.id.apply(s_elem, [s_elem]) : + options.id; + $(s_elem).attr('tag-server', id || '*'); + return $.extend({}, if_defined(id) ? { + id: id + } : {}, { + command_pipeline: options.command_pipeline || [], + defaults: options.defaults || {} + }); + }, + jquery: { + element_constructor: function( options ){ + options = options || {}; + return this.each(function(){ + var clean_options = {}; + + if ( options.id !== undefined ) { + clean_options.id = options.id; + } + if ( options.command_pipeline && options.command_pipeline.slice ) { + clean_options.command_pipeline = options.command_pipeline.slice(0); + } + if ( options.defaults ) { + clean_options.defaults = $.clone(options.defaults); + } + + Server(this, clean_options); + }); + } + } +}); + +// Slash.TagUI.Server private implementation details +// this is the one function that handles all three of the public entry-points +function ajax( s_elem, commands, options ){ + var ts = s_elem.tagui_server; + + if ( (commands = qw(commands)).length > 0 && + ts.command_pipeline && + ts.command_pipeline.length > 0 ) { + + var no_more_commands = false; + $.each(ts.command_pipeline, function(i, fn){ + commands = fn.apply(s_elem, [ commands, options ]); + if ( ! commands.length ) { + no_more_commands = true; + return false; + } + }); + if ( no_more_commands ) { + return s_elem; + } + } + + var settings = resolve_defaults(s_elem, options); + settings.request_data.tags = qw.as_string(commands); + + + function signal_event(s, o){ + Broadcaster.broadcast(s_elem, s, commands, o); + } + + signal_event('', settings.command_feedback); + signal_event(''); + $.ajax($.extend(settings.ajax, { + data: settings.request_data, + success: function( data ){ + if ( settings.ajax.dont_parse_response ) { + signal_event('', settings.success_feedback); + } else { + Broadcaster.broadcast_sequence(s_elem, ''+data, settings.success_feedback); + } + var success_fn = if_fn(settings.ajax.success); + if ( success_fn ) { + success_fn(data); + } + }, + complete: function(){ + signal_event(''); + } + })); + return s_elem; +} + +// XXX move to utils +function resolve_defaults( s_elem, caller_opts ){ + var answer = {}; + var class_opts = Server.defaults; + var this_opts = s_elem.tagui_server && s_elem.tagui_server.defaults || {}; + caller_opts = caller_opts || {}; + for ( var k in class_opts ) { + answer[k] = $.extend({}, class_opts[k], this_opts[k]||{}, caller_opts[k]||{}); + } + return answer; +} + +})(); + +// Slash.TagUI.Markup: (mostly) managing CSS classes and marking up tags +(function(){ Markup = + +// public API +new Package({ named: 'Slash.TagUI.Markup', + api: { + styles: static_styles_for_tag, + add_style_triggers: function( trigger_tags, styles ){ + update_tag_styles(tag_styles, styles, trigger_tags); + }, + refresh_styles: refresh_tag_styles_in_entry, + markup_tag: function( tag ){ + try { + return tag.replace(/^([^a-zA-Z]+)/, '$1'); + } catch (e) { + return tag; + } + }, + markup_tag_menu: function( op ){ + return '
  • '+op+'
  • '; + } + }, + jquery: { + element_api: { + refresh_styles: function(){ + return this.each(function(){ + refresh_tag_styles_in_entry(this); + }); + } + } + } +}); + +// Slash.TagUI.Markup private implementation details + +var signal_styles = { + user: 'u', + top: 't', + system: 's' +}; + +var prefix_styles = { + '!': 'bang', + '#': 'pound', + ')': 'descriptive', + '_': 'ignore', + '-': 'minus' +}; + +/* Other css classes for tags: + 'w' warning + 'd' data type + 'e' editor tag ('hold', 'back', etc) + 'f' feedback tag ('error', 'dupe', etc) + 'p' private tag + 't2' topic + 's1' section + 'y' nod + 'x' nix + */ + +var tag_styles = {}; + +function static_styles_for_tag( tag, more ){ + return qw.concat_strings( + tag_styles[ bare_tag(tag) ], + prefix_styles[ tag[0] ], + more + ); +} + +function static_styles_for_menu( op ){ + return prefix_styles[op] || prefix_styles[op[0]] || op=='x' && prefix_styles['-'] || op; +} + + +function update_tag_styles( map, styles, tags ){ + qw.each(tags, function(){ + map[this] = qw.concat_strings(map[this], styles); + }); +} + +function apply_tag_styles( $tags, styles, styles_fn ){ + // apply css classes to every element in $tags according to styles and/or styles_fn + // if styles_fn is supplied, it will be called for any tags not mapped by styles + // _and_ styles WILL BE MODIFIED + + if ( $.isFunction(styles) ) { + styles_fn = styles; + styles = {}; + } + + $tags.each(function(){ + var $tag=$(this), tag=$tag.text(); + if ( styles_fn && ! (tag in styles) ) { + styles[tag] = styles_fn(tag); + } + $tag.parent().setClass(styles[tag]); + }); +} + +function compute_tag_styles( $displays, signal_styles, static_styles_fn ){ + // return a dictionary mapping actual, non-bare, tags to css classes + + // "computed styles" is the set of css classes that tell in which displays a tag appears, + // e.g., a tag that appears in both the user and system displays gets css classes 'u s' + var signals_done={}, styles={}, signals_remaining=keys(signal_styles).length; + $displays.filter('.ready[signal]:not(.no-tags)').each(function(){ + var $display=$(this), signal=$display.attr('signal'); + if ( (signal in signal_styles) && !(signal in signals_done) ) { + update_tag_styles(styles, signal_styles[signal], $display.tagui__tags()); + signals_done[signal] = true; + return --signals_remaining!==0; + } + }); + + // "static styles" is the set of css classes based only on the tag itself + // e.g., a tag that is a section name gets css class 's1' + $.each(styles, function( tag ){ + styles[tag] = static_styles_fn(tag, styles[tag]); + }); + + return styles; +} + +function refresh_tag_styles_in_entry( entry ){ + var $displays = $('.tag-display', entry); + apply_tag_styles( + $displays.find('span.tag'), + compute_tag_styles($displays, signal_styles, get_static_styles), + get_static_styles + ); + + $displays.filter('[signal=user]').each(function(){ + var $this=$(this); + $this.toggleClassTo('no-visible-tags', ! $this.is(':has(li.u:not(.t,.s,.p,.minus))')); + }); +} + +})(); + +// Slash.TagUI.Display: an ordered, and usually visible, list of tags; Responder listens for particular categories of tags +(function(){ Display = + +// public API +new Package({ named: 'Slash.TagUI.Display', + api: { + metadata: function( d_elem ){ + var $d_elem = $(d_elem); + return { + for_display: { + tags: $d_elem.text(), + defaults: $d_elem.metadata({type:'attr', name:'init'}) + }, + for_responder: { + signals: $d_elem.attr('signal') + } + }; + }, + defaults: { + menu: 'x !' + } + }, + element_api: { + // replace existing tags and/or add new tags; preserves order of existing tags + // optional string, options.order, tells where to add new tags { 'append', 'prepend' } + // optional string, options.classes, tells a css class to add to all touched tags + update_tags: function( d_elem, tags, options ){ + options = $.extend( + {}, + { + order: 'append', + classes: '' + }, + options ); + + // invariant: before.count_tags() <= after.count_tags() + // no other call adds tags (except by calling _me_) + + // the intersection of the requested vs. existing tags are the ones I can update in-place + var update_map = map_tags(d_elem, tags = qw(tags))[0]; + + // update in-place the ones we can; build a list of the ones we can't ($.map returns a js array) + var new_tags_seen = {}; + var new_tags = $.map(tags, function(t){ + var bt = bare_tag(t); + var mt = Markup.markup_tag(t); + if ( bt in update_map ) { + $(update_map[bt]).html(mt); + } else if ( !(bt in new_tags_seen) ) { + new_tags_seen[bt] = true; + return mt; + } + }); + + // a $ list of the actual .tag elements we updated in-place + var $changed_tags = $(values(update_map)); + + if ( new_tags.length ) { + // construct all the completely new tag entries and associated machinery + var $new_elems = $(join_wrap( + new_tags, + '
  • ', + '
  • ')). + append(d_elem.tagui_display.menu_template); + + d_elem.tagui_display._$list_el[options.order]($new_elems); + + // add in a list of the actual .tag elements we created from scratch + $changed_tags = $changed_tags.add( $new_elems.find('.tag') ); + + $mark_empty(d_elem, false); + } + + // for every .tag we added/changed, fix parent
  • 's css class(es) + // Use case for options.classes: the tag was modified locally, we mark it with "not-saved" until the server + // comes back with a complete list in response that will wipe out the "not-saved" class, essentially + // confirming the user's change has been recorded + $changed_tags.each(function(){ + var $tag = $(this); + $tag.parent().setClass(Markup.styles($tag.text(), options.classes)); + }); + return d_elem; + }, + remove_tags: function( d_elem, tags, options ){ + var opts = $.extend({}, { fade_remove: 0 }, options); + + // invariant: before.count_tags() >= after.count_tags() + // no other call removes tags (except by calling _me_) + + // when called without an argument, removes all tags, otherwise + // tags to remove may be specified by string, an array, or the result of a previous call to map_tags + var if_remove_all; + if ( !tags || tags.length ) { + var mapped = map_tags(d_elem, tags); + tags = mapped[0]; + if_remove_all = mapped[1]; + } + + var $remove_li = $(values(tags)).parent(); + + if ( opts.fade_remove ) { + $remove_li + .fadeOut(opts.fade_remove) + .queue(function(){ + $(this).remove().dequeue(); + if ( if_remove_all ) { + $mark_empty(d_elem); + } + }); + } else { + $remove_li.remove(); + $mark_empty(d_elem, if_remove_all); + } + + return d_elem; + }, + // like remove_tags() followed by update_tags(tags) except order preserving for existing tags + set_tags: function( d_elem, tags, options ){ + var allowed_tags = qw.as_set(tags = qw(tags), bare_tag); + var removed_tags = map_tags(d_elem, function(bt){ + return !(bt in allowed_tags); + })[0]; + + return d_elem. + tagui_display.remove_tags(removed_tags, options). + tagui_display.update_tags(tags, options); + } + }, + element_constructor: function( d_elem, options ){ + var o = $.extend({}, Display.metadata(d_elem), options || {}); + Responder(d_elem, $.extend({ + fn: function( signal, tags, options ){ + return this.tagui_display.set_tags(tags, options); + } + }, o.for_responder)); + + var $d_elem = $(d_elem).html('
      '). + removeClass('tag-display-stub'). + addClass('tag-display no-tags'). + removeAttr('init'); + if ( o.for_display.tags ) { + d_elem.tagui_display.set_tags(o.for_display.tags); + } + + var menu = o.for_display.menu || Display.defaults.menu; + var ext = { + _$list_el: $d_elem.find('ul'), + _menu_template: menu ? ( + '
        ' + + $.map(qw(menu), function( op ){ + return Markup.markup_tag_menu(op); + }).join('') + + '
      ' ) : '' + }; + if ( o.for_display.defaults ) { + ext.defaults = o.for_display.defaults; + } + + return ext; + }, + jquery: true +}); + +// Slash.TagUI.Display private implementation details + +// return a dictionary mapping bare tags to the corresponding *.tag DOM element +function map_tags( d_elem, how ){ + // map_tags() does not add, remove, or alter any tags + + // we may limit the result, if the caller says how + var map_fn; + if ( !how ) { + // no limit, return a set of all my tags + map_fn = function(){return true;}; + } else if ( $.isFunction(how) ) { + // the caller supplied a filter function + // return a set containing only tags for which how(bare_tag(t)) answers true + map_fn = how; + } else { + // how must be a list + // return a set that is the intersection of how and the tags I actually have + var allowed_tags = qw.as_set(how, bare_tag); + map_fn = function(bt){return bt in allowed_tags;}; + } + + // now that we know how, iterate over my actual tags to build the result set + var if_mapped_all = true, map = {}; + $('.tag', d_elem).each(function(){ + var bt = bare_tag($(this).text()); + if ( map_fn(bt) ) { + map[bt] = this; + } else { + if_mapped_all = false; + } + }); + return [ map, if_mapped_all ]; +} + +function $mark_empty( d_elem, if_empty ){ + var $d_elem = $(d_elem); + if ( if_empty === undefined ) { + if_empty = ! $d_elem.is(':has(span.tag)'); + } + return $d_elem.toggleClassTo('no-tags', if_empty); +} + +function join_wrap( a, elem_prefix, elem_suffix, list_prefix, list_suffix ) { + // always returns a string, even if it's the empty string, '' + var result = ''; + a = qw(a); + if ( a && a.length ) { + var ep = elem_prefix || ''; + var es = elem_suffix || ''; + // Example: + result = (list_prefix || '') + ep + // '
      • ' + a.join(es+ep) + // .join('
      • ') + es + (list_suffix || ''); // '
      + } + return result; +} + +})(); + +// Slash.TagUI.Command +(function(){ Command = + +// public API +new Package({ named: 'Slash.TagUI.Command', + api: { + normalize_nodnix: normalize_nodnix, + normalize_tag_commands: normalize_tag_commands, + normalize_tag_menu_command: normalize_tag_menu_command + } +}); + +// Slash.TagUI.Command private implementation details + +function normalize_tag_menu_command( tag, op ){ + if ( op == "x" ) { + return '-' + tag; + } else if ( tag.length > 1 && op.length == 1 && op == tag[0] ) { + return tag.slice(1); + } else if ( op != tag ) { + return op + tag; + } else { + return tag; + } +} + +// Tags.pm doesn't automatically handle '!(nod|nix)' +// and requires (some) hand-holding to prevent an item from being tagged both nod and nix at once +var nodnix_commands = { + 'nod': ['nod', '-nix'], + 'nix': ['nix', '-nod'], + '!nod': ['nix', '-nod'], + '!nix': ['nod', '-nix'], + '-nod': ['-nod'], + '-nix': ['-nix'], + '-!nod': ['-nix'], + '-!nix': ['-nod'] +}; + +function normalize_nodnix( commands ){ + return $.map(commands, function( cmd ){ + return (cmd in nodnix_commands) ? nodnix_commands[cmd] : cmd; + }); +} + +// filters commands, returning a list 'normalized' (as per comment at 'nodnix_commands', above) +// and omitting any "add" commands for tags in excludes, or "deactivate" commands for tags _not_ in excludes +// commands is a list (string or array) +// excludes is either a list or set of tags/commands to remove, +// or else a jQuery selector (DOM element, string selector, or jQuery wrapped list) under which +// exists a user tag list... we'll build the real exclusion list from that +function normalize_tag_commands( commands, excludes ){ + + // want to iterate over commands, so ensure it is an array + commands = qw(commands); + if ( !commands.length ) { + return []; + } + + // beware, provide a complete list for excludes, or nothing at all, + // else -tag commands can be dropped on the floor + + // want to repeatedly test for inclusion in excludes, so ensure excludes is a set + if ( excludes ) { + try { + // if excludes looks like a string + if ( excludes.split ) { + // and that string works as a jQuery selector + var $temp = $(excludes); + if ( $temp.length ) { + // treat it as such + excludes = $temp; + } + // otherwise a string is probably a space-separated command list + } + + // if excludes is dom element or a jquery wrapped list... + if ( excludes.nodeType !== undefined || excludes.jquery !== undefined ) { + // ...caller means a list of the user tags within (returns an array) + excludes = cached_user_tags(excludes); + } + + // if excludes is a list (string or array)... + if ( excludes.length !== undefined ) { + excludes = qw.as_set(excludes); + } + + // excludes should already be a set, let's make sure it's not empty + if ( !keys(excludes).length ) { + excludes = null; + } + } catch (e) { + excludes = null; + } + } + + var filter_minus = true; + if ( !excludes ) { + filter_minus = false; + excludes = {}; + } + + function un( tag ){ + return tag[0]=='-' ? tag.substring(1) : '-'+tag; + } + + // .reverse(): process the commands from right to left + // so only the _last_ occurance is kept in case of duplicates + var already = {}; + return $.map(commands.reverse(), function( cmd ){ + if ( cmd && + !(cmd in already) && + !(cmd in excludes) && + ( !filter_minus || + cmd[0] != '-' || + un(cmd) in excludes ) ) { + + already[ cmd ] = true; + already[ un(cmd) ] = true; + return cmd; + } + }).reverse(); +} + +})(); + +// Slash.TagUI.Fx +(function(){ Fx = + +new Package({ named: 'Slash.TagUI.Fx' +}); + +function animate_wiggle( $selector ){ + $selector. + animate({left: '-=3px'}, 20). + animate({left: '+=6px'}, 20). + animate({left: '-=6px'}, 20). + animate({left: '+=6px'}, 20). + animate({left: '-=3px'}, 20). + queue(function(){ + $(this).css({left: ''}).dequeue(); + }); +} + + +function $position_context_display( $display ){ + var RIGHT_PADDING = 18; + + var $entry = $display.nearest_parent('[tag-server]'); + var left_edge = $entry.offset().left; + var right_edge = left_edge + $entry.width() - RIGHT_PADDING; + + var global_align = $related_trigger.offset().left; + global_align = Math.max(left_edge, global_align); + + var need_minimal_fix = true; + if ( $display.nearest_parent(':hidden').length===0 ) { + try { + var display_width = $display.children('ul:first').width(); + $display.css({ + right: '', + width: display_width + }); + + global_align = Math.max( + left_edge, + Math.min(right_edge-display_width, global_align) ); + var distance = global_align - $display.offset().left; + if ( distance ) { + $display.animate({left: '+='+distance}); + } + + need_minimal_fix = false; + } catch (e0) { + } + } + + if ( need_minimal_fix ) { + try { + var BROKEN_NEGATIVE_MARGIN_CALCULATION = -10; + + // we may not be visible, so can't trust offsetParent() on ourself + // better get it from our parent + var x_adjust = -$display.parent().offsetParent().offset().left; + $display.css({ + left: global_align + x_adjust + BROKEN_NEGATIVE_MARGIN_CALCULATION, + right: right_edge + x_adjust + }); + } catch (e1) { + } + } + + return $display; +} + +function $queue_reposition( $display, if_only_width ){ + return $display.queue(function(){ + $position_context_display($display, if_only_width).dequeue(); + }); +} + +})(); + +// Slash.TagUI.Widget: a container for Displays that manages a current "context" +(function(){ Widget = + +// public API +new Package({ named: 'Slash.TagUI.Widget', + element_api: { + set_context: set_widget_context + }, + stem_function: function(){ + } +}); + +// Slash.TagUI.Widget private implementation details + +var $previous_context_trigger = $().filter(); + +function init(){ + $init_tag_displays($('.tag-display-stub', this)); + + return this; +} + + +function set_widget_context( context, force, $related_trigger ){ + var w_elem = this, w = w_elem.tagui_widget; + + if ( context ) { + if ( context == w._current_context && + (!$previous_context_trigger.length || + $related_trigger[0] === $previous_context_trigger[0]) && !force ) { + context = ''; + } else { + if ( !(context in suggestions_for_context) && context in context_triggers ) { + context = (w._current_context != 'default') ? 'default' : ''; + } + + } + } + + // cancel any existing timeout... the context to be hidden is going away + if ( w._context_timeout ) { + clearTimeout(w._context_timeout); + w._context_timeout = null; + } + + // only have to set_tags on the display if the context really is changing + if ( context != w._current_context ) { + var context_tags = []; + if ( context && context in suggestions_for_context ) { + context_tags = qw.as_array(suggestions_for_context[context]); + } + + var has_tags = context_tags.length !== 0; + + $('.ready[signal=related]', this) + .each(function(){ + var d_elem = this, d = d_elem.tagui_display; + var $display = $(d_elem); + + var had_tags = $display.find('span.tag').length !== 0; + + // animations are automatically queued... + if ( had_tags < has_tags ) { + $display.css('display', 'none'); + } else if ( had_tags > has_tags ) { + $display.slideUp(400); + } + // ...when regular code needs to synchronize with animation + $display.queue(function(){ + // I have to queue that code up myself + d.set_tags(context_tags, { classes: 'suggestion' }); + if ( has_tags && w.modify_context ) { + w.modify_context(d_elem, context); + } + $display.dequeue(); + }); + if ( has_tags ) { + $queue_reposition($display); + if ( !had_tags ) { + $queue_reposition($display.slideDown(400)); + } + } + }); + + w._current_context = context; + } else if ( $previous_context_trigger.length && + $previous_context_trigger[0] !== $related_trigger[0] ) { + + $position_context_display($('.ready[signal=related]', this)); + } + + $previous_context_trigger = $related_trigger; + + // if there's a context to hide, and hiding on a timeout is requested... + if ( context && w.defaults.context_timeout ) { + w._context_timeout = setTimeout(function(){ + w.set_context(); + }, w.defaults.context_timeout); + } + + return this; +} + + + +function $init_tag_widgets( $stubs, options ){ + options = options || {}; + + $stubs + .each(function(){ + var $this = $(this); + + var init_data = $this.metadata({type:'attr', name:'init'}); + $this.removeAttr('init'); + + var local_state = { tag_widget_data: {} }; + if ( init_data.context_timeout ) { + local_state.tag_widget_data.context_timeout = init_data.context_timeout; + } + + $.extend( + this, + tag_widget_fns, + local_state, + options ). + init(); + }). + mapClass({'tag-widget-stub': 'tag-widget'}); + + return $stubs; +} + + + +})(); + +})(jQuery); diff --git a/plugins/Ajax/htdocs/images/slash.util.js b/plugins/Ajax/htdocs/images/slash.util.js new file mode 100644 index 000000000..e4432dda0 --- /dev/null +++ b/plugins/Ajax/htdocs/images/slash.util.js @@ -0,0 +1,483 @@ +(function($){ + +function if_defined( expr ){ + return expr !== undefined; +} + +function if_undefined( expr ){ + return expr === undefined; +} + +function if_defined_false( expr ){ + return !if_undefined(expr) && !expr; +} + +function if_object( expr ){ + return (typeof expr === 'object') && expr; +} + +function if_fn( expr ){ + return $.isFunction(expr) && expr; +} + +function if_inherits_property(obj, property_name){ +/*jslint evil: true */ + return if_defined(eval('obj.'+property_name)) && +/*jslint evil: false */ + !obj.propertyIsEnumerable(property_name); +} + +function if_inherits_method(obj, method_name){ + return if_inherits_property(obj, method_name) && + if_fn(obj[method_name]); +} + +function if_inherits_jquery(obj){ + return if_inherits_property(obj, 'jquery'); +} + +function if_inherits_string_like(obj){ + return if_inherits_method(obj, 'split') && ! if_inherits_jquery(obj); +} + +function if_inherits_array_iteration(obj){ + return if_inherits_method(obj, 'join') || if_inherits_jquery(obj); +} + + +function each( obj, fn ){ + var N = obj.length; + if ( if_undefined(N) || if_fn(obj) ) { + for ( var k in obj ) { + if ( if_defined_false(fn.call(obj[k], k, obj[k])) ) { + break; + } + } + } else { + var i = 0; + for ( var value=obj[i]; i 0; + + var o = initial_value; + each(collection, function(k, v){ + var args = [k, v]; + if ( if_others ) { + each(others, function(i, other){ + args.push(other[k]); + }); + } + accumulate_fn.apply(o, args); + }); + return o; +} + +function keys(obj){ + return accumulate([], function(k){ this.push(k); }, obj); +} + +function values(obj){ + return accumulate([], function(k, v){ this.push(v); }, obj); +} + +function rotate_list(list, n){ + if ( list.length && if_inherits_method(list, 'slice') ) { + var N = list.length; + n = ((n===undefined ? 1 : n) % N + N) % N; + return list.slice(n).concat(list.slice(0,n)); + } +} + +function qw_as_array( qw ){ + if ( ! qw ) { return []; } + + if ( if_inherits_string_like(qw) ) { + qw = (' '+qw+' ').split(/\s+/).slice(1, -1); + } + if ( ! if_inherits_array_iteration(qw) ) { + qw = accumulate([], function(k, v){if(v){this.push(k);}}, qw); + } + // else: qw already _is_ an array + + return qw; +} + +function qw_as_set( qw ){ + if ( ! qw ) { return {}; } + + if ( if_inherits_jquery(qw) || if_inherits_string_like(qw) ) { + qw = qw_as_array(qw); + } + if ( if_inherits_array_iteration(qw) ) { + qw = accumulate({}, function(k,v){this[v]=true;}, qw); + } + // else qw already _is_ a set + + return qw; +} + +function qw_as_string( qw ){ + if ( !qw ) { return ''; } + + if ( if_inherits_string_like(qw) ) { + return /\S/.test(qw) ? qw : ''; + } + // else turn it _into_ a string + return qw_as_array(qw).join(' '); +} + +function qw_concat_strings(){ + return $.map(arguments, function(v){ + var s = qw_as_string(v); + if ( s ) { + return s; + } + }).join(' '); +} + +function qw_each( qw, fn ){ + if ( ! qw ) { return; } + + if ( if_inherits_jquery(qw) || if_inherits_string_like(qw) ) { + qw = qw_as_array(qw); + } + + var use_key = ! if_inherits_array_iteration(qw); + each(qw, function(k, v){ + if ( ! if_defined_false(v) ) { + return fn.call(use_key ? k : v); + } + }); +} + +function map_toggle( list ){ + var keys = qw_as_array(list); + if ( keys.length > 1 ) { + return accumulate({}, function(i, k, v){ this[k]=v; }, keys, rotate_list(keys)); + } +} + +function splice_string( s, offset, length, replacement ){ + if ( length || replacement ) { + s = s.slice(0, offset) + (replacement||'') + s.slice(offset+(length||0)); + } + return s; +} + +function ensure_namespace( path ){ + if ( path.join ) { + path = path.slice(0); + } else { + path = qw_as_array(path.replace(/\./g, ' ')); + } + + if ( path.length ) { + var name_space = window; + if ( path[0]==='window' ) { + path.shift(); + } + while ( path.length ) { + var component_name = path.shift(); + if ( name_space[component_name] === undefined ) { + name_space[component_name] = {}; + } + name_space = name_space[component_name]; + } + + return name_space; + } +} + + +function Package( o ){ + var root_name = qw_as_array((o.named||'').replace(/\.+/g, ' ')); + var stem_name = root_name.pop(); // root_name.length > 0 implies stem_name + var estem_name = (root_name.length > 1 ? root_name.slice(-1) : []). + concat(stem_name). + join('_'). + toLowerCase(); + + var e_api = stem_name && o.element_api; + // e_api implies stem_name + + function inject_free_api( stem_obj, extra ){ + if ( ! if_defined_false(o.exports) ) { + stem_obj.__api__ = stem_obj.__api__ && [].concat(stem_obj.__api__, o) || o; + } + // roll in the element_api first, so the free api can override same-named + return $.extend(stem_obj, e_api||{}, o.api||{}, extra||{}); + } + + var defn_stem_fn = e_api && if_fn(o.element_constructor) || if_fn(o.stem_function); + function e_ctor_fn( stem_name ){ + return function( e ){ + return $.extend( + (e[stem_name] = inject_element_api(e, e_api, e[stem_name]||{})), + defn_stem_fn ? defn_stem_fn.apply(e, arguments) : clone(arguments[1]) + ); + }; + } + + var root_obj = root_name.length && ensure_namespace(root_name); + // therefore, root_obj implies stem_name + + var extant_stem_obj = root_obj && root_obj[stem_name]; + var e_ctor = e_api && e_ctor_fn(estem_name); + var stem_obj = inject_free_api(e_ctor || defn_stem_fn || extant_stem_obj || {}); + + if ( e_api ) { + stem_obj[stem_name] = e_ctor_fn(estem_name); + } + + var oj = o.jquery; + if ( oj ) { + var jstem_name = oj.named || estem_name; + + // $.jstem_name + if ( ! if_defined_false(oj.api) ) { + $[jstem_name] = if_object(oj.api) ? + inject_free_api(e_api && e_ctor_fn(jstem_name) || {}, oj.api) : + stem_obj; + } + // $(expr).jstem_name() + var je_api = oj.element_api; + var defn_jstem_fn = je_api && if_fn(oj.element_constructor) || if_fn(oj.stem_function); + var je_ctor = if_fn(defn_jstem_fn) || e_ctor && jproxy_free_fn(e_ctor); + if ( je_ctor ) { + $.fn[jstem_name] = je_ctor; + } + // $(expr).jstem_name__fn_name() + if ( ! if_defined_false(je_api) ) { + var j_prefix = jstem_name + '__'; + if ( if_object(e_api) ) { + each(e_api, function( fn_name, fn ){ if ( if_fn(fn) ) { + $.fn[j_prefix + fn_name] = je_ctor ? + function(){ + var args = arguments; + return this.each(function(){ + var fn_proxy = this[jstem_name] && this[jstem_name][fn_name]; + if ( fn_proxy ) { + fn_proxy.apply(this, args); + } + }); + } : + jproxy_free_fn(fn); + }}); + } + if ( if_object(je_api) ) { + each(je_api, function( fn_name, fn ){ if ( if_fn(fn) ) { + $.fn[j_prefix + fn_name] = fn; + }}); + } + } + } + + if ( root_obj && (extant_stem_obj !== stem_obj) ) { + if ( extant_stem_obj ) { + stem_obj = $.extend(extant_stem_obj, stem_obj); + } else { + root_obj[stem_name] = stem_obj; + } + } + + return stem_obj; +} + +// return a function, fn', such that a call fn'(a, b, c) really means fn(obj, a, b, c) { this===obj } +function proxy_fn( obj, fn ){ + return function(){ + return fn.apply(obj, [obj].concat($.makeArray(arguments))); + }; +} + +// return a function, fn', such that a call $selection.fn'(a, b, c) really means +// fn(elem, a, b, c){ this===elem } for each elem in $selection +function jproxy_free_fn( fn ){ + return function(){ + var args = arguments; + return this.each(function(){ + proxy_fn(this, fn).apply(this, args); + }); + }; +} + + +// attach any number of functions to obj such that, for each function, fn, in api_defn +// elem.fn(a, b, c) ==> api_defn.fn(elem, a, b, c){ this===elem } +// e.g., elem.tag_server.ajax(a, b, c) ==> tag_server_api.ajax(elem, a, b, c){ this===elem } +function inject_element_api(elem, api_defn, obj){ + obj = obj || elem; + each(api_defn, function(fn_name, fn){ + if ( if_fn(fn) ) { + obj[fn_name] = proxy_fn(elem, fn); + } + }); + return obj; +} + +function with_packages(){ + var result = ''; + for ( var i = 0; i < arguments.length; ++i ) { + var api_instance_name = arguments[i]; + if ( typeof api_instance_name !== 'string' ) { + continue; + } +/*jslint evil: true */ + var exports = [], api_instance = eval(api_instance_name); +/*jslint evil: false */ + if ( api_instance && api_instance.__api__ && api_instance.__api__.exports ) { + var allowed_exports = api_instance.__api__.exports.split(/\s+/); + each(allowed_exports, function(i, member_name){ + if ( member_name in api_instance ) { + exports.push(member_name); + } + }); + } + + if ( exports.length ) { + result += 'var ' + + $.map(exports, function(k, v){ + return k+'='+api_instance_name+'.'+k; + }).join(',') + + ';'; + } + } + return result; +} + +Package({ named: 'Slash.Util.Package', + api: { + with_packages: with_packages + }, + stem_function: Package +}); + +Package({ named: 'Slash.Util.if_inherits', + api: { + property: if_inherits_property, + method: if_inherits_method, + jquery: if_inherits_jquery + } +}); + +Package({ named: 'Slash.Util.qw', + api: { + as_array: qw_as_array, + as_set: qw_as_set, + as_string: qw_as_string, + concat_strings: qw_concat_strings, + each: qw_each + }, + stem_function: qw_as_array +}); + +Package({ named: 'Slash.Util', + api: { + if_defined: if_defined, + if_undefined: if_undefined, + if_defined_false: if_defined_false, + if_object: if_object, + if_fn: if_fn, + if_string_like: if_inherits_string_like, + // if_array_like: if_inherits_array_iteration, + clone: clone, + splice_string: splice_string, + ensure_namespace: ensure_namespace + }, + exports: 'if_defined if_undefined if_defined_false if_object if_fn ' + + 'if_string_like ' + + 'clone splice_string ' + + 'Package if_inherits qw' +}); + +Package({ named: 'Slash.Util.Algorithm', + api: { + each: each, + accumulate: accumulate, + keys: keys, + values: values, + rotate_list: rotate_list + }, + exports: 'each accumulate keys values rotate_list' +}); + +// Yes, I could phrase this as a Package; but I don't need to, here. +$.fn.extend({ + setClass: function( cn ) { + var fn = $.isFunction(cn) ? cn : function(){ return cn; }; + return this.each(function(){ + if ( ! (this.className = qw_as_string(fn.apply(this, [ qw_as_set(this.className) ]))) ) { + this.removeAttribute('className'); + } + }); + }, + toggleClassTo: function( cn, expr ){ + if ( ! cn ) { return this; } + var fn = if_inherits_string_like(expr) ? function(e){ return $(e).is(expr); } : function(){ return expr; }; + return this.setClass(function(cn_set){ cn_set[cn] = fn.apply(this); return cn_set; }); + }, + mapClasses: function( map ){ + var Map = accumulate({}, function(k, v){ this[k]=qw_as_set(v); }, map); + var for_unknown=Map['*'] || {}, for_all=Map['+'] || {}, for_missing=Map['?'] || {}; + return this.setClass(function(cn_set){ + var if_missing = true; + var answer = accumulate( + {}, + function(cn){ + if ( cn in Map ) { + if_missing = false; + $.extend(this, Map[cn]); + } else if ( for_unknown ) { + $.extend(this, for_unknown); + } else { + this[cn] = true; + } + }, + cn_set + ); + return $.extend(answer, for_all, if_missing ? for_missing : {}); + }); + }, + toggleClasses: function( list ){ + return this.mapClasses( map_toggle(arguments.length==1 ? list : arguments) ); + } +}); + +})(jQuery); diff --git a/plugins/Ajax/htdocs/images/t/api.html b/plugins/Ajax/htdocs/images/t/slash.util.html similarity index 50% rename from plugins/Ajax/htdocs/images/t/api.html rename to plugins/Ajax/htdocs/images/t/slash.util.html index d6ee2fff7..67fb8100c 100644 --- a/plugins/Ajax/htdocs/images/t/api.html +++ b/plugins/Ajax/htdocs/images/t/slash.util.html @@ -1,23 +1,146 @@ - Unit Tests: api.js + Unit Tests: base.js - + + + + + + + + + + + +

      Unit Tests: broadcaster.tagui.js

      + +

      +
        +
        +
        +
        +
        +
        +
        + + diff --git a/plugins/Ajax/htdocs/images/t/tagui_display.html b/plugins/Ajax/htdocs/images/t/tagui_display.html new file mode 100644 index 000000000..cfc1e7c62 --- /dev/null +++ b/plugins/Ajax/htdocs/images/t/tagui_display.html @@ -0,0 +1,89 @@ + + + Unit Tests: display.tagui.js + + + + + + + + + + + + + +

        Unit Tests: display.tagui.js

        + +

        +
          +
          +
          +
          +
          +
          +
          +
          + + diff --git a/plugins/Ajax/htdocs/images/t/responder.tagui.html b/plugins/Ajax/htdocs/images/t/tagui_responder.html similarity index 58% rename from plugins/Ajax/htdocs/images/t/responder.tagui.html rename to plugins/Ajax/htdocs/images/t/tagui_responder.html index 1ca40969d..bedb61df8 100644 --- a/plugins/Ajax/htdocs/images/t/responder.tagui.html +++ b/plugins/Ajax/htdocs/images/t/tagui_responder.html @@ -6,35 +6,39 @@ - - - + +