/*
* Copyright 2007-2009 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
*/
package net.liftweb.http;
import S._
import _root_.net.liftweb.util._
import _root_.net.liftweb.util.Helpers._
import _root_.net.liftweb.http.js._
import _root_.net.liftweb.http.js.AjaxInfo
import JE._
import JsCmds._
import _root_.scala.xml._
object SHtml {
/**
* Create an Ajax button. When it's pressed, the function is executed
*
* @param text -- the name/text of the button
* @param func -- the function to execute when the button is pushed. Return Noop if nothing changes on the browser.
*
* @return a button to put on your page
*/
def ajaxButton(text: NodeSeq, func: () => JsCmd, attrs: (String, String)*): Elem = {
attrs.foldLeft(fmapFunc(func)(name =>
<button onclick={makeAjaxCall(Str(name+"=true")).toJsCmd+
"; return false;"}>{text}</button>))(_ % _)
}
/**
* Create an Ajax button. When it's pressed, the function is executed
*
* @param text -- the name/text of the button
* @param func -- the function to execute when the button is pushed. Return Noop if nothing changes on the browser.
*
* @return a button to put on your page
*/
def ajaxButton(text: String, func: () => JsCmd, attrs: (String, String)*): Elem =
ajaxButton(Text(text), func, attrs :_*)
/**
* create an anchor tag around a body which will do an AJAX call and invoke the function
*
* @param func - the function to invoke when the link is clicked
* @param body - the NodeSeq to wrap in the anchor tag
*/
def a(func: () => JsCmd, body: NodeSeq, attrs: (String, String)*): Elem = {
val key = Helpers.nextFuncName
addFunctionMap(key, (a: List[String]) => func())
attrs.foldLeft(<lift:a key={key}>{body}</lift:a>)(_ % _)
}
def makeAjaxCall(in: JsExp): JsExp = new JsExp {
def toJsCmd = "lift_ajaxHandler("+ in.toJsCmd+", null, null)"
}
/**
* Create an anchor with a body and the function to be executed when the anchor is clicked
*/
def a(body: NodeSeq, attrs: (String, String)*)(func: => JsCmd): Elem =
a(() => func, body, attrs :_*)
/**
* Create an anchor that will run a JavaScript command when clicked
*/
def a(body: NodeSeq, cmd: JsCmd, attrs: (String, String)*): Elem =
attrs.foldLeft(<a href="javascript://"
onclick={cmd.toJsCmd + "; return false;"}>{body}</a>)(_ % _)
/**
* Create a span that will run a JavaScript command when clicked
*/
def span(body: NodeSeq, cmd: JsCmd, attrs: (String, String)*): Elem =
attrs.foldLeft(<span onclick={cmd.toJsCmd}>{body}</span>)(_ % _)
/**
* Build a JavaScript function that will perform an AJAX call based on a value calculated in JavaScript
* @param jsCalcValue -- the JavaScript to calculate the value to be sent to the server
* @param func -- the function to call when the data is sent
*
* @return the JavaScript that makes the call
*/
def ajaxCall(jsCalcValue: JsExp, func: String => JsCmd): (String, JsExp) = ajaxCall_*(jsCalcValue, SFuncHolder(func))
def fajaxCall[T](jsCalcValue: JsExp, func: String => JsCmd)(f: (String, JsExp) => T): T =
{
val (name, js) = ajaxCall(jsCalcValue, func)
f(name, js)
}
/**
* Build a JavaScript function that will perform an AJAX call based on a value calculated in JavaScript
* @param jsCalcValue -- the JavaScript to calculate the value to be sent to the server
* @param func -- the function to call when the data is sent
*
* @return the JavaScript that makes the call
*/
private def ajaxCall_*(jsCalcValue: JsExp, func: AFuncHolder): (String, JsExp) =
fmapFunc(func)(name =>
(name, makeAjaxCall(JsRaw("'"+name+"=' + "+jsCalcValue.toJsCmd))))
def toggleKids(head: Elem, visible: Boolean, func: () => Any, kids: Elem): NodeSeq = {
fmapFunc(func){
funcName =>
val (nk, id) = findOrAddId(kids)
val rnk = if (visible) nk else nk % ("style" -> "display: none")
val nh = head %
("onclick" -> (LiftRules.jsArtifacts.toggle(id).cmd & makeAjaxCall(JsRaw("'"+funcName+"=true'")).cmd))
nh ++ rnk
}
}
/**
* This function does not really submit a JSON request to server instead json is a function
* that allows you to build a more complex JsCmd based on the JsExp <i>JE.JsRaw("this.value")</i>.
* This function is called by the overloaded version of jsonText.
*
* @param value - the initial value of the text field
* @param json - takes a JsExp which describes how to recover the
* value of the text field and returns a JsExp containing the thing
* to execute on blur/return
*
* @return a text field
*/
def jsonText(value: String, json: JsExp => JsCmd, attrs: (String, String)*): Elem = {
(attrs.foldLeft(<input type="text" value={value}/>)(_ % _)) %
("onkeypress" -> """lift_blurIfReturn(event)""") %
("onblur" -> (json(JE.JsRaw("this.value"))))
}
/**
* Create a JSON text widget that makes a JSON call on blur or "return".
*
* @param value - the initial value of the text field
* @param cmd - the json command name
* @param json - the JsonCall returned from S.buildJsonFunc
*
* @return a text field
*/
def jsonText(value: String, cmd: String, json: JsonCall, attrs: (String, String)*): Elem =
jsonText(value, exp => json(cmd, exp), attrs :_*)
def ajaxText(value: String, func: String => JsCmd): Elem = ajaxText_*(value, SFuncHolder(func))
private def ajaxText_*(value: String, func: AFuncHolder, attrs: (String, String)*): Elem = {
fmapFunc(func){
funcName =>
(attrs.foldLeft(<input type="text" value={value}/>)(_ % _)) %
("onkeypress" -> """lift_blurIfReturn(event)""") %
("onblur" -> makeAjaxCall(JsRaw("'" +funcName + "=' + encodeURIComponent(this.value)")))
}
}
def ajaxCheckbox(value: Boolean, func: Boolean => JsCmd, attrs: (String, String)*): Elem =
ajaxCheckbox_*(value, LFuncHolder(in => func(in.exists(toBoolean(_)))), attrs :_*)
private def ajaxCheckbox_*(value: Boolean, func: AFuncHolder, attrs: (String, String)*): Elem = {
fmapFunc(func) {
funcName =>
(attrs.foldLeft(<input type="checkbox"/>)(_ % _)) %
checked(value) %
("onclick" -> makeAjaxCall(JsRaw("'" + funcName+"='+this.checked")))
}
}
def ajaxSelect(opts: Seq[(String, String)], deflt: Box[String],
func: String => JsCmd, attrs: (String, String)*): Elem =
ajaxSelect_*(opts, deflt, SFuncHolder(func), attrs :_*)
private def ajaxSelect_*(opts: Seq[(String, String)],deflt: Box[String],
func: AFuncHolder, attrs: (String, String)*): Elem = {
val vals = opts.map(_._1)
val testFunc = LFuncHolder(in => in.filter(v => vals.contains(v)) match {case Nil => false case xs => func(xs)}, func.owner)
fmapFunc(testFunc) {
funcName =>
(attrs.foldLeft(<select>{
opts.flatMap{case (value, text) => (<option value={value}>{text}</option>) % selected(deflt.exists(_ == value))}
}</select>)(_ % _)) %
("onchange" -> makeAjaxCall(JsRaw("'" + funcName+"='+this.options[this.selectedIndex].value")))
}
}
def ajaxInvoke(func: () => JsCmd): (String, JsExp) =
fmapFunc(NFuncHolder(func))(name => (name, makeAjaxCall(Str(name) + "=true")))
/**
* Build a swappable visual element. If the shown element is clicked on, it turns into the hidden element and when
* the hidden element blurs, it swaps into the shown element.
*/
def swappable(shown: Elem, hidden: Elem): Elem = {
val (rs, sid) = findOrAddId(shown)
val (rh, hid) = findOrAddId(hidden)
val ui = LiftRules.jsArtifacts
(<span>{rs % ("onclick" -> (ui.hide(sid).cmd &
ui.showAndFocus(hid).cmd & JsRaw("return false;")))}
{dealWithBlur(rh % ("style" -> "display: none"), (ui.show(sid).cmd & ui.hide(hid).cmd))}
</span>)
}
def swappable(shown: Elem, hidden: String => Elem): Elem = {
val (rs, sid) = findOrAddId(shown)
val hid = Helpers.nextFuncName
val ui = LiftRules.jsArtifacts
val rh = <span id={hid}>{hidden(ui.show(sid).toJsCmd + ";" + ui.hide(hid).toJsCmd + ";")}</span>
(<span>{rs % ("onclick" -> (ui.hide(sid).toJsCmd + ";" + ui.show(hid).toJsCmd + "; return false;"))}{
(rh % ("style" -> "display: none"))}</span>)
}
private def dealWithBlur(elem: Elem, blurCmd: String): Elem = {
(elem \ "@onblur").toList match {
case Nil => elem % ("onblur" -> blurCmd)
case x :: xs => val attrs = elem.attributes.filter(_.key != "onblur")
Elem(elem.prefix, elem.label, new UnprefixedAttribute("onblur", Text(blurCmd + x.text), attrs), elem.scope, elem.child :_*)
}
}
/**
* create an anchor tag around a body
*
* @param func - the function to invoke when the link is clicked
* @param body - the NodeSeq to wrap in the anchor tag
*/
def link(to: String, func: () => Any, body: NodeSeq,
attrs: (String, String)*): Elem = {
fmapFunc((a: List[String]) => {func(); true})(key =>
attrs.foldLeft(<a href={to+"?"+key+"=_"}>{body}</a>)(_ % _))
}
private def makeFormElement(name: String, func: AFuncHolder,
attrs: (String, String)*): Elem =
fmapFunc(func)(funcName =>
attrs.foldLeft(<input type={name} name={funcName}/>)(_ % _))
def text_*(value: String, func: AFuncHolder, attrs: (String, String)*): Elem =
makeFormElement("text", func, attrs :_*) % new UnprefixedAttribute("value", Text(value), Null)
def password_*(value: String, func: AFuncHolder, attrs: (String, String)*): Elem =
makeFormElement("password", func, attrs :_*) % ("value" -> value)
def hidden_*(func: AFuncHolder, attrs: (String, String)*): Elem =
makeFormElement("hidden", func, attrs :_*) % ("value" -> "true")
def submit_*(value: String, func: AFuncHolder, attrs: (String, String)*): Elem =
makeFormElement("submit", func, attrs :_*) % ("value" -> value)
def text(value: String, func: String => Any, attrs: (String, String)*): Elem =
makeFormElement("text", SFuncHolder(func), attrs :_*) % new UnprefixedAttribute("value", Text(value), Null)
def password(value: String, func: String => Any, attrs: (String, String)*): Elem =
makeFormElement("password", SFuncHolder(func), attrs :_*) % new UnprefixedAttribute("value", Text(value), Null)
def hidden(func: () => Any, attrs: (String, String)*): Elem =
makeFormElement("hidden", NFuncHolder(func), attrs :_*) % ("value" -> "true")
def submit(value: String, func: () => Any, attrs: (String, String)*): Elem =
makeFormElement("submit", NFuncHolder(func), attrs :_*) %
new UnprefixedAttribute("value", Text(value), Null)
def ajaxForm(body: NodeSeq) = (<lift:form>{body}</lift:form>)
def ajaxForm(onSubmit: JsCmd, body: NodeSeq) = (<lift:form onsubmit={onSubmit.toJsCmd}>{body}</lift:form>)
def ajaxForm(body: NodeSeq, onSubmit: JsCmd) = (<lift:form onsubmit={onSubmit.toJsCmd}>{body}</lift:form>)
def jsonForm(jsonHandler: JsonHandler, body: NodeSeq): NodeSeq = jsonForm(jsonHandler, Noop, body)
def jsonForm(jsonHandler: JsonHandler, onSubmit: JsCmd, body: NodeSeq): NodeSeq = {
val id = Helpers.nextFuncName
<form onsubmit={(onSubmit & jsonHandler.call("processForm", FormToJSON(id)) & JsReturn(false)).toJsCmd} id={id}>
{body}
</form>
}
private[http] def secureOptions[T](options: Seq[(T, String)], default: Box[T],
onSubmit: T => Unit) = {
val secure = options.map{case (obj, txt) => (obj, randomString(20), txt)}
val defaultNonce = default.flatMap(d => secure.find(_._1 == d).map(_._2))
val nonces = secure.map{case (obj, nonce, txt) => (nonce, txt)}
def process(nonce: String): Unit =
secure.find(_._2 == nonce).map(x => onSubmit(x._1))
(nonces, defaultNonce, SFuncHolder(process))
}
/**
* Create a select box based on the list with a default value and the function to be executed on
* form submission
*
* @param opts -- the options. A list of value and text pairs (value, text to display)
* @param deflt -- the default value (or Empty if no default value)
* @param func -- the function to execute on form submission
*/
def select(opts: Seq[(String, String)], deflt: Box[String], func: String => Any, attrs: (String, String)*): Elem =
select_*(opts, deflt, SFuncHolder(func), attrs :_*)
/**
* Create a select box based on the list with a default value and the function
* to be executed on form submission
*
* @param options -- a list of value and text pairs (value, text to display)
* @param default -- the default value (or Empty if no default value)
* @param onSubmit -- the function to execute on form submission
*/
def selectObj[T](options: Seq[(T, String)], default: Box[T],
onSubmit: T => Unit, attrs: (String, String)*): Elem = {
val (nonces, defaultNonce, secureOnSubmit) =
secureOptions(options, default, onSubmit)
select_*(nonces, defaultNonce, secureOnSubmit, attrs:_*)
}
/**
* Create a select box based on the list with a default value and the function to be executed on
* form submission
*
* @param opts -- the options. A list of value and text pairs
* @param deflt -- the default value (or Empty if no default value)
* @param func -- the function to execute on form submission
*/
def select_*(opts: Seq[(String, String)],deflt: Box[String],
func: AFuncHolder, attrs: (String, String)*): Elem = {
val vals = opts.map(_._1)
val testFunc = LFuncHolder(in => in.filter(v => vals.contains(v)) match {case Nil => false case xs => func(xs)}, func.owner)
attrs.foldLeft(fmapFunc(testFunc)(fn => <select name={fn}>{
opts.flatMap{case (value, text) => (<option value={value}>{text}</option>) % selected(deflt.exists(_ == value))}
}</select>))(_ % _)
}
/**
* Create a select box based on the list with a default value and the function to be executed on
* form submission. No check is made to see if the resulting value was in the original list.
* For use with DHTML form updating.
*
* @param opts -- the options. A list of value and text pairs
* @param deflt -- the default value (or Empty if no default value)
* @param func -- the function to execute on form submission
*/
def untrustedSelect(opts: Seq[(String, String)], deflt: Box[String],
func: String => Any, attrs: (String, String)*): Elem =
untrustedSelect_*(opts, deflt, SFuncHolder(func))
/**
* Create a select box based on the list with a default value and the function to be executed on
* form submission. No check is made to see if the resulting value was in the original list.
* For use with DHTML form updating.
*
* @param opts -- the options. A list of value and text pairs
* @param deflt -- the default value (or Empty if no default value)
* @param func -- the function to execute on form submission
*/
def untrustedSelect_*(opts: Seq[(String, String)],deflt: Box[String],
func: AFuncHolder, attrs: (String, String)*): Elem =
fmapFunc(func)(funcName =>
attrs.foldLeft(<select name={funcName}>{
opts.flatMap{case (value, text) => (<option value={value}>{text}</option>) % selected(deflt.exists(_ == value))}
}</select>)(_ % _))
private def selected(in: Boolean) = if (in) new UnprefixedAttribute("selected", "selected", Null) else Null
def multiSelect(opts: Seq[(String, String)], deflt: Seq[String],
func: String => Any, attrs: (String, String)*): Elem =
multiSelect_*(opts, deflt, SFuncHolder(func), attrs :_*)
def multiSelect_*(opts: Seq[(String, String)],
deflt: Seq[String],
func: AFuncHolder, attrs: (String, String)*): Elem =
fmapFunc(func)(funcName =>
attrs.foldLeft(<select multiple="true" name={funcName}>{
opts.flatMap(o => (<option value={o._1}>{o._2}</option>) % selected(deflt.contains(o._1)))
}</select>)(_ % _))
def textarea(value: String, func: String => Any, attrs: (String, String)*): Elem =
textarea_*(value, SFuncHolder(func), attrs :_*)
def textarea_*(value: String, func: AFuncHolder, attrs: (String, String)*): Elem =
fmapFunc(func)(funcName =>
attrs.foldLeft(<textarea name={funcName}>{value}</textarea>)(_ % _))
def radio(opts: Seq[String], deflt: Box[String], func: String => Any,
attrs: (String, String)*): ChoiceHolder[String] =
radio_*(opts, deflt, SFuncHolder(func), attrs :_*)
def radio_*(opts: Seq[String], deflt: Box[String],
func: AFuncHolder, attrs: (String, String)*): ChoiceHolder[String] = {
fmapFunc(func){name =>
val itemList = opts.map(v => ChoiceItem(v,
attrs.foldLeft(<input type="radio" name={name} value={v}/>)(_ % _) %
checked(deflt.filter((s: String) => s == v).isDefined)))
ChoiceHolder(itemList)
}
}
def fileUpload(func: FileParamHolder => Any): Elem =
fmapFunc(BinFuncHolder(func))(name => <input type="file" name={name} />)
case class ChoiceItem[T](key: T, xhtml: NodeSeq)
case class ChoiceHolder[T](items: Seq[ChoiceItem[T]]) {
def apply(in: T) = items.filter(_.key == in).first.xhtml
def apply(in: Int) = items(in).xhtml
def map[A](f: ChoiceItem[T] => A) = items.map(f)
def flatMap[A](f: ChoiceItem[T] => Iterable[A]) = items.flatMap(f)
def filter(f: ChoiceItem[T] => Boolean) = items.filter(f)
def toForm: NodeSeq = flatMap(c => (<span>{c.xhtml} {c.key.toString}<br /></span>))
}
private def checked(in: Boolean) = if (in) new UnprefixedAttribute("checked", "checked", Null) else Null
private def setId(in: Box[String]) = in match { case Full(id) => new UnprefixedAttribute("id", Text(id), Null); case _ => Null}
def checkbox[T](possible: Seq[T], actual: Seq[T], func: Seq[T] => Any, attrs: (String, String)*): ChoiceHolder[T] = {
val len = possible.length
fmapFunc(LFuncHolder( (strl: List[String]) => {func(strl.map(toInt(_)).filter(x =>x >= 0 && x < len).map(possible(_))); true})){
name =>
ChoiceHolder(possible.toList.zipWithIndex.map(p =>
ChoiceItem(p._1,
attrs.foldLeft(<input type="checkbox" name={name} value={p._2.toString}/>)(_ % _) %
checked(actual.contains(p._1)) ++ (if (p._2 == 0) (<input type="hidden" name={name} value="-1"/>) else Nil))))
}
}
/**
* Defines a new checkbox set to {@code value} and running {@code func} when the
* checkbox is submitted.
*/
def checkbox(value: Boolean, func: Boolean => Any, attrs: (String, String)*): NodeSeq = {
checkbox_id(value, func, Empty, attrs :_*)
}
/**
* Defines a new checkbox set to {@code value} and running {@code func} when the
* checkbox is submitted. Has an id of {@code id}.
*/
def checkbox_id(value: Boolean, func: Boolean => Any,
id: Box[String], attrs: (String, String)*): NodeSeq = {
def from(f: Boolean => Any): List[String] => Boolean = (in: List[String]) => {
f(in.exists(toBoolean(_)))
true
}
checkbox_*(value, LFuncHolder(from(func)), id, attrs :_*)
}
def checkbox_*(value: Boolean, func: AFuncHolder, id: Box[String],
attrs: (String, String)*): NodeSeq = {
fmapFunc(func)(name =>
(<input type="hidden" name={name} value="false"/>) ++
(attrs.foldLeft(<input type="checkbox" name={name} value="true" />)(_ % _) % checked(value) % setId(id))
)
}
}