# HG changeset patch # User jbe/bsw # Date 1265996422 -3600 # Node ID d76a8857ba629e716ee61194af3e4ed807214542 # Parent e017c47d43b5bd576859b50bfd202add708f94c8 Added ui.partial and other functions, which allow partial content replacement using XMLHttpRequests; Image support for ui.link Also includes following changes: - Fix for rocketcgi library to accept POST data content-types, which contain additional charset information. - Support arrays passed as params to encode.url (only for keys ending with "[]") - Version information changed to "1.0.7" Documentation for added functions is not yet complete. diff -r e017c47d43b5 -r d76a8857ba62 doc/autodoc-header.htmlpart --- a/doc/autodoc-header.htmlpart Wed Feb 03 00:57:18 2010 +0100 +++ b/doc/autodoc-header.htmlpart Fri Feb 12 18:40:22 2010 +0100 @@ -55,10 +55,10 @@ color: #505050; } - WebMCP 1.0.6 Documentation + WebMCP 1.0.7 Documentation -

WebMCP 1.0.6 Documentation

+

WebMCP 1.0.7 Documentation

WebMCP is a completely new web development framework, and has not been extensively tested yet. The API might change at any time, but in future releases there will be a list of all changes, which break downward compatibility.

diff -r e017c47d43b5 -r d76a8857ba62 framework/cgi-bin/webmcp.lua --- a/framework/cgi-bin/webmcp.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/cgi-bin/webmcp.lua Fri Feb 12 18:40:22 2010 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env lua -_WEBMCP_VERSION = "1.0.6" +_WEBMCP_VERSION = "1.0.7" -- include "../lib/" in search path for libraries do @@ -388,7 +388,7 @@ if not success then local errobj = error_info.errobj local stacktrace = error_info.stacktrace - if not request.get_status() then + if not request.get_status() and not request.get_json_request_slots() then request.set_status("500 Internal Server Error") end slot.set_layout('system_error') @@ -417,6 +417,10 @@ if slot_dump ~= "" then redirect_params.tempstore = tempstore.save(slot_dump) end + local json_request_slots = request.get_json_request_slots() + if json_request_slots then + redirect_params["_webmcp_json_slots[]"] = json_request_slots + end cgi.redirect( encode.url{ base = request.get_absolute_baseurl(), diff -r e017c47d43b5 -r d76a8857ba62 framework/env/encode/url.lua --- a/framework/env/encode/url.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/env/encode/url.lua Fri Feb 12 18:40:22 2010 +0100 @@ -81,7 +81,14 @@ add("_webmcp_id=", encode.url_part(id), "&") end for key, value in pairs(params) do - add(encode.url_part(key), "=", encode.url_part(value), "&") + -- TODO: better way to detect arrays? + if string.match(key, "%[%]$") then + for idx, entry in ipairs(value) do + add(encode.url_part(key), "=", encode.url_part(entry), "&") + end + else + add(encode.url_part(key), "=", encode.url_part(value), "&") + end end result[#result] = nil -- remove last '&' or '?' end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/__init.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/__init.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,2 @@ +ui._partial_loading_enabled = false +ui._partial_state = nil diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/_partial_load_js.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/_partial_load_js.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,93 @@ +--[[-- +ui._partial_load_js{ +} + +TODO: documentation + +NOTE: may return nil + +--]]-- + +function ui._partial_load_js(args, mode) + local args = args or {} + local module + local view + local id + local params = {} + local target + if args.view and args.target then + module = args.module + view = args.view + id = args.id + target = args.target + elseif not args.view and not args.target then + if not ui._partial_state then + return nil + end + module = ui._partial_state.module + view = ui._partial_state.view + id = ui._partial_state.id + target = ui._partial_state.target + else + error("Unexpected arguments passed to ui._partial_load_js{...}") + end + + if ui._partial_state then + if ui._partial_state.params then + for key, value in pairs(ui._partial_state.params) do + params[key] = value + end + end + for param_name, dummy in pairs(ui._partial_state.param_name_hash) do + params[param_name] = cgi.params[param_name] + end + end + if args.params then + for key, value in pairs(args.params) do + params[key] = value + end + end + local encoded_url = encode.json( + encode.url{ + module = module, + view = view, + id = id, + params = params + } + ) + + if mode == "form_normal" then + -- NOTE: action in "action_mode" refers to WebMCP actions, while action + -- in "this.action" refers to the action attribute of HTML forms + slot.put('this.action = ', encoded_url, '; ') + end + + return slot.use_temporary(function() + slot.put( + 'partialMultiLoad({', + -- mapping: + '"trace": "trace", "system_error": "system_error", ', + encode.json(target), ': "default" }, ', + -- tempLoadingContents: + '{}, ', + -- failureContents: + '"error", ', + -- url: + (mode == "form_normal" or mode == "form_action") and ( + 'this' + ) or ( + encoded_url + ), ', ', + -- urlParams: + '"_webmcp_json_slots[]=default&_webmcp_json_slots[]=trace&_webmcp_json_slots[]=system_error", ', + -- postParams: + '{}, ', + -- successHandler: + 'function() {}, ', + -- failureHandler: + 'function() {} ', + '); ', + 'return false;' + ) + end) +end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/add_partial_param_names.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/add_partial_param_names.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,16 @@ +--[[-- +ui.add_partial_param_names( + name_list +) + +TODO: documentation + +--]]-- + +function ui.add_partial_param_names(name_list) + if ui._partial_state then + for idx, param_name in ipairs(name_list) do + ui._partial_state.param_name_hash[param_name] = true + end + end +end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/enable_partial_loading.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/enable_partial_loading.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,11 @@ +--[[-- +ui.enable_partial_loading() + +TODO: documentation + +--]]-- + +function ui.enable_partial_loading() + ui._partial_loading_enabled = true + request.force_absolute_baseurl() +end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/form.lua --- a/framework/env/ui/form.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/env/ui/form.lua Fri Feb 12 18:40:22 2010 +0100 @@ -27,6 +27,35 @@ --]]-- +local function prepare_routing_params(params, routing, default_module) + local routing_default_given = false + if routing then + for status, settings in pairs(routing) do + if status == "default" then + routing_default_given = true + end + local module = settings.module or default_module or request.get_module() + assert(settings.mode, "No mode specified in routing entry.") + assert(settings.view, "No view specified in routing entry.") + params["_webmcp_routing." .. status .. ".mode"] = settings.mode + params["_webmcp_routing." .. status .. ".module"] = module + params["_webmcp_routing." .. status .. ".view"] = settings.view + params["_webmcp_routing." .. status .. ".id"] = settings.id + if settings.params then + for key, value in pairs(settings.params) do + params["_webmcp_routing." .. status .. ".params." .. key] = value + end + end + end + end + if not routing_default_given then + params["_webmcp_routing.default.mode"] = "forward" + params["_webmcp_routing.default.module"] = request.get_module() + params["_webmcp_routing.default.view"] = request.get_view() + end + return params +end + function ui.form(args) local args = args or {} local slot_state = slot.get_state_table() @@ -39,31 +68,7 @@ else slot_state.form_readonly = false local params = table.new(args.params) - local routing_default_given = false - if args.routing then - for status, settings in pairs(args.routing) do - if status == "default" then - routing_default_given = true - end - local module = settings.module or args.module or request.get_module() - assert(settings.mode, "No mode specified in routing entry.") - assert(settings.view, "No view specified in routing entry.") - params["_webmcp_routing." .. status .. ".mode"] = settings.mode - params["_webmcp_routing." .. status .. ".module"] = module - params["_webmcp_routing." .. status .. ".view"] = settings.view - params["_webmcp_routing." .. status .. ".id"] = settings.id - if settings.params then - for key, value in pairs(settings.params) do - params["_webmcp_routing." .. status .. ".params." .. key] = value - end - end - end - end - if not routing_default_given then - params["_webmcp_routing.default.mode"] = "forward" - params["_webmcp_routing.default.module"] = request.get_module() - params["_webmcp_routing.default.view"] = request.get_view() - end + prepare_routing_params(params, args.routing, args.module) params._webmcp_csrf_secret = request.get_csrf_secret() local attr = table.new(args.attr) attr.action = encode.url{ @@ -73,6 +78,45 @@ action = args.action, } attr.method = args.method and string.upper(args.method) or "POST" + if ui.is_partial_loading_enabled() and args.partial then + attr.onsubmit = slot.use_temporary(function() + local partial_mode = "form_normal" + if args.action then + partial_mode = "form_action" + slot.put( + 'var element; ', + 'var formElements = []; ', + 'for (var i=0; i= 0) { ', + 'element.parentNode.removeChild(element); ', + '} ', + '}' + ) + local routing_params = {} + prepare_routing_params( + routing_params, + args.partial.routing, + args.partial.module + ) + for key, value in pairs(routing_params) do + slot.put( + ' ', + 'element = document.createElement("input"); ', + 'element.setAttribute("type", "hidden"); ', + 'element.setAttribute("name", ', encode.json(key), '); ', + 'element.setAttribute("value", ', encode.json(value), '); ', + 'this.appendChild(element);' + ) + end + slot.put(' ') + end + slot.put(ui._partial_load_js(args.partial, partial_mode)) + end) + end if slot_state.form_opened then error("Cannot open a non-readonly form inside a non-readonly form.") end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/is_partial_loading_enabled.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/is_partial_loading_enabled.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,11 @@ +--[[-- +result = +ui.is_partial_loading_enabled() + +TODO: documentation + +--]]-- + +function ui.is_partial_loading_enabled() + return ui._partial_loading_enabled +end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/link.lua --- a/framework/env/ui/link.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/env/ui/link.lua Fri Feb 12 18:40:22 2010 +0100 @@ -9,7 +9,13 @@ params = params, -- optional parameters to be passed to the view or action routing = routing, -- optional routing information for action links, as described for ui.form{...} text = text, -- link text - content = content -- link content (overrides link text, except for submit buttons for action calls without JavaScript) + content = content, -- link content (overrides link text, except for submit buttons for action calls without JavaScript) + partial = { -- TODO: documentation + module = module, + view = view, + id = id, + params = params + } } This function inserts a link into the active slot. It may be either an internal application link ('module' given and 'view' or 'action' given), or a link to an external web page ('external' given), or a link to a file in the static file directory of the application ('static' given). @@ -21,7 +27,9 @@ local content = args.content or args.text assert(content, "ui.link{...} needs a text.") local function wrapped_content() - -- TODO: icon/image + if args.image then + ui.image(args.image) + end if type(content) == "function" then content() else @@ -49,6 +57,7 @@ id = args.id, params = args.params, routing = args.routing, + partial = args.partial, attr = form_attr, content = function() ui.submit{ text = args.text, attr = args.submit_attr } @@ -85,6 +94,9 @@ id = args.id, params = args.params, } + if ui.is_partial_loading_enabled() and args.partial then + a_attr.onclick = ui._partial_load_js(args.partial) + end return ui.tag{ tag = "a", attr = a_attr, content = wrapped_content } end end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/paginate.lua --- a/framework/env/ui/paginate.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/env/ui/paginate.lua Fri Feb 12 18:40:22 2010 +0100 @@ -46,13 +46,22 @@ if current_page == page then attr.class = "active" end + local partial + if ui.is_partial_loading_enabled() then + partial = { + params = { + [name] = tostring(page) + } + } + end ui.link{ - attr = attr, + attr = attr, module = request.get_module(), view = request.get_view(), id = id, params = params, - text = tostring(page) + text = tostring(page), + partial = partial } end end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/partial.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/env/ui/partial.lua Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,26 @@ +--[[-- +ui.partial{ + module = + view = + id = + params = + target = + content = function() + ... -- + end +} + +TODO: documentation + +--]]-- + +function ui.partial(args) + local old_state = ui._partial_state + ui._partial_state = table.new(args) + ui._partial_state.param_name_hash = {} + if args.param_names then + ui.add_partial_param_names(args.param_names) + end + args.content() + ui._partial_state = old_state +end diff -r e017c47d43b5 -r d76a8857ba62 framework/env/ui/script.lua --- a/framework/env/ui/script.lua Wed Feb 03 00:57:18 2010 +0100 +++ b/framework/env/ui/script.lua Fri Feb 12 18:40:22 2010 +0100 @@ -9,7 +9,7 @@ This function is used to insert a script into the active slot. -WARNING: The given script MUST NOT include two closing square brackets directly followed by a greater-than sign, unless the output is interpreted strictly as XHTML. For string literals this is ensured automatically, if being encoded with encode.json{...}. +WARNING: If the script contains two closing square brackets directly followed by a greater-than sign, it will be rejected to avoid ambiguity related to HTML vs. XML parsing. Additional space characters can be added within the program code to avoid occurrence of the character sequence. The function encode.json{...} encodes all string literals in a way that the sequence is not contained. --]]-- @@ -31,19 +31,21 @@ if attr.src then ui.tag{ tag = "script", attr = attr, content = "" } elseif script then + local script_string + if type(script) == "function" then + script_string = slot.use_temporary(script) + else + script_string = script + end + if string.find(script_string, "]]>") then + error('Script contains character sequence "]]>" and is thus rejected to avoid ambiguity. If this sequence occurs as part of program code, please add additional space characters. If this sequence occurs inside a string literal, please encode one of this characters using the \\uNNNN unicode escape sequence.') + end ui.tag{ tag = "script", attr = attr, content = function() slot.put("/* ", "]]]]>"))) + slot.put(script_string) slot.put("/* ]]> */") end } diff -r e017c47d43b5 -r d76a8857ba62 framework/js/partialload.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/js/partialload.js Fri Feb 12 18:40:22 2010 +0100 @@ -0,0 +1,271 @@ + +partialload_queue = []; +partialload_queueRPos = 0; +partialload_queueWPos = 0; + +function partialload_getFormKeyValuePairs(form) { + var result = {}; + for (var i=0; i= 0) { + if (url.search(/&$/) >= 0) { + url = url + params; + } else { + url = url + "&" + params; + } + } else { + url = url + "?" + params; + } + } + return url; +} + +function partialload_mergeEncodedFormData(data1, data2) { + if (data2 == null || data2 == "") return data1; + if (data1 == null || data1 == "") return data2; + return data1 + "&" + data2; +} + +function partialload_startNextRequest() { + var entry = partialload_queue[partialload_queueRPos++]; + var req = new XMLHttpRequest(); + req.open(entry.method, entry.url, true); + req.onreadystatechange = function() { + if (req.readyState == 4) { + if (req.status == 200) { + if (entry.successHandler != null) entry.successHandler(req.responseText); + } else { + if (entry.failureHandler != null) entry.failureHandler(); + } + if (partialload_queue[partialload_queueRPos]) { + partialload_startNextRequest(); + } else { + partialload_queue = []; + partialload_queueRPos = 0; + partialload_queueWPos = 0; + } + } + } + if (entry.data) { + req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } + req.send(entry.data); +} + +function queuedHttpRequest( + url_or_form, + urlParams, + postParams, + successHandler, + failureHandler +) { + var method; + var data = null; + if (typeof(postParams) == "string") { + data = postParams; + } else if (postParams != null) { + data = partialload_encodeFormData(postParams); + } + var url; + if (typeof(url_or_form) == "object") { + // form element given + var form = url_or_form; + url = partialload_addFormDataToUrl(form.action, urlParams); + var dataFromForm = partialload_encodeFormData( + partialload_getFormKeyValuePairs(form) + ); + if (form.method != null && form.method.search(/^POST$/i) >= 0) { + method = "POST"; + data = partialload_mergeEncodedFormData(data, dataFromForm); + } else { + method = (postParams == NULL) ? "GET" : "POST"; + url = partialload_addFormDataToUrl(url, dataFromForm); + } + } else { + // URL given + url = partialload_addFormDataToUrl(url_or_form, urlParams); + if (postParams == null) { + method = "GET"; + } else { + method = "POST"; + if (typeof(postParams) == "string") { + data = postParams; + } else { + data = partialload_encodeFormData(postParams); + } + } + } + partialload_queue[partialload_queueWPos++] = { + method: method, + url: url, + data: data, + successHandler: successHandler, + failureHandler: failureHandler + }; + if (partialload_queueRPos == 0) { + partialload_startNextRequest(); + } +} + +function setHtmlContent(node, htmlWithScripts) { + var uniquePrefix = "placeholder" + Math.floor(Math.random()*10e16) + "_"; + var i = 0; + var scripts = []; + var htmlWithPlaceholders = ""; + // NOTE: This function can not handle CDATA blocks at random positions. + htmlWithPlaceholders = htmlWithScripts.replace( + /]*>(.*?)<\/script>/ig, + function(all, inside) { + scripts[i] = inside; + var placeholder = ''; + i++; + return placeholder; + } + ) + node.innerHTML = htmlWithPlaceholders; + var documentWriteBackup = document.write; + var documentWritelnBackup = document.writeln; + var output; + document.write = function(str) { output += str; } + document.writeln = function(str) { output += str + "\n"; } + for (i=0; i 0) { + var childNode = placeholderNode.childNodes[0]; + placeholderNode.removeChild(childNode); + placeholderNode.parentNode.insertBefore(childNode, placeholderNode); + } + } + placeholderNode.parentNode.removeChild(placeholderNode); + } + document.write = documentWriteBackup; + document.writeln = documentWritelnBackup; +} + +function partialLoad( + node, + tempLoadingContent, + failureContent, + url_or_form, + urlParams, + postParams, + successHandler, + failureHandler +) { + if (typeof(node) == "string") node = document.getElementById(node); + if (tempLoadingContent != null) setHtmlContent(node, tempLoadingContent); + queuedHttpRequest( + url_or_form, + urlParams, + postParams, + function(response) { + setHtmlContent(node, response); + if (successHandler != null) successHandler(); + }, + function() { + if (failureContent != null) setHtmlContent(node, failureContent); + if (failureHandler != null) failureHandler(); + } + ); +} + +function partialMultiLoad( + mapping, + tempLoadingContents, + failureContents, + url_or_form, + urlParams, + postParams, + successHandler, + failureHandler +) { + if (mapping instanceof Array) { + var mappingHash = {} + for (var i=0; i