--[[ - @author Bruno Massa - @file form.lua - Ajato provides a rich featured Form API to let developers - easely create forms. ]] --[[ - Convert a table into HTML tag attributes. Some tag need - attributes, and this function provides a fast and reliable - way to insert them. Like - - @param attributes - Table, the tag attributes and values - @return - HTML, the tag attribute format ]] function ajato_element_attributes(attributes) local output = {} attributes = attributes or {} for index, value in pairs(attributes) do if type(value) == 'table' or type(value) == 'string' or type(value) == 'number' then if type(value) == 'table' then value = table.concat(value) end -- All (X)HTML values should come between double quotes output[#output + 1] = string.format('%s=%q', index, value) end end return table.concat(output, ' ') end --[[ - Return a list of attributes from a given form field or - a list of children fields - - @param element - Table, the tag element - @param only_attributes - Boolean, TRUE to list only attributes (those that start with "#") - @param content - Boolean, TRUE to add the field content - @return - Function, a pairs() with all fields that fill the requirements ]] function ajato_element_children(element, only_attributes, content) if type(element) ~= 'table' then return end local children = {} local attribute -- Check every field for field in pairs(element) do if type(field) == 'string' then -- Check if the index start with the "#" attribute = string.find(tostring(field), '#', 1) if (only_attributes and attribute) or (not only_attributes and not attribute) then -- Add the content to if content then children[field] = element[field] else children[#children + 1] = field end end end end return children end --[[ - Retrieve the default properties for the defined element type. - - @param el_type - String, the element type - @param reset - Boolean, TRUE to remake the "cache" - @return - Table, the element properties ]] function ajato_element_info(el_type, reset) local cache return function(el_type, reset) if not hv(cache) or reset then cache = {} for _, module in pairs(ajato_module_implements('elements')) do local elements = _G[module ..'_elements']() if elements and type(elements) == 'table' then cache = ajato_tables_merge(cache, elements) end end if #cache then for element_type, info in pairs(cache) do cache[element_type] = ajato_tables_merge({ ['#description'] = nil, ['#required'] = false, ['#tree'] = false }, info) end end end return cache[el_type] or {} end end; ajato_element_info = ajato_element_info() --[[ - Return a pairs function with a list of attributes from a - given form field or a list of children fields - - @param element - Table, the tag element - @param only_attributes - Boolean, true in case to list only attributes (those that - start with "#") - @return - Function, a pairs() with all fields that fill the requirements ]] function ajato_element_pairs(element, only_attributes) -- Return the pairs function return pairs(ajato_element_children(element, only_attributes, true)) end --[[ - Renders HTML given a structured table tree. Recursively iterates over each - of the table elements, generating HTML code. This function is usually - called from within a another function, like ajato_form_get() or node_view(). - - @param elements - Table, The data to be rendered. - @return - HTML, The rendered HTML. ]] function ajato_element_render(elements) if not elements or elements['#rendered'] or (elements['#access'] and not hv(elements['#access'])) then return '' end elements['#rendered'] = true -- If the default values for this element haven't been loaded yet, -- populate them. if not hv(elements['#defaults_loaded']) then local info = ajato_element_info(elements['#type']) if not hv(elements['#type']) and hv(info) then elements = ajato_tables_merge(elements, info) end end -- Make any final changes to the element before it is rendered. This means -- that the element or the children can be altered or corrected before the -- element is rendered into the final text. if elements['#pre_render'] then for _, func in pairs(elements['#pre_render']) do if _G[func] then elements = _G[func](elements) end end end -- Element output local content = ajato_string() -- Either the elements did not go through form_builder -- or one of the children has a #weight. if not elements['#sorted'] then -- uasort(elements, '_element_sort') end elements = ajato_tables_merge({['#title'] = nil, ['#description'] = nil}, elements) -- If there is no children elements if not elements['#children'] then local children = ajato_element_children(elements) -- Render all the children that use a theme function if elements['#theme'] and not hv(elements['#theme_used']) then elements['#theme_used'] = true local previous = {} for _, key in pairs({'#value', '#type', '#prefix', '#suffix'}) do previous[key] = elements[key] end -- If we rendered a single element, then we will skip the renderer. if not hv(children) then elements['#printed'] = true else elements['#value'] = '' end elements['#type'] = 'markup' elements['#prefix'] = nil elements['#suffix'] = nil content = content .. theme(elements['#theme'], elements) for _, key in pairs({'#value', '#type', '#prefix', '#suffix'}) do elements[key] = previous[key] end end -- Render each of the children using ajato_element_render and concatenate them for _, key in pairs(children) do content = content .. ajato_element_render(elements[key]) end end if hv(tostring(content)) then elements['#children'] = tostring(content) end -- Until now, we rendered the children, here we render the element itself if not hv(elements['#printed']) then content = theme((elements['#type'] or 'markup'), elements) elements['#printed'] = true end content = tostring(content) if hv(content) then -- Filter the outputted content and make any last changes before the -- content is sent to the browser. The changes are made on content -- which allows the output'ed text to be filtered. if elements['#post_render'] then for _, func in pairs(elements['#post_render']) do if _G[func] then content = _G[func](content, elements) end end end local prefix = elements['#prefix'] or '' local suffix = elements['#suffix'] or '' return prefix .. content .. suffix end end --[[ - @defgroup form Form generation - @{ - Functions to enable the processing and display of HTML forms. - - Ajato uses these functions to achieve consistency in its form processing and - presentation, while simplifying code and reducing the amount of HTML that - must be explicitly generated by modules. - - The ajato_form_get() function handles retrieving, processing, and - displaying a rendered HTML form for modules automatically. For example: - - -- Display the user registration form. - output = ajato_form_get('user_register') - - Forms can also be built and submitted programmatically without any user input - using the ajato_form_execute() function. ]] --[[ - Walk through the structured form table, adding any required - properties to each element and mapping the incoming sys['post'] - data to the proper elements. - - @param form_id - String, A unique string identifying the form for validation, - submission, theming, and hook_form_alter functions. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. In this - context, it is used to accumulate information about which button - was clicked when the form was submitted, as well as the sanitized - sys['post'] data. ]] function ajato_form_build(form_id, form, form_state) -- Initialize as unprocessed. form['#processed'] = false -- Use element defaults local info = ajato_element_info(form['#type']) if hv(form['#type']) and info then -- Overlay info onto form, retaining preexisting keys in form. form = ajato_tables_merge(info, form) end if form['#type'] and form['#type'] == 'form' and hv(form['#programmed']) then form_state['submitted'] = true end if form['#input'] and hv(form['#input']) then ajato_form_build_element(form_id, form, form_state) end form['#defaults_loaded'] = true -- We start off assuming all form elements are in the correct order. form['#sorted'] = true -- Recurse through all child elements. local count = 0 local children = ajato_element_children(form) if hv(children) then for _, key in pairs(children) do form[key]['#post'] = form['#post'] form[key]['#programmed'] = form['#programmed'] -- Don't squash an existing tree value. if not form[key]['#tree'] then form[key]['#tree'] = form['#tree'] end -- Deny access to child elements if parent is denied. if form['#access'] and not hv(form['#access']) then form[key]['#access'] = false end -- Don't squash existing parents value. if not form[key]['#parents'] then -- Check to see if a tree of child elements is present. If so, -- continue down the tree if required. if form[key]['#tree'] and form['#tree'] then form[key]['#parents'] = ajato_tables_merge({key}, form['#parents'], true) else form[key]['#parents'] = {key} end end -- Assign a decimal placeholder weight to preserve original table order. if not form[key]['#weight'] then form[key]['#weight'] = count/1000 else -- If one of the child elements has a weight then we will need to sort -- later. form['#sorted'] = nil end form[key] = ajato_form_build(form_id, form[key], form_state) count = count + 1 end end -- The #after_build flag allows any piece of a form to be altered -- after normal input parsing has been completed. if form['#after_build'] and not hv(form['#after_build_done']) then for func in pairs(form['#after_build']) do form = _G[func](form, form_state) form['#after_build_done'] = true end end -- Now that we've processed everything, we can go back to handle the funky -- Internet Explorer button-click scenario. ajato_form_build_ie_cleanup(form, form_state) -- After handling the special IE case, we no longer need the buttons collection. form_state['buttons'] = nil return form end --[[ - Populate the #value and #name properties of input elements so they - can be processed and rendered. Also, execute any #process handlers - attached to a specific element. - - @param form_id - String, A unique string identifying the form for validation, - submission, theming, and hook_form_alter functions. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. In this - context, it is used to accumulate information about which button - was clicked when the form was submitted, as well as the sanitized - sys['post'] data. ]] function ajato_form_build_element(form_id, form, form_state) -- Define the attribute name for those dont have yet if not form['#name'] then local name = table.remove(form['#parents']) form['#name'] = name if form['#type'] == 'file' then -- To make it easier to handle files in file.lua, we place all -- file fields in the 'files' table. Also, we do not support -- nested file names. form['#name'] = 'files['.. form['#name'] ..']' elseif hv(#form['#parents']) then form['#name'] = form['#name'] .. '['.. table.concat(form['#parents'], '][') ..']' end table.insert(form['#parents'], 1, name) end -- Define the attribute id for those dont have yet if not form['#id'] then form['#id'] = ajato_form_clean_id('edit-'.. table.concat(form['#parents'], '-')) end -- Disabled fields if hv(form['#disabled']) then form['#attributes'] = form['#attributes'] or {} form['#attributes']['disabled'] = 'disabled' end if not form['#value'] then local func = form['#value_callback'] or 'ajato_form_type_'.. form['#type'] ..'_value' -- Check if its was already submitted if form['#programmed'] or ((not form['#access'] or hv(form['#access'])) and form['#post'] and form['#post']['form_id'] and form['#post']['form_id'] == form_id) then local edit = form['#post'] for _, parent in pairs(form['#parents']) do if edit[parent] then edit = edit[parent] else edit = nil break end end if not hv(form['#programmed']) or edit then -- Call #type_value to set the form value; if _G[func] then form['#value'] = _G[func](form, edit) end if not form['#value'] and edit then form['#value'] = edit end end -- Mark all posted values for validation. if form['#value'] or hv(form['#required']) then form['#needs_validation'] = true end end -- Load defaults. if not form['#value'] then -- Call #type_value without a second argument to -- request default_value handling. if _G[func] then form['#value'] = _G[func](form) end -- Final catch. If we haven't set a value yet, -- use the explicit default value. if not form['#value'] then form['#value'] = form['#default_value'] or '' end end end -- Determine which button (if any) was clicked to submit the form. -- We compare the incoming values with the buttons defined in the form, -- and flag the one that matches. We have to do some funky tricks to -- deal with Internet Explorer's handling of single-button forms, though. if hv(form['#post']) and form['#executes_submit_callback'] then -- First, accumulate a collection of buttons, divided into two bins: -- those that execute full submit callbacks and those that only validate. if not form_state['buttons'] then form_state['buttons'] = {} end button_type = ((form['#executes_submit_callback'] and 'submit') or 'button') if not form_state['buttons'][button_type] then form_state['buttons'][button_type] = {} end form_state['buttons'][button_type][#form_state['buttons'][button_type] + 1] = form if ajato_form_button_was_clicked(form) then form_state['submitted'] = form_state['submitted'] or form['#executes_submit_callback'] -- In most cases, we want to use form_set_value() to manipulate -- the global variables. In this special case, we want to make sure that -- the value of this element is listed in form_variables under 'op'. form_state['values'][form['#name']] = form['#value'] form_state['clicked_button'] = form if form['#validate'] then form_state['validate_handlers'] = form['#validate'] end if form['#submit'] then form_state['submit_handlers'] = form['#submit'] end end end -- Allow for elements to expand to multiple elements, e.g., radios, -- checkboxes and files. if form['#process'] and not form['#processed'] then for _, process in pairs(form['#process']) do if _G[process] then _G[process](form) else ajato_add_message(t('The function %func does not exist', {['%func'] = process}), 'error') end end form['#processed'] = true end ajato_form_set_value(form, form_state, form['#value']) end --[[ - In IE, if only one submit button is present, AND the enter key is - used to submit the form, no form value is sent for it and our normal - button detection code will never detect a match. We call this - function after all other button-detection is complete to check - for the proper conditions, and treat the single button on the form - as 'clicked' if they are met. - - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. In this - context, it is used to accumulate information about which button - was clicked when the form was submitted, as well as the sanitized - sys['post'] data. ]] function ajato_form_build_ie_cleanup(form, form_state) -- Quick check to make sure we're always looking at the full form -- and not a sub-element. if hv(form['#type']) and form['#type'] == 'form' then -- If we haven't recognized a submission yet, and there's a single -- submit button, we know that we've hit the right conditions. Grab -- the first one and treat it as the clicked button. if not hv(form_state['submitted']) and hv(form_state['buttons']) and hv(form_state['buttons']['submit']) and not hv(form_state['buttons']['button']) then button = form_state['buttons']['submit'][0] -- Set up all the form_state information that would have been -- populated had the button been recognized earlier. form_state['submitted'] = true form_state['submit_handlers'] = button['#submit'] or nil form_state['validate_handlers'] = button['#validate'] or nil form_state['values'][button['#name']] = button['#value'] form_state['clicked_button'] = button end end end --[[ - Helper function to handle the sometimes-convoluted logic of button - click detection. - - In Internet Explorer, if ONLY one submit button is present, AND the - enter key is used to submit the form, no form value is sent for it - and we'll never detect a match. That special case is handled by - ajato_form_builder_ie_cleanup(). - - @param form - Table, the entire form table - @return - Boolean, TRUE when its ok and valid ]] function ajato_form_button_was_clicked(form) -- First detect normal 'vanilla' button clicks. Traditionally, all -- standard buttons on a form share the same name (usually 'op'), -- and the specific return value is used to determine which was -- clicked. This ONLY works as long as form['#name'] puts the -- value at the top level of the tree of sys['post'] data. -- -- When image buttons are clicked, browsers do NOT pass the form element -- value in sys['post']. Instead they pass an integer representing the -- coordinates of the click on the button image. This means that image -- buttons MUST have unique form['#name'] values, but the details of -- their sys['post'] data should be ignored. if form['#post'][form['#name']] and form['#post'][form['#name']] == form['#value'] then return true elseif hv(form['#has_garbage_value']) and form['#value'] and form['#value'] ~= '' then return true end return false end --[[ - Fetch a form from cache ]] function ajato_form_cache_get(form_build_id, form_state) -- Get the form structure, if available local cached = ajato_cache_get('form_'.. form_build_id, 'cache_form') if hv(cached) then form = cached['data'] -- Get the form state, if available cached = ajato_cache_get('storage_'.. form_build_id, 'cache_form') if hv(cached) then form_state['storage'] = cached['data'] end end return form, form_state end --[[ - Store a form in the cache ]] function ajato_form_cache_set(form_build_id, form, form_state) local expire = max(ini_get('session.cookie_lifetime'), 86400) -- Store the form structure ajato_cache_set('form_'.. form_build_id, form, 'cache_form', expire) -- Store the form state, if available if hv(form_state['storage']) then ajato_cache_set('storage_'.. form_build_id, form_state['storage'], 'cache_form', expire) end end --[[ - Remove invalid characters from an HTML ID attribute string - - @param id - The ID to clean. - @return - The cleaned ID. ]] function ajato_form_clean_id(id) id = id or '' id = id:gsub('][', '-'):gsub('_', '-'):gsub(' ', '-') return id end --[[ - Flag an element as having an error. - - @param element - Table, the element table - @param message - String, the error message ]] function ajato_form_error(element, message) ajato_form_error_set(table.concat(element['#parents'], ']['), (message or '')) end --[[ - Return an associative array of all errors. - - @return - Table, the elements that have error ]] function ajato_form_errors_get() form = ajato_form_error_set() if hv(form) then return form end end --[[ - File an error against a form element. If the name of the element is - edit[foo][bar] then you may pass either foo or foo][bar as name - foo will set an error for all its children. - - @param name - String, The name of the element - @param message - String, the error message ]] function ajato_form_error_set(name, message) -- Static variables local form = {} return function(name, message) if hv(name) and not form[name] then form[name] = message if hv(message) then ajato_add_message(message, 'error') end end return form end end; ajato_form_error_set = ajato_form_error_set() --[[ - A helper function used to execute custom validation and submission - handlers for a given form. Button-specific handlers are checked - first. If none exist, the function falls back to form-level handlers. - - @param handler_type - String, The type of handler to execute. 'validate' or 'submit' are the - defaults used by Form API. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. If the user - submitted the form by clicking a button with custom handler functions - defined, those handlers will be stored here. ]] function ajato_form_execute_handlers(handler_type, form, form_state) local result = false local handlers = {} if form_state[handler_type ..'_handlers'] then handlers = form_state[handler_type ..'_handlers'] elseif form['#'.. handler_type] then handlers = form['#'.. handler_type] else handlers = {} end for _, func in pairs(handlers) do if _G[func] then -- batch = batch_get() if type == 'submit' and batch then -- Some previous _submit handler has set a batch. We store the call -- in a special 'control' batch set, for execution at the correct -- time during the batch processing workflow. batch['sets'][#batch['sets'] + 1] = {['form_submit'] = func} else _G[func](form, form_state) end result = true end end return result end --[[ - Get form data to build the form in HMTL. The form is then passesed - on for processing and rendered for display if necessary. - - @param form_id - String, the form name - @return - HTML, the printed form ]] function ajato_form_get(form_id, ...) local form_state = {['storage'] = nil, ['submitted'] = nil} local form -- local batch_form_state = cgilua.session.load('batch_form_state') if batch_form_state then -- We've been redirected here after a batch processing : the form has -- already been processed, so we grab the post-process form_state value -- and move on to form display. See _batch_finished() function. form_state = cgilua.session.load('batch_form_state') cgilua.session.set('batch_form_state', nil) else -- If the incoming sys['post'] contains a form_build_id, we'll check the -- cache for a copy of the form in question. If it's there, we don't -- have to rebuild the form to proceed. In addition, if there is stored -- form_state data from a previous step, we'll retrieve it so it can -- be passed on to the form processing code. if hv(sys['post']['form_build_id']) and sys['post']['form_id'] == form_id then form, form_state = ajato_form_cache_get(sys['post']['form_build_id'], form_state) end -- We're hitting the form for the first time and we need -- to build it from scratch. if not hv(form) then form_state['post'] = sys['post'] form = _G[form_id]() require('md5') form['#build_id'] = 'form-'.. md5.sumhexa(math.random()) -- Fill all missing values form = ajato_form_prepare(form_id, form) -- Cache the form if form['#cache'] then -- By not sending the form state, we avoid storing the storage which -- won't have been touched yet. ajato_form_cache_set(form_build_id, form, nil) end form_state['post'] = nil end form['#post'] = sys['post'] -- Now that we know we have a form, we'll process it (validating, -- submitting, and handling the results returned by its submission -- handlers. Submit handlers accumulate data in the form_state by -- altering the form_state variable, which is passed into them by -- reference. form = ajato_form_process(form_id, form, form_state) end -- Render the form return ajato_form_render(form_id, form) end --[[ - Prepares a structured form table by adding required elements, - executing any hook_form_alter functions, and optionally inserting - a validation token to prevent tampering. - - @param form_id - String, A unique string identifying the form for validation, - submission, theming, and hook_form_alter functions. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. Passed in here so that - hook_form_alter() calls can use it, as well. ]] function ajato_form_prepare(form_id, form, form_state) -- global user form['#type'] = 'form'; form['#programmed'] = hv(form['#post']) if form['#build_id'] then form['form_build_id'] = { ['#type'] = 'hidden', ['#value'] = form['#build_id'], ['#id'] = form['#build_id'], ['#name'] = 'form_build_id', } end -- Add a token, based on either #token or form_id, to any form displayed to -- authenticated users. This ensures that any submitted form was actually -- requested previously by the user and protects against cross site request -- forgeries. if form['#token'] then if form['#token'] == false or user['uid'] == 0 or form['#programmed'] then form['#token'] = nil else form['form_token'] = { ['#type'] = 'token', ['#default_value'] = ajato_get_token(form['#token']) } end elseif user and user['uid'] and hv(user['uid']) and form['#programmed'] then form['#token'] = form_id form['form_token'] = { ['#id'] = ajato_form_clean_id('edit-'.. form_id ..'-form-token'), ['#type'] = 'token', ['#default_value'] = ajato_get_token(form['#token']) } end if form_id then form['form_id'] = { ['#type'] = 'hidden', ['#value'] = form_id, ['#id'] = ajato_form_clean_id('edit-'.. form_id) } end if not form['#id'] then form['#id'] = ajato_form_clean_id(form_id) end form = ajato_tables_merge(ajato_element_info('form'), form) -- Add the submit and the validate hooks if not form['#validate'] and _G[form_id ..'_validate'] then form['#validate'] = {form_id ..'_validate'} end if not form['#submit'] and _G[form_id ..'_submit'] then form['#submit'] = {form_id ..'_submit'} end ajato_alter('form_'.. form_id, form, form_state) ajato_alter('form', form, form_state, form_id) return form end --[[ - This function is the heart of form API. The form gets built, validated and in - appropriate cases, submitted. - - @param form_id - String, The unique string identifying the current form. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. This - includes the current persistent storage data for the form, and - any data passed along by earlier steps when displaying a - multi-step form. Additional information, like the sanitized sys['post'] - data, is also accumulated here. ]] function ajato_form_process(form_id, form, form_state) form_state['values'] = {} -- Build the form. Its needed when its the first time it -- is displayed and also when submitted and now we need to -- validate and execute the actions form = ajato_form_build(form_id, form, form_state) -- If the form was submited and the form_id matches -- with the current form_id if hv(form['#programmed']) or hv(form['#post']) and form['#post']['form_id'] == form_id then ajato_form_validate(form_id, form, form_state) if hv(form_state['submitted']) and not ajato_form_errors_get() and not hv(form_state['rebuild']) then form_state['redirect'] = nil ajato_form_execute_handlers('submit', form, form_state) -- We'll clear out the cached copies of the form and its stored data -- here, as we've finished with them. The in-memory copies are still -- here, though. if ajato_variable_get('cache', C.CACHE_DISABLED) == C.CACHE_DISABLED and hv(form_state['values']['form_build_id']) then ajato_cache_clear_all('form_'.. form_state['values']['form_build_id'], 'cache_form') ajato_cache_clear_all('storage_'.. form_state['values']['form_build_id'], 'cache_form') end -- If batches were set in the submit handlers, we process them now, -- possibly ending execution. -- local batch = batch_get() if batch then -- The batch uses its own copies of form and form_state for -- late execution of submit handlers and post-batch redirection. batch['form'] = form batch['form_state'] = form_state batch['progressive'] = not form['#programmed'] batch_process() -- Execution continues only for programmatic forms. -- For 'regular' forms, we get redirected to the batch processing -- page. Form redirection will be handled in _batch_finished(), -- after the batch is processed. end -- If no submit handlers have populated the form_state['storage'] -- bundle, and the form_state['rebuild'] flag has not been set, -- we're finished and should redirect to a new destination page -- if one has been set (and a fresh, unpopulated copy of the form -- if one hasn't). If the form was called by ajato_form_execute(), -- however, we'll skip this and let the calling function examine -- the resulting form_state bundle itself. if not form['#programmed'] and not hv(form_state['rebuild']) and not hv(form_state['storage']) then ajato_form_redirect(form, form_state['redirect']) end end end return form end --[[ - Redirect the user to a URL after a form has been processed. - - @param form - Table, The structure of the form. - @param redirect - String (Optional), the destination path to redirect to - if none is specified by the form. ]] function ajato_form_redirect(form, redirect) local goto if redirect == false or hv(redirect) then goto = redirect end if goto ~= false and form['#redirect'] then goto = form['#redirect'] end if goto == false or hv(goto) then if hv(goto) then if type(goto) == 'table' then ajato_goto(unpack(goto)) else ajato_goto(goto) end end ajato_goto(sys['get']['q']); end end --[[ - Render the form table into HTML - - @param form - Table, the form table - @return - HTML, the redered form ]] function ajato_form_render(form_id, form) -- Don't override #theme if someone already set it. if not form['#theme'] and _G['themes'][form_id] then form['#theme'] = form_id end return ajato_element_render(form) end --[[ - Use this function to make changes to form values in the form validate - phase, so they will be available in the submit phase in form_state. - - Specifically, if form['#parents'] is {'foo', 'bar'} - and value is 'baz' then this function will make - form_state['values']['foo']['bar'] to be 'baz'. - - @param form - Table, The form item. Keys used: #parents, #value - @param value - Any type, The value for the form item. - @param form_state - Table, The current state of the form. This - includes the current persistent storage data for the form, and - any data passed along by earlier steps when displaying a - multi-step form. Additional information, like the sanitized sys['post'] - data, is also accumulated here. ]] function ajato_form_set_value(form, form_state, value) local parents = ajato_tables_merge(form['#parents'], {}) _ajato_form_set_value(form_state['values'], parents, value) end --[[ - Helper function for ajato_form_set_value(). - - We iterate over parents and create nested tables for them - in form_state['values'] if needed. Then we insert the value into - the right table. - - @param form - Table, The form item. Keys used: #parents, #value - @param value - Any type, The value for the form item. - @param form_state - Table, The current state of the form. This - includes the current persistent storage data for the form, and - any data passed along by earlier steps when displaying a - multi-step form. Additional information, like the sanitized sys['post'] - data, is also accumulated here. ]] function _ajato_form_set_value(form_values, parents, value) local parent = table.remove(parents, 1) if not hv(parents) then form_values[parent] = value else if not form_values[parent] then form_values[parent] = {} end _ajato_form_set_value(form_values[parent], parents, value) end end --[[ - Helper function to determine the value for a checkbox form element. - - @param form - Table, The form element whose value is being populated. - @param edit - Table, The incoming POST data to populate the form element. If this is FALSE, - the element's default value should be returned. - @return - Table, The data that will appear in the form_state['values'] collection - for this element. Return nothing to use the default. ]] function ajato_form_type_checkbox_value(form, edit) if edit then return ((hv(edit) and form['#return_value']) or 0) end end --[[ - Helper function to determine the value for a checkboxes form element. - - @param form - Table, The form element whose value is being populated. - @param edit - Table, The incoming POST data to populate the form element. If this is FALSE, - the element's default value should be returned. - @return - Table, The data that will appear in the form_state['values'] collection - for this element. Return nothing to use the default. ]] function ajato_form_type_checkboxes_value(form, edit) if not edit then local value = {} form['#default_value'] = form['#default_value'] or {} for _, key in pairs(form['#default_value']) do value[key] = 1 end return value end end --[[ - Helper function to determine the value for a password_confirm form - element. - - @param form - Table, The form element whose value is being populated. - @param edit - Table, The incoming POST data to populate the form element. If this is FALSE, - the element's default value should be returned. - @return - Table, The data that will appear in the form_state['values'] collection - for this element. Return nothing to use the default. - @todo convert this function ]] function ajato_form_type_password_confirm_value(form, edit) if edit == false then form = ajato_tables_merge({['#default_value'] = {}}, form) return ajato_tables_merge(form['#default_value'], { ['pass1'] = '', ['pass2'] = ''}) end end --[[ - Helper function to determine the value for a select form element. - - @param form - The form element whose value is being populated. - @param edit - The incoming POST data to populate the form element. If this is FALSE, - the element's default value should be returned. - @return - The data that will appear in the form_state['values'] collection - for this element. Return nothing to use the default. - @todo convert this function ]] function ajato_form_type_select_value(form, edit) if edit ~= false then if form['#multiple'] and hv(form['#multiple']) then return (type(edit) == 'table' and ajato_map_assoc(edit)) or {} else return edit end end end --[[ - Helper function to determine the value for a textfield form element. - - @param form - Table, The form element whose value is being populated. - @param edit - Table, The incoming POST data to populate the form element. If this is FALSE, - the element's default value should be returned. - @return - Table, The data that will appear in the form_state['values'] collection - for this element. Return nothing to use the default. ]] function ajato_form_type_textfield_value(form, edit) if edit then -- Equate edit to the form value to ensure it's marked for -- validation. return string.gsub(string.gsub(edit, '\n', ''), '\r', '') end end --[[ - Validates user-submitted form data from the form_state using - the validate functions defined in a structured form array. - - @param form_id - Sting, A unique string identifying the form for validation, submission, - theming, and hook_form_alter functions. - @param form - Table, The structure of the form. - @param form_state - Table, The current state of the form. The current - user-submitted data is stored in form_state['values'], though - form validation functions are passed an explicit copy of the - values for the sake of simplicity. Validation handlers can also - form_state to pass information on to submit handlers. For example: - form_state['data_for_submision'] = data - This technique is useful when validation requires file parsing, - web service requests, or other expensive requests that should - not be repeated in the submission step. ]] function ajato_form_validate(form_id, form, form_state) -- Static variables local validated_forms = {} return function(form_id, form, form_state) if validated_forms[form_id] then return end -- If the session token was set by ajato_form_prepare(), ensure that it -- matches the current user's session. if form['#token'] then if ajato_valid_token(form_state['values']['form_token'], form['#token']) then -- Setting this error will cause the form to fail validation. ajato_form_error_set('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.')) end end _ajato_form_validate(form, form_state, form_id) validated_forms[form_id] = true end end; ajato_form_validate = ajato_form_validate() --[[ - Performs validation on form elements. First ensures required fields are - completed, #maxlength is not exceeded, and selected options were in the - list of options given to the user. Then calls user-defined validators. - - @param elements - Table, Structure of the form. - @param form_state - Table, The current state of the form. The current - user-submitted data is stored in form_state['values'], though - form validation functions are passed an explicit copy of the - values for the sake of simplicity. Validation handlers can also - form_state to pass information on to submit handlers. For example: - form_state['data_for_submision'] = data - This technique is useful when validation requires file parsing, - web service requests, or other expensive requests that should - not be repeated in the submission step. - @param form_id - String, A unique string identifying the form for validation, submission, - theming, and hook_form_alter functions. ]] function _ajato_form_validate(elements, form_state, form_id) -- Recurse through all children. for key in ajato_element_pairs(elements) do if hv(elements[key]) then _ajato_form_validate(elements[key], form_state) end end -- Validate the current input if not hv(elements['#validated']) then if elements['#needs_validation'] then -- An empty textfield returns '' and empty checkbox -- and a textfield could return '0' so we use hv() if elements['#required'] and not hv(elements['#value']) then ajato_form_error(elements, t('!name field is required.', {['!name'] = elements['#title']})) end -- Verify that the value is not longer than #maxlength. if elements['#maxlength'] and string.len(elements['#value']) > elements['#maxlength'] then form_error(elements, t('!name cannot be longer than %max characters but is currently %length characters long.', {['!name'] = elements['#title'] or elements['#parents'][0], ['%max'] = elements['#maxlength'], ['%length'] = string.len(elements['#value'])} )); end -- Add legal choice check if element has #options. Can be skipped, but -- then you must validate your own element. if elements['#options'] and elements['#value'] and not elements['#DANGEROUS_SKIP_CHECK'] then local options if elements['#type'] == 'select' then options = form_options_flatten(elements['#options']) else options = elements['#options'] end if type(elements['#value']) == 'table' then local value if elements['#type'] == 'checkboxes' then value = ajato_map_assoc(elements['#value']) for index in pairs(value) do if not hv(value[index]) then value[index] = nil end end else value = elements['#value'] end for v in pairs(value) do if not options[v] then ajato_form_error(elements, t('An illegal choice has been detected. Please contact the site administrator.')) watchdog('form', t('Illegal choice %choice in !name element.', {['%choice'] = v, ['!name'] = elements['#title'] or elements['#parents'][0]}), C.WATCHDOG_ERROR) end end elseif not options[elements['#value']] then ajato_form_error(elements, t('An illegal choice has been detected. Please contact the site administrator.')) watchdog('form', t('Illegal choice %choice in %name element.', {['%choice'] = elements['#value'], ['!name'] = elements['#title'] or elements['#parents'][0]}), C.WATCHDOG_ERROR) end end end -- Call user-defined form level validators. -- or -- Call any element-specific validators. These must act on the element -- #value data. if form_id then ajato_form_execute_handlers('validate', elements, form_state) elseif elements['#element_validate'] then for func in pairs(elements['#element_validate']) do if _G[func] then _G[func](elements, form_state) end end end -- The field passed in all checks elements['#validated'] = true end end --[[ - @} End of "defgroup form". ]] --[[ - Add AHAH information about a form element to the page to communicate with - javascript. If #ahah_path is set on an element, this additional javascript is - added to the page header to attach the AHAH behaviors. See ahah.js for more - information. - - @param element - Table, The properties of the element. - Properties used: ahah_event, ahah_path, ahah_wrapper, ahah_parameters, - ahah_effect. - @return - Table, no modifications from the param ]] function expand_ahah(element) local js_added = {} return function(element) -- Adding the same javascript settings twice will cause a recursion error, -- we avoid the problem by checking if the javascript has already been added. if not js_added[element['#id']] and element['#ahah_event'] and element['#ahah_path'] then ajato_add_js('misc/ahah.js') ajato_add_js('misc/progress.js') local ahah_binding = { ['id'] = element['#id'], ['uri'] = ajato_path_format(element['#ahah_path']), ['event'] = element['#ahah_event'], ['effect'] = element['#ahah_effect'] or 'none', ['method'] = element['#ahah_method'] or 'replace' } if hv(element['#ahah_wrapper']) then ahah_binding['wrapper'] = element['#ahah_wrapper'] end -- ajato_add_js({['ahah'] = {[element['#id']] = ahah_binding}}, 'setting') js_added[element['#id']] = true end return element end end; expand_ahah = expand_ahah() --[[ - Expand all checkboxes options - - @param element - Table, The properties of the element - ahah_effect. - @return - Table, the checkboxes options ]] function expand_checkboxes(element) local value = element['#value'] or {} element['#tree'] = true if #element['#options'] then if not element['#default_value'] or element['#default_value'] == 0 then element['#default_value'] = {} end for key, choice in pairs(element['#options']) do if not element[key] then element[key] = { ['#type'] = 'checkbox', ['#processed'] = true, ['#title'] = choice, ['#return_value'] = key, ['#attributes'] = ajato_tables_merge({}, element['#attributes']) } if value[key] then element[key]['#default_value'] = value[key] end end end end return element end --[[ - Format the HTML a button. - - @param element - Table, the properties of the element. - Properties used: title, value, return_value, description, required - @return - HTML, the button. - @ingroup theme ]] function themes.button(element) local attr = element['#attributes'] or {} attr['type'] = 'submit' attr['name'] = element['#name'] attr['id'] = element['#id'] attr['value'] = element['#value'] attr['class'] = ajato_tables_merge(attr['class'], {'form-button'}, true) return '\n" end --[[ - Format a checkbox. - - @param element - Table, the properties of the element. - Properties used: title, value, return_value, description, required - @return - HTML, the checkbox. - @ingroup theme ]] function themes.checkbox(element) local attr = element['#attributes'] or {} attr['type'] = 'checkbox' attr['name'] = element['#name'] attr['id'] = element['#id'] attr['value'] = element['#return_value'] if hv(element['#value']) then attr['checked'] = tostring(element['#value']) end attr['class'] = attr['class'] or {} attr['class'][#attr['class'] + 1] = 'form-checkbox' output = '' if element['#title'] then output = '' end element['#title'] = nil return theme('form_element', element, output) end --[[ - Format a set of checkboxes. - - @param element - Table, the properties of the element. - @return - HTML, the checkbox. set. - @ingroup theme ]] function themes.checkboxes(element) local attr = element['#attributes'] or {} attr['class'] = attr['class'] or {} attr['class'][#attr['class'] + 1] = 'form-checkbox' element['#children'] = '
'.. ajato_choose(element['#children'], element['#children'], '') ..'
' if (element['#title'] or element['#description']) then element['#id'] = nil return theme('form_element', element, element['#children']) else return element['#children'] end end --[[ - Format a group of form items. - - @param element - Table, The properties of the element. - Properties used: attributes, title, value, description, children, collapsible, collapsed - @return - HTML, The form item group. ]] function themes.fieldset(element) if element['#collapsible'] then ajato_add_js('misc/collapse.js') if not element['#attributes'] then element['#attributes'] = {} end if not element['#attributes']['class'] then element['#attributes']['class'] = '' end element['#attributes']['class'] = element['#attributes']['class'] ..' collapsible'; if element['#collapsed'] then element['#attributes']['class'] = element['#attributes']['class'] ..' collapsed' end end return '
'.. ((element['#title'] and ''.. element['#title'] ..'') or '') .. (((element['#description'] and element['#description']) and '
'.. element['#description'] ..'
') or '') .. (element['#children'] or '') .. (element['#value'] or '') ..'
\n' end --[[ - Format a form. - - @param element - Table, The properties of the element. - Properties used: action, method, attributes, children - @return - HMTL, the form - @ingroup theme ]] function themes.form(element) local attr = element['#attributes'] or {} attr['id'] = element['#id'] attr['action'] = element['#action'] or '' attr['method'] = element['#method'] local content = element['#children'] or '' return '
\n'.. content ..'
\n' end --[[ - All form elements share some basic appearance. - - @param element - Table, properties of the element. - Properties used: title, description, id, required - @param value - Table, the form element's data. - @return - HTML, the form element. - @ingroup theme ]] function themes.form_element(element, value) local output = ajato_string('
' -- Add the field title if element['#title'] then title = element['#title'] if element['#id'] then output = output .. '
\n' return tostring(output) end --[[ - Format a form item. - - @param element - Table, the properties of the element. - Properties used: title, value, description, required, error - @return - HTML, then form item - @ingroup theme ]] function themes.item(element) return theme('form_element', element, element['#value'] .. ajato_choose(element['#children'], element['#children'], '')) end --[[ - Format HTML hidden value for use in forms. - - @param element - Table, The properties of the element. - Properties used: value, children. - @return - A themed HTML string representing the HTML hidden value. - @ingroup theme ]] function themes.hidden(element) local attr = element['#attributes'] or {} attr['type'] = 'hidden' attr['name'] = element['#name'] attr['id'] = element['#id'] attr['value'] = element['#value'] return '\n' end --[[ - Format HTML markup for use in forms. - This is used in more advanced forms, such as theme selection and filter format. - - @param element - Table, The properties of the element. - Properties used: value, children. - @return - A themed HTML string representing the HTML markup. - @ingroup theme ]] function themes.markup(element) return (element['#value'] or '') .. (element['#children'] or '') end --[[ - Format a password field. - - @param element - Table, The properties of the element. - Properties used: title, value, description, size, maxlength, required, attributes - @return - A themed HTML string representing the form. - @ingroup theme ]] function themes.password(element) local attr = element['#attributes'] or {} local class = element['#attributes']['class'] or {} attr['type'] = 'password' attr['name'] = element['#name'] attr['id'] = element['#id'] if element['#size'] then attr['size'] = element['#size'] end if element['#maxlength'] then attr['maxlength'] = element['#maxlength'] end attr['class'] = ajato_tables_merge(class, {'form-text'}, true) local output = '' return theme('form_element', element, output) end --[[ - Format a textfield. - - @param element - Table, The properties of the element. - Properties used: title, value, description, size, maxlength, required, attributes autocomplete_path - @return - A themed HTML string representing the textfield. - @ingroup theme ]] function themes.textfield(element) local output = ajato_string() local attr = element['#attributes'] or {} attr['class'] = ajato_tables_merge(attr['class'], {' form-checkbox'}, true) -- Add the special class autocomplete if (element['#autocomplete_path']) then ajato_add_js('misc/autocomplete.js') attr['class'][#attr['class'] + 1] = ' form-autocomplete' output = output .. '' end if element['#field_prefix'] then output = output .. ''.. element['#field_prefix'] ..' ' attr['maxlength'] = element['#maxlength'] end -- The input tag itself attr['class'][#attr['class'] + 1] = ' form-text' attr['type'] = 'text' attr['name'] = element['#name'] attr['id'] = element['#id'] attr['value'] = element['#value'] attr['size'] = element['#size'] output = output .. '' if element['#field_suffix'] then output = output .. ''.. element['#field_suffix'] ..'' end return theme('form_element', element, tostring(output)) end --[[ - Format a textarea. - - @param element - Table, The properties of the element. - Properties used: title, value, description, rows, cols, required, attributes - @return - A themed HTML string representing the textarea. - @ingroup theme ]] function themes.textarea(element) local attr = element['#attributes'] or {} attr['type'] = 'password' attr['name'] = element['#name'] attr['id'] = element['#id'] attr['cols'] = element['#cols'] attr['rows'] = element['#rows'] attr['class'] = ajato_tables_merge(attr['class'], {'form-textarea'}, true) -- Add teaser behaviour (must come before resizable) if element['#teaser'] then ajato_add_js('misc/teaser.js') -- ajato_add_js({['teaserCheckbox'] = {element['#id'] = element['#teaser_checkbox']}}, 'setting') -- ajato_add_js({['teaser'] = {element['#id'] = element['#teaser']}}, 'setting') attr['class'][#attr['class'] + 1] = 'teaser' end -- Add resizable behaviour if element['#resizable'] then ajato_add_js('misc/textarea.js') attr['class'][#attr['class'] + 1] = 'resizable' end return theme('form_element', element, '') end --[[ - Format the HTML a token value (hidden value). - - @param element - Table, The properties of the element. Properties used: title, - value, description, rows, cols, required, attributes - @return - HTML, the submit button. - @ingroup theme ]] function themes.token(element) return theme('hidden', element) end --[[ - Format the HTML a submit button. - - @param element - Table, the properties of the element. - Properties used: title, value, return_value, description, required - @return - HTML, the submit button. - @ingroup theme ]] function themes.submit(element) return theme('button', element) end