
// // Copyright (c) 2008, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 18 Mar 08 Andy Frank Creation // using web ** ** Widget is the base class for all web-based UI widgets. ** ** See `docLib::Widget` ** @serializable @collection abstract class Widget : Weblet { ////////////////////////////////////////////////////////////////////////// // Widget Tree ////////////////////////////////////////////////////////////////////////// ** ** Get this widget's parent or null if not mounted. ** @transient readonly Widget parent ** ** The unique name for this widget within the parent, or ** null if this widget is not mounted. ** @transient readonly Str name ** ** Iterate the children widgets. ** Void each(|Widget w, Int i| f) { kids.each(f) } ** ** Get the children widgets. ** Widget[] children() { return kids.ro } ** ** Return the child Widget with the given name, or ** null if one does not exist. ** Widget get(Str name) { return kids.find |Widget w->Bool| { return w.name == name } } ** ** Add a child widget. If child is null, then do nothing. ** If child is already parented throw ArgErr. Return this. ** virtual This add(Widget child) { if (child == null) return this if (child.parent != null) throw ArgErr("Child already parented: $child") child.parent = this child.name = "w" + nextId++ kids.add(child) return this } ** ** Remove a child widget. If child is null, then do ** nothing. If this widget is not the child's current ** parent throw ArgErr. Return this. ** virtual This remove(Widget child) { if (child == null) return this if (kids.removeSame(child) == null) throw ArgErr("not my child: $child") child.name = null return this } ** ** Remove all child widgets. Return this. ** virtual This removeAll() { kids.dup.each |Widget kid| { remove(kid) } return this } ** ** Return the uri to this Widget from the base widget, ** or null if this widget is not mounted. ** Uri uri() { if (name == null) return null path := Str[,] w := this while (w != null && w.name != null) { path.add(w.name) w = w.parent } return ("/" + path.reverse.join("/")).toUri } ** ** Return the Widget with the given Uri, or null ** if one cannot be found. ** Widget find(Uri uri) { w := this path := uri.path for (i:=0; i<path.size; i++) { w = w.get(path[i]) if (w == null) return null } return w } ////////////////////////////////////////////////////////////////////////// // Invoke ////////////////////////////////////////////////////////////////////////// ** ** Return the Uri used to invoke the given function. 'func' ** must be a Str or a Method type: ** ** toInvoke(&onPost) ** toInvoke("onPost") ** ** The Uri required to invoke functions follows the form: ** ** req.uri.plusQuery(["invoke":"$uri/$name"]) ** Uri toInvoke(Obj func) { Str name := null if (func is Func && func->method != null) { name = (func as Func).method.name } else if (func is Str) { name = func as Str } else { throw ArgErr("func must be Method or Str: $func.type") } uri := uri if (uri == null) throw Err("Widget not mounted") return req.uri.plusQuery(["invoke":"$uri/$name"]) } ** ** Invoke the Func defined by 'name'. The default ** implemenation will invoke the method on this Type ** with the given name. ** virtual Void invoke(Str name) { type.method(name).call1(this) } ////////////////////////////////////////////////////////////////////////// // Weblet ////////////////////////////////////////////////////////////////////////// ** ** Handle configuring the inital Widget pipeline. To allow ** Widgets to be nested, we use two thread local Bufs to capture ** the output for the '<head>' and '<body>' tags separately. ** After the request has been serviced, we flush the Bufs to the ** actual output stream via `complete`. ** ** If this method is called again (on any instance) after the ** initial call, it short-circuits and simply calls the default ** `web::Weblet.service` implemenation. ** override Void service() { // if service has already been called on this thread // then just route to the default implemenation if (Thread.locals["webapp.widget.head"] != null) { super.service return } try { // create bufs head := Buf(1024) body := Buf(8192) // add locals Thread.locals["webapp.widget.head"] = WebOutStream(head.out) Thread.locals["webapp.widget.body"] = WebOutStream(body.out) // verify flash exists if (req.session["webapp.widget.flash"] == null) req.session["webapp.widget.flash"] = Str:Obj[:] // if func exist, then invoke, otherwise route to super func := req.uri.query["invoke"]?.toUri if (req.method == "POST" && func != null) { try w := find(func[0..-2])->invoke(func.name) catch (Err e) throw Err("Could not invoke $func", e) } else { super.service } // complete request complete(head, body) } finally { // clear flash on gets if (req.method == "GET") flash.clear // remove locals Thread.locals.remove("webapp.widget.head") Thread.locals.remove("webapp.widget.body") } } ////////////////////////////////////////////////////////////////////////// // Methods ////////////////////////////////////////////////////////////////////////// ** ** The buffered WebOutStream for the <head> element. ** once WebOutStream head() { return Thread.locals["webapp.widget.head"] as WebOutStream } ** ** The buffered WebOutStream for the <body> element. ** once WebOutStream body() { return Thread.locals["webapp.widget.body"] as WebOutStream } ** ** A short-term map that only exists for a single GET request, ** and is then automatically cleaned up. It is convenient for ** passing notifications following a POST. ** Str:Obj flash() { return req.session["webapp.widget.flash"] as Str:Obj } ** ** Complete the current request by flushing the 'head' and 'body' ** Bufs to the actual response OutStream. If the current request ** is a GET, this method is responsible for adding the appropriate ** markup to make the resulting HTML a valid page. ** virtual Void complete(Buf head, Buf body) { // if the response is already committed, assume this is // an error or redirect, in which case, we don't need to // deal with the buffers if (res.isCommitted) return get := req.method == "GET" if (get) { // TODO - we need to get our charset without calling 'out' // which flushes the headers, so we can't set them after! charset := "UTF-8" //res.out.charset.name res.headers["Content-Type"] = "text/html; charset=$charset" res.headers["Content-Encoding"] = charset res.out.docType res.out.html res.out.head res.out.printLine("<meta http-equiv='Content-Type' content='text/html; charset=$charset'/>") } res.out.writeBuf(head.flip) if (get) { res.out.headEnd res.out.body } res.out.writeBuf(body.flip) if (get) { res.out.bodyEnd res.out.htmlEnd } } ////////////////////////////////////////////////////////////////////////// // Private ////////////////////////////////////////////////////////////////////////// private Widget[] kids := Widget[,] private Int nextId := 0 }