/* * SPL - The SPL Programming Language * Copyright (C) 2004, 2005 Clifford Wolf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * mod_wsf.spl: WebSPL Forms Library */ /** * WebSPL Forms Library * * This module provides a frame work for doing application development * with WebSPL. The basic idea is to split down the user interface into * so-called components. Each component provides a part of the DOM tree * which is displayed in the browser window. There is a seperate program * task for each component - so they can act as independent as the * programmer wants. * * This module provides [[WsfComponent]], the base object for WSF Components, * and [[WsfDocument]], the object which manages the interaction with the * Browser. * * Other Modules provide additional WSF Components. E.g.: * * [[wsf_debug:WsfDebug]] [[wsf_dialog:WsfDialog]] * [[wsf_display:WsfDisplay]] [[wsf_edit:WsfEdit]] * [[wsf_edit_sql:WsfEditSql]] [[wsf_graph:WsfGraph]] * [[wsf_menu:WsfMenu]] [[wsf_switch:WsfSwitch]] * * If the browser has support for it, [[WsfDocument]] does only send those * parts of the DOM tree to the browser which have actually changed and replace * them 'in place' in the current page using a little JavaScript hack. */ load "task"; load "cgi"; load "encode_xml"; /** * Checks the HTTP Agent string and auto-detects if the browser is able * to synamically update the DOM tree. * * It is possible to specify methods as arguments in the order of preference. * Then this methods are checked in that order. E.g.: * * var method = wsf_get_domupdate_status("iframe", "xmlhttprequest"); * * Will return "iframe", "xmlhttprequest" or "none". * * If no arguments are given the function eighter returns "iframe" or * "none". The "xmlhttprequest" method is left out in this case. * * Usually this is only used internally by the [[WsfDocument]] object to * initialize [[WsfDocument.domupdate]] and doesn't need to be called by the * user. */ function wsf_get_domupdate_status(@methods) { // Agent examples, Mozilla: "Mozilla/5.0 (X11; U; Linux i686; en-US; // rv:1.7) Gecko/20040917 Firefox/0.8" // Agent examples, KHTML: // "Mozilla/5.0 (compatible; Konqueror/3.3; Linux) (KHTML, like Gecko)" // "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12" if (elementsof methods == 0) methods = [ "iframe" ]; if (not declared cgi.agent) return "none"; foreach[] m (methods) { if (m ~== "none") return "none"; if (m ~== "iframe") { if (cgi.agent =~ /Mozilla/) { if (cgi.agent =~ /KHTML, like Gecko/) continue; if (cgi.agent =~ /compatible/) continue; return "iframe"; } continue; } if (m ~== "xmlhttprequest") { if (cgi.agent =~ /Mozilla/) return "xmlhttprequest"; continue; } } return "none"; } /** * The base WsfComponent object. All other Wsf Components are derived from * this Object. */ object WsfComponent { /** * This counter is used by the constructor to create the [[.id]] for * new instances of Wsf Components. */ static id_counter = 0; /** * The id of this component instance. It is set automatically by the * contructor and must be included as "id" attribute in the top element * of the HTML tree returned by [[.get_html()]]. * * This also is the name of the task created for this component by the * constructor. */ var id; /** * The sid (session id) for the task running this component. It must * be included as parameter 'sid' in all query strings. The methods * [[.add_action()]], [[.add_href()]] and [[.add_javascript()]] should * be used for creating links, etc. */ var sid; /** * This variable must be set to 1 whenever it is neccessary to call * [[.get_html()]] again and refresh the browser view of this component. * * It is also possible to set this to 1 if there have been any changes * in the [[.children]] array. */ var dirty = 0; /** * An array of child components. The keys may be freely choosen. The * [[.get_html()]] method must run [[.get_html()]] for all children and * include the return value in its own output. */ var children; /** * The name of the main task running the [[WsfDocument]] instance which * is responsible for this component. This is automatically set by * [[WsfDocument]] when checking for set dirty flags and by the * [[.child_set()]] method. */ var main_task; /** * Whenever a form is generated by [[.get_html()]], this method must be * used to create the "action" attribute in the
tag. A hidden * input field for the "sid" parameter mus also be generated. E.g.: * * * * ... *
*/ method add_action(url) { if ( WsfDocument.domupdate ~== "iframe") return 'target="wsftarget" action="${xml::url}"'; if ( WsfDocument.domupdate ~== "xmlhttprequest") return 'onsubmit="return wsf_req_form(this, \'${xml::url}\');"'; return 'action="${xml::url}"'; } /** * Whenever a HTML link is created by [[.get_html()]], this method * must be used to create the "href" attribute in the tag. E.g.: * * Foobar */ method add_href(url) { if ( WsfDocument.domupdate ~== "iframe") return 'target="wsftarget" href="${xml::url}"'; if ( WsfDocument.domupdate ~== "xmlhttprequest") return 'href="javascript:wsf_req_link(\'${xml::url}\')"'; return 'href="${xml::url}"'; } /** * Whenever JavaScript is used in the code created by [[.get_html()]], * this method must be used to create the statement which sets * location.href to the new value. E.g.: * * onClick="${ add_javascript("${cgi.url}?sid=${sid}&foo=bar") }" * * the URL will be quoted with single quotes in the generated code. So * there is no problem with embedding it using "onFoobar" JavaScript * event handlers. No additional quoting is done. So something like that * for creating query string parameters with JavaScript is possible too: * * onClick="${ add_javascript("${cgi.url}?sid=${sid}&foo=' + (3+5) + '") }" */ method add_javascript(url) { if ( WsfDocument.domupdate ~== "iframe") return "wsftarget.location.href='$url'"; if ( WsfDocument.domupdate ~== "xmlhttprequest") return "wsf_req_link('$url')"; return "location.href='$url'"; } /** * A simple method for calling [[.get_html_cached()]] on all children * and concatenating the results. */ method get_html_children() { var html; foreach i (children) { html = html ~ children.[i].get_html_cached(); } return html; } var __WsfComponent_get_html_cached_data; /** * This method returns the HTML code for this component. If the * [[.dirty]] flag is set, [[.get_html()]] is called to generate * the HTML code. Otherwise a cached version of the HTML code is * returned. */ method get_html_cached() { if (not dirty and defined __WsfComponent_get_html_cached_data) return __WsfComponent_get_html_cached_data; dirty = 0; return __WsfComponent_get_html_cached_data = get_html(); } /** * The method for creating the HTML code. The top HTML element must * have the attribute "id" set to the value of the [[.id]] member * variable. The default behavior is to simply return: * * '
\n' ~ get_html_children() ~ '
\n' */ method get_html() { return '
\n' ~ get_html_children() ~ '
\n'; } /** * The main function for the task running this component. It is first * called by the constructor and is running until it calls * [[task:task_co_return()]]. * * After that, [[.get_html()]] is called by the [[WsfDocument]] object * to create the HTML representation. * * The [[task:task_co_return()]] function returns when the user has done * something in his browser window which effects this component; i.e. * has clicked a link or submitted a form generated by [[.get_html()]]. * * Then this function can react to the event and call [[task:task_co_return()]] * again when it has finished processing this event. Don't forget to * set [[.dirty]] to 1 if [[.get_html()]] needs to be called again. * * If this method does not call [[task:task_co_return()]], the task will hang * in an endless loop. So don't forget to do that! */ method main() { task_co_return(); } /** * Create (substitute) a child component. * The 1st parameter is the key in the [[.children]] array, the 2nd * parameter the new component object. */ method child_set(name, obj) { if (declared children[name]) children[name].destroy(); task_co_setdefault(obj.id, obj.main_task = main_task); children[name] = obj; dirty = 1; } /** * Remove a child component. * The parameter is the key in the [[.children]] array. */ method child_remove(name) { if (declared children[name]) { children[name].destroy(); delete children[name]; dirty = 1; } } /** * The destructor. It needs to be called when the object isn't needed * anymore to kill the task assigned to that object. It also calls the * destroy method of all child components. * * It is save to call that from the [[.main()]] method. If you do so, * killing the task is postponed until [[.main()]] calls [[task:task_co_return()]] * the next time. [[.main()]] will then never return from this function * again. */ method destroy() { foreach i (children) { children.[i].destroy(); delete children.[i]; } task_late_kill(id); } /** * The constructor. */ method init() { id = "wsfc_" ~ (++id_counter); sid = cgi.sid_vm ~ ":" ~ id; task_create(id, "while (1) { main(); }", this); task_public(id); task_co_call(id); return this; } } /** * The WsfDocument object. There must be one WsfDocument object for * every browser window which is under control of WSF. Usually it * is instanciated and assigned to a variable called 'page', then * initialized and finally the [[.main()]] method is called. E.g.: * * var page = new WsfDocument(); * page.title = "My Application Title"; * page.root = new MyFunnyRootWsfComponent(); * page.main(); * * The [[.main()]] method never returns. */ object WsfDocument { /** * This variable is automatically set when the module is loaded. * It contains the return code of [[wsf_get_domupdate_status()]]. * * It is possible to change that variable before instanciating * WsfDocument the first time. After that, the variable shouldn't * be touched anymore. * * Additionally it is possible to change that variable by passing * a cgi query string parameter on program startup: * * wsf_domupdate={ iframe | xmlhttprequest | none } * * If the domupdate mechanism is running in "iframe" mode, * the iframe can be set visible by passing the query string * parameter "wsf_showiframe". * * The "xmlhttprequest" method is probably the best implementation, * but right now it is not possible to do file uploads using this * method. */ static domupdate = wsf_get_domupdate_status(); /** * If [[.domupdate]] is set to "iframe" and this variable is set to 1, * the iframe used for the domupdate mechanism will be visible. This * is only of interest for debugging purposes. */ static showiframe = 0; if ( declared cgi.param.wsf_domupdate ) domupdate = cgi.param.wsf_domupdate; if ( declared cgi.param.wsf_showiframe ) showiframe = cgi.param.wsf_showiframe; /** * This is the root component for this WsfDocument. It must be set * to an instance of [[WsfComponent]] (or any derived object) before * the [[.main()]] method is called. */ var root; /** * The content for the tag generated by this object. This * can not be changed later, if [[.domupdate]] is set to "iframe". * So it must be set before calling [[.main()]], or not at all. */ var title = ""; /** * HTML code to be included between <head> and </head>. * Must be set before calling [[.main()]], or not at all. */ var html_head = ""; /** * HTML attributes to be set in the <body> tag. * Must be set before calling [[.main()]], or not at all. */ var body_attr = ""; /** * A hash with arrays of callback functions. Use [[.callback_add]] * and [[.callback_del]] to maintain the entries. */ var callbacks; /** * Add a callback function to the [[.callbacks]] data structure. * * The following callback types are called by this object: * * pre_update called before the html (xml) output is created. * post_update called after the html (xml) output is created. */ method callback_add(type, func) { push callbacks[type], func; } /** * Add a callback function from the [[.callbacks]] data structure. */ method callback_del(type, func) { foreach i (callbacks[type]) if (callbacks[type][i] ^== func) delete callbacks[type][i]; } /** * Call all callbacks of a type. The callback is passed the type as * first parameter and this [[WfsDocument]] object as 2nd parameter. * The named parameters are also passed thru to the callback functions. */ method callback_call(type, %options) { foreach[] c (callbacks[type]) c(type, this, %options); } /** * This method implements the main loop of a [[WsfDocument]]. * It must be called after setting up the variables described * below. * * It handles the creation of HTML pages which are then passed * to the browser. Whenever the [[WsfComponent.main()]] method * calls task_co_return() after processing a user event, control * is passed back to this method so it can update the browser window. * * This function does never return. */ method main() { function do_updates2(comp) { if (comp.main_task ~!= task_getname()) { comp.main_task = task_getname(); task_co_setdefault(comp.id, task_getname()); } foreach i (comp.children) do_updates2(comp.children.[i]); } function do_updates(comp, parents) { var html; if (comp.dirty or not defined comp.__WsfComponent_get_html_cached_data) { do_updates2(comp); html ~= comp.get_html_cached(); if ( domupdate ~== "iframe" ) { html ~= <<EOT <script><!-- copy_to_parent("${comp.id}"); //--></script> EOT; } else if ( domupdate ~== "xmlhttprequest" ) { /* nothing to do */ } else { foreach p (parents) parents[p].dirty = 1; } } else { push parents, comp; foreach i (comp.children) html ~= do_updates(comp.children.[i], parents); pop parents; } return html; } while (1) { // Not stating we are XHTML - this will give us // much more freedom with bad html code... // // <?xml version="1.0" encoding="iso-8859-1"?> // <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" // "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> // <html xmlns="http://www.w3.org/1999/xhtml"> write(<<EOT <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>${title} ${html_head} EOT); if ( domupdate ~== "iframe" ) { if ( showiframe ) write(< EOT); else write(<