All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.liftweb.http.SHtml.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2007-2011 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
package http

import S._
import common._
import util._
import util.Helpers._
import http.js._
import http.js.AjaxInfo
import JE._
import JsCmds._
import scala.xml._
import json._

class SHtmlJBridge {
  def sHtml = SHtml
}

/**
 * The SHtml object defines a suite of XHTML element generator methods
 * to simplify the creation of markup, particularly with forms and AJAX.
 */
object SHtml extends SHtml

/**
 * The SHtml object defines a suite of XHTML element generator methods
 * to simplify the creation of markup, particularly with forms and AJAX.
 */
trait SHtml extends Loggable {

  /**
   * Convert a T to a String for display in Select, MultiSelect,
   * etc.
   */
  trait PairStringPromoter[T] extends Function1[T, String]

  /**
   * A companion object that does implicit conversions
   */
  object PairStringPromoter {
    implicit val strPromot: PairStringPromoter[String] =
      new PairStringPromoter[String]{ def apply(in: String): String = in}

    implicit val intPromot: PairStringPromoter[Int] =
      new PairStringPromoter[Int]{ def apply(in: Int): String = in.toString}

    implicit def funcPromote[T](f: T => String): PairStringPromoter[T] =
      new PairStringPromoter[T]{def apply(in: T): String = f(in)}


    type EnumerationTypeWorkaround = Enumeration#Value

    implicit def enumToStrValPromo[EnumerationTypeWorkaround]: SHtml.PairStringPromoter[EnumerationTypeWorkaround] =
      new SHtml.PairStringPromoter[EnumerationTypeWorkaround]{
        def apply(in: EnumerationTypeWorkaround): String =
          in.toString
      }
  }

  /**
   * An attribute that can be applied to an element.  Typically,
   * this will be a key-value pair, but there is a class of HTML5
   * attributes that should be similated in JavaScript.
   */
  trait ElemAttr extends Function1[Elem, Elem] {
    /**
     * Apply the attribute to the element
     */
    def apply(in: Elem): Elem
  }

  /**
   * The companion object that has some very helpful conversion
   */
  object ElemAttr {
    implicit def pairToBasic(in: (String, String)): ElemAttr =
      new BasicElemAttr(in._1, in._2)

    implicit def funcToElemAttr(f: Elem => Elem): ElemAttr =
      new ElemAttr{def apply(in: Elem): Elem = f(in)}

    implicit def strSeqToElemAttr(in: Seq[(String, String)]):
    Seq[ElemAttr] = in.map(a => a: ElemAttr)

    def applyToAllElems(in: Seq[Node], elemAttrs: Seq[ElemAttr]): Seq[Node] = in map {
      case Group(ns) => Group(applyToAllElems(ns, elemAttrs))
      case e: Elem =>
        val updated = elemAttrs.foldLeft(e)((e, f) => f(e))
        updated.copy(child = applyToAllElems(updated.child, elemAttrs))
      case n => n
    }
  }

  class ApplicableElem(in: Elem) {
    def %(attr: ElemAttr): Elem = attr.apply(in)
  }

  implicit def elemToApplicable(e: Elem): ApplicableElem =
    new ApplicableElem(e)

  /**
   * Any old attribute.  You should not explicitly construct one of these,
   * but rather use "name" -> "value" and let the implicit conversion
   * take care of making a BasicElemAttr.
   */
  final case class BasicElemAttr(name: String, value: String) extends ElemAttr {
    /**
     * Apply the attribute to the element
     */
    def apply(in: Elem): Elem = in % (name -> value)
  }

  /**
   * Invokes the Ajax request
   * @param in the JsExp that returns the request data
   */
  def makeAjaxCall(in: JsExp): JsExp = new JsExp {
    def toJsCmd = "lift.ajax(" + in.toJsCmd + ", null, null, null)"
  }

  /**
   * Invokes the Ajax request
   * @param in the JsExp that returns the request data
   * @param context defines the response callback functions and the response type (JavaScript or JSON)
   */
  def makeAjaxCall(in: JsExp, context: AjaxContext): JsExp = new JsExp {
    def toJsCmd = "lift.ajax(" + in.toJsCmd + ", " + (context.success openOr "null") +
            ", " + (context.failure openOr "null") +
            ", " + context.responseType.toString.encJs +
            ")"
  }

  /**
   * Build a JavaScript function that will perform an AJAX call based on a value calculated in JavaScript
   *
   * @param jsCalcValue the JavaScript that will be executed on the client to calculate the value to be sent to the server
   * @param func the function to call when the data is sent
   *
   * @return the function ID and JavaScript that makes the call
   */
  def ajaxCall(jsCalcValue: JsExp, func: String => JsCmd): GUIDJsExp = ajaxCall_*(jsCalcValue, SFuncHolder(func))

  /**
   * Build a JavaScript function that will perform an AJAX call based on a value calculated in JavaScript
   *
   * @param jsCalcValue the JavaScript that will be executed on the client to calculate the value to be sent to the server
   * @param jsContext the context instance that defines JavaScript to be executed on call success or failure
   * @param func the function to call when the data is sent
   *
   * @return the function ID and JavaScript that makes the call
   */
  def ajaxCall(jsCalcValue: JsExp, jsContext: JsContext, func: String => JsCmd): GUIDJsExp =
    ajaxCall_*(jsCalcValue, jsContext, SFuncHolder(func))

  /**
   * A wrapper around parseOpt for use in the jsonCall variants that will notify the developer on the console when
   * parseOpt is unsuccessful at parsing the JSON.
  **/
  private def parseOptOrLog(rawJsonText: String) = {
    parseOpt(rawJsonText) match {
      case None =>
        logger.error(s"Browser sent back something that wasn't JSON to a jsonCall function: $rawJsonText")
        None

      case anythingElse =>
        anythingElse
    }
  }

  /**
   * Build a JavaScript function that will perform a JSON call based on a value calculated in JavaScript.
   * This method uses the Lift-JSON package rather than the old, slow, not-typed JSONParser.  This is the preferred
   * way to do client to server JSON calls.
   *
   * @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 function ID and JavaScript that makes the call
   */
  def jsonCall(jsCalcValue: JsExp, func: JsonAST.JValue => JsCmd): GUIDJsExp =
    jsonCall_*(jsCalcValue, SFuncHolder(s => parseOptOrLog(s).map(func) getOrElse Noop))

  /**
   * Build a JavaScript function that will perform a JSON call based on a value calculated in JavaScript.
   * This method uses the Lift-JSON package rather than the old, slow, not-typed JSONParser.  This is the preferred
   * way to do client to server JSON calls.
   *
   * @param jsCalcValue the JavaScript to calculate the value to be sent to the server
   * @param jsContext the context instance that defines JavaScript to be executed on call success or failure
   * @param func the function to call when the data is sent
   *
   * @return the function ID and JavaScript that makes the call
   */
  def jsonCall(jsCalcValue: JsExp, jsContext: JsContext, func: JsonAST.JValue => JsCmd): GUIDJsExp =
    jsonCall_*(jsCalcValue, jsContext, SFuncHolder(s => parseOptOrLog(s).map(func) getOrElse Noop))


  /**
   * Build a JavaScript function that will perform a JSON call based on a value calculated in JavaScript.
   *
   * The JSON generated by func will be returned to the client and passed as argument to the javascript function specified in
   * jsonContext.success
   *
   * @param jsCalcValue the JavaScript to calculate the value to be sent to the server
   * @param jsonContext the context instance that defines JavaScript to be executed on call success or failure
   * @param func the function to call when the JSON data is sent. The returned JSON is sent back to the client
   *
   * @return the function ID and JavaScript that makes the call
   */
  def jsonCall(jsCalcValue: JsExp, jsonContext: JsonContext, func: JsonAST.JValue => JsonAST.JValue): GUIDJsExp =
    jsonCall_*(jsCalcValue, jsonContext, S.SFuncHolder(s => parseOptOrLog(s).map(func) getOrElse JsonAST.JNothing))

  /**
   * 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 function ID and JavaScript that makes the call
   */
  private def jsonCall_*(jsCalcValue: JsExp, func: AFuncHolder): (String, JsExp) =
    fmapFunc((func))(name =>
            (name, makeAjaxCall(JsRaw("'" + name + "=' + encodeURIComponent(JSON.stringify(" + jsCalcValue.toJsCmd + "))"))))

  /**
   * 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 ajaxContext -- the context defining the javascript callback functions and the response type
   * @param func -- the function to call when the data is sent
   *
   * @return the function ID and JavaScript that makes the call
   */
  private def jsonCall_*(jsCalcValue: JsExp,
                         ajaxContext: AjaxContext,
                         func: AFuncHolder): (String, JsExp) =
    fmapFunc((func))(name =>
            (name, makeAjaxCall(JsRaw("'" + name + "=' + encodeURIComponent(JSON.stringify(" + jsCalcValue.toJsCmd + "))"), ajaxContext)))

  def fajaxCall[T](jsCalcValue: JsExp, func: String => JsCmd)(f: (String, JsExp) => T): T = {
    val (name, js) = ajaxCall(jsCalcValue, func).product
    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 + "=' + encodeURIComponent(" + jsCalcValue.toJsCmd + ")"))))

  /**
   * 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 ajaxContext -- the context defining the javascript callback functions and the response type
   * @param func -- the function to call when the data is sent
   *
   * @return the JavaScript that makes the call
   */
  private def ajaxCall_*(jsCalcValue: JsExp,
                         ajaxContext: AjaxContext,
                         func: AFuncHolder): (String, JsExp) =
    fmapFunc((func))(name =>
            (name, makeAjaxCall(JsRaw("'" + name + "=' + encodeURIComponent(" + jsCalcValue.toJsCmd + ")"), ajaxContext)))


  private def deferCall(data: JsExp, jsFunc: Call): Call =
    Call(jsFunc.function, (jsFunc.params ++ List(AnonFunc(makeAjaxCall(data)))): _*)

  /**
   * 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.
   * @param attrs -- the list of node attributes
   *
   * @return a button to put on your page
   */
  def ajaxButton(text: NodeSeq, func: () => JsCmd, attrs: ElemAttr*): Elem = {
    attrs.foldLeft(fmapFunc((func))(name =>
            ))((e, f) => f(e))
  }

  /**
   * Memoize the NodeSeq used in apply() and then call
   * applyAgain() in an Ajax call and you don't have to
   * explicitly capture the template
   */
  def memoize(f: => NodeSeq => NodeSeq): MemoizeTransform = {
    new MemoizeTransform {
      private var lastNodeSeq: NodeSeq = NodeSeq.Empty

     def apply(ns: NodeSeq): NodeSeq = {
        lastNodeSeq = ns
        f(ns)
      }

      def applyAgain(): NodeSeq = f(lastNodeSeq)
    }
  }

  /**
   * Memoize the NodeSeq used in apply() and then call
   * applyAgain() in an Ajax call and you don't have to
   * explicitly capture the template
   */
  def idMemoize(f: IdMemoizeTransform => NodeSeqFuncOrSeqNodeSeqFunc): IdMemoizeTransform = {
    new IdMemoizeTransform {
      var latestElem: Elem = 

      var latestKids: NodeSeq = NodeSeq.Empty

      var latestId = Helpers.nextFuncName

      private def fixElem(e: Elem): Elem = {
        e.attribute("id") match {
          case Some(id) => latestId = id.text ; e
          case None => e % ("id" -> latestId)
        }
      }

      def apply(ns: NodeSeq): NodeSeq =
        Helpers.findBox(ns){e => latestElem = fixElem(e);
                            latestKids = e.child; Full(e)}.
      map(ignore => applyAgain()).openOr(NodeSeq.Empty)

      def applyAgain(): NodeSeq =
        latestElem.copy(child = f(this)(latestKids))

      def setHtml(): JsCmd = SetHtml(latestId, f(this)(latestKids))
    }
  }

  /**
   * Create an Ajax button that when pressed, submits an Ajax request and expects back a JSON
   * construct which will be passed to the success function
   *
   * @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.
   * @param ajaxContext -- defines the callback functions and the JSON response type
   * @param attrs -- the list of node attributes
   *
   * @return a button to put on your page
   *
   */
  def jsonButton(text: NodeSeq, func: () => JsObj, ajaxContext: JsonContext, attrs: ElemAttr*): Elem = {
    attrs.foldLeft(fmapFunc((func))(name =>
            ))((e, f) => f(e))
  }

  /**
   * Create an Ajax button that when pressed, executes the function
   *
   * @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.
   * @param attrs -- the list of node attributes
   *
   * @return a button to put on your page
   */
  def ajaxButton(text: NodeSeq, jsExp: JsExp, func: String => JsCmd, attrs: ElemAttr*): Elem = {
    attrs.foldLeft(fmapFunc((SFuncHolder(func)))(name =>
            ))(_ % _)
  }

  /**
   * Create an Ajax button that when pressed, executes the function
   *
   * @param text -- the name/text of the button
   * @param jsFunc -- the user function that will be executed. This function will receive as last parameter
   *                  the function that will actually do the ajax call. Hence the user function can decide when
   * 				  to make the ajax request.
   * @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 pagejsFunc.params ++ List(AnonFunc(makeAjaxCall(Str(name+"=true"))))
   */
  def ajaxButton(text: NodeSeq, jsFunc: Call, func: () => JsCmd, attrs: ElemAttr*): Elem = {
    attrs.foldLeft(fmapFunc((func))(name =>
            ))(_ % _)
  }

  /**
   * Create an Ajax button that when pressed, executes the function
   *
   * @param text -- the name/text of the button
   * @param jsFunc -- the user function that will be executed. This function will receive as last parameter
   *                  the function that will actually do the ajax call. Hence the user function can decide when
   * 				  to make the ajax request.
   * @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: ElemAttr*): Elem =
    ajaxButton(Text(text), func, attrs: _*)

  /**
   * Create an Ajax button that when pressed, executes the function
   *
   * @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, jsFunc: Call, func: () => JsCmd, attrs: ElemAttr*): Elem =
    ajaxButton(Text(text), jsFunc, func, attrs: _*)

  /**
   * This method generates an AJAX editable field.
   *
   * Normally, the displayContents will be shown, with an "Edit" button.
   * If the "Edit" button is clicked, the field will be replaced with
   * the edit form, along with an "OK" and "Cancel" button.
   * If the OK button is pressed, the form fields are submitted and the onSubmit
   * function is called, and then the displayContents are re-run to get a new display.
   * If cancel is pressed then the original displayContents are re-shown.
   *
   * Note that the editForm NodeSeq is wrapped inside of an ajaxForm, so it can be comprised
   * of normal (non-AJAX) SHtml form elements. For example:
   *
   * 
   * ajaxEditable(Test("Click me"),
   *              SHtml.text("Edit me", s => println("Edited with " +s)),
   *              () => { println("submitted"); Noop })
   * 
*/ def ajaxEditable (displayContents : => NodeSeq, editForm : => NodeSeq, onSubmit : () => JsCmd) : NodeSeq = { import net.liftweb.http.js import js.{jquery,JsCmd,JsCmds,JE} import jquery.JqJsCmds import JsCmds.{Noop,SetHtml} import JE.Str import JqJsCmds.{Hide,Show} val divName = Helpers.nextFuncName val dispName = divName + "_display" val editName = divName + "_edit" def swapJsCmd (show : String, hide : String) : JsCmd = Show(show) & Hide(hide) def setAndSwap (show : String, showContents : => NodeSeq, hide : String) : JsCmd = (SHtml.ajaxCall(Str("ignore"), {ignore : String => SetHtml(show, showContents)})._2.cmd & swapJsCmd(show,hide)) def displayMarkup : NodeSeq = displayContents ++ Text(" ") ++ def editMarkup : NodeSeq = { val formData : NodeSeq = editForm ++ ++ hidden(onSubmit) ++ ajaxForm(formData, Noop, setAndSwap(dispName, displayMarkup, editName)) }
{displayMarkup}
} /** * 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 * @param attrs - the anchor node attributes */ def a(func: () => JsCmd, body: NodeSeq, attrs: ElemAttr*): Elem = { attrs.foldLeft(fmapFunc((func))(name => {body}))(_ % _) } /** * Create an anchor tag around a body which will do an AJAX call and invoke the function * * @param jsFunc -- the user function that will be executed. This function will receive as last parameter * the function that will actually do the ajax call. Hence the user function can decide when * to make the ajax request. * @param func - the function to invoke when the link is clicked * @param body - the NodeSeq to wrap in the anchor tag * @param attrs - the anchor node attributes */ def a(jsFunc: Call, func: () => JsCmd, body: NodeSeq, attrs: ElemAttr*): Elem = { attrs.foldLeft(fmapFunc((func))(name => {body}))(_ % _) } def a(func: () => JsObj, jsonContext: JsonContext, body: NodeSeq, attrs: ElemAttr*): Elem = { attrs.foldLeft(fmapFunc((func))(name => {body}))(_ % _) } /** * Create an anchor with a body and the function to be executed when the anchor is clicked */ def a(body: NodeSeq, attrs: ElemAttr*)(func: => JsCmd): Elem = a(() => func, body, attrs: _*) /** * Create an anchor with a body and the function to be executed when the anchor is clicked * @param jsFunc -- the user function that will be executed. This function will receive as last parameter * the function that will actually do the ajax call. Hence the user function can decide when * to make the ajax request. * @param body - the NodeSeq to wrap in the anchor tag * @param attrs - the anchor node attributes */ def a(jsFunc: Call, body: NodeSeq, attrs: ElemAttr*)(func: => JsCmd): Elem = a(jsFunc, () => func, body, attrs: _*) /** * Create an anchor that will run a JavaScript command when clicked */ def a(body: NodeSeq, cmd: JsCmd, attrs: ElemAttr*): Elem = attrs.foldLeft({body})(_ % _) /** * Create a span that will run a JavaScript command when clicked */ def span(body: NodeSeq, cmd: JsCmd, attrs: ElemAttr*): Elem = attrs.foldLeft({body})(_ % _) def toggleKids(head: Elem, visible: Boolean, func: () => JsCmd, 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 the server. Instead, json is a function * that allows you to build a more complex JsCmd based on the JsExp JE.JsRaw("this.value"). * This function is called by the overloaded version of jsonText. * * @param value - the initial value of the text field * @param ignoreBlur - ignore the onblur event and only do the event if the enter key is pressed * @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, ignoreBlur: Boolean, json: JsExp => JsCmd, attrs: ElemAttr*): Elem = (attrs.foldLeft( "" case s => s}}/>)(_ % _)) % ("onkeypress" -> """liftUtils.lift_blurIfReturn(event)""") % (if (ignoreBlur) Null else ("onblur" -> (json(JE.JsRaw("this.value"))))) /** * This function does not really submit a JSON request to the server. Instead, json is a function * that allows you to build a more complex JsCmd based on the JsExp JE.JsRaw("this.value"). * 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: ElemAttr*): Elem = jsonText(value, false, json, attrs :_*) /** * 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.createJsonFunc * * @return a text field */ def jsonText(value: String, cmd: String, json: JsonCall, attrs: ElemAttr*): Elem = jsonText(value, exp => json(cmd, exp), attrs: _*) def ajaxTextElem(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = ajaxText(settable.get, (b: String) => {settable.set(b); Noop}, attrs :_*) def ajaxText(value: String, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxText_*(value, false, Empty, SFuncHolder(func), attrs: _*) def ajaxText(value: String, jsFunc: Call, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxText_*(value, false, Full(jsFunc), SFuncHolder(func), attrs: _*) def ajaxText(value: String, ignoreBlur: Boolean, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxText_*(value, ignoreBlur, Empty, SFuncHolder(func), attrs: _*) def ajaxText(value: String, ignoreBlur: Boolean, jsFunc: Call, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxText_*(value, ignoreBlur, Full(jsFunc), SFuncHolder(func), attrs: _*) private def ajaxText_*(valuePreNull: String, ignoreBlur: Boolean, jsFunc: Box[Call], func: AFuncHolder, attrs: ElemAttr*): Elem = { val value = (Box !! valuePreNull).openOr("") val raw = (funcName: String, value: String) => JsRaw("'" + funcName + "=' + encodeURIComponent(" + value + ".value)") val key = formFuncName fmapFunc((func)) { funcName => (attrs.foldLeft()(_ % _)) % ("onkeypress" -> """liftUtils.lift_blurIfReturn(event)""") % (if (ignoreBlur) Null else ("onblur" -> (jsFunc match { case Full(f) => JsCrVar(key, JsRaw("this")) & deferCall(raw(funcName, key), f) case _ => makeAjaxCall(raw(funcName, "this")) }) )) } } /** * This function does not really submit a JSON request to the server. Instead, json is a function * that allows you to build a more complex JsCmd based on the JsExp JE.JsRaw("this.value"). * This function is called by the overloaded version of jsonTextarea. * * @param value - the initial value of the text area field * @param json - takes a JsExp which describes how to recover the * value of the text area field and returns a JsExp containing the thing * to execute on blur * * @return a text area field */ def jsonTextarea(value: String, json: JsExp => JsCmd, attrs: ElemAttr*): Elem = (attrs.foldLeft()(_ % _)) % ("onblur" -> (json(JE.JsRaw("this.value")))) /** * Create a JSON text area widget that makes a JSON call on blur * * @param value - the initial value of the text field * @param cmd - the json command name * @param json - the JsonCall returned from S.createJsonFunc * * @return a text field */ def jsonTextarea(value: String, cmd: String, json: JsonCall, attrs: ElemAttr*): Elem = jsonTextarea(value, exp => json(cmd, exp), attrs: _*) def ajaxTextarea(value: String, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxTextarea_*(value, Empty, SFuncHolder(func), attrs: _*) def ajaxTextarea(value: String, jsFunc: Call, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxTextarea_*(value, Full(jsFunc), SFuncHolder(func), attrs: _*) private def ajaxTextarea_*(value: String, jsFunc: Box[Call], func: AFuncHolder, attrs: ElemAttr*): Elem = { val raw = (funcName: String, value: String) => JsRaw("'" + funcName + "=' + encodeURIComponent(" + value + ".value)") val key = formFuncName fmapFunc((func)) { funcName => (attrs.foldLeft()(_ % _)) % ("onblur" -> (jsFunc match { case Full(f) => JsCrVar(key, JsRaw("this")) & deferCall(raw(funcName, key), f) case _ => makeAjaxCall(raw(funcName, "this")) }) ) } } trait AreaShape { def shape: String def coords: String } case class RectShape(left: Int, top: Int, right: Int, bottom: Int) extends AreaShape { def shape: String = "rect" def coords: String = ""+left+", "+top+", "+right+", "+bottom } case class CircleShape(centerX: Int, centerY: Int, radius: Int) extends AreaShape { def shape: String = "circle" def coords: String = ""+centerX+", "+centerY+", "+radius } case class CirclePercentShape(centerX: Int, centerY: Int, radiusPercent: Int) extends AreaShape { def shape: String = "circle" def coords: String = ""+centerX+", "+centerY+", "+radiusPercent+"%" } case class PolyShape(polyCoords: (Int, Int)*) extends AreaShape { def shape: String = "poly" def coords: String = polyCoords.map{ case (x, y) => ""+x+", "+y}.mkString(", ") } /** * Generate an Area tag * * @param shape - the shape of the area (RectShape, CircleShape, CirclePercentShape, PolyShape) * @param alt - the contents of the alt attribute * @param attrs - the balance of the attributes for the tag */ def area(shape: AreaShape, alt: String, attrs: ElemAttr*): Elem = attrs.foldLeft({alt})(_ % _) /** * Generate an Area tag * * @param shape - the shape of the area (RectShape, CircleShape, CirclePercentShape, PolyShape) * @param jsCmd - the JavaScript to execute on the client when the area is clicked * @param alt - the contents of the alt attribute * @param attrs - the balance of the attributes for the tag */ def area(shape: AreaShape, jsCmd: JsCmd, alt: String, attrs: ElemAttr*): Elem = area(shape, alt, (("onclick" -> jsCmd.toJsCmd): ElemAttr) :: attrs.toList :_*) /** * Generate an Area tag * * @param shape - the shape of the area (RectShape, CircleShape, CirclePercentShape, PolyShape) * @param func - The server side function to execute when the area is clicked on. * @param alt - the contents of the alt attribute * @param attrs - the balance of the attributes for the tag */ def area(shape: AreaShape, func: () => JsCmd, alt: String, attrs: ElemAttr*): Elem = { fmapFunc((func)) { funcName => area(shape, alt, (("onclick" -> (makeAjaxCall(Str(funcName + "=true")).toJsCmd + "; return false;")): ElemAttr) :: attrs.toList :_*) } } def ajaxCheckboxElem(settable: Settable{type ValueType = Boolean}, attrs: ElemAttr*): Elem = ajaxCheckbox(settable.get, (b: Boolean) => {settable.set(b); Noop}, attrs :_*) def ajaxCheckbox(value: Boolean, func: Boolean => JsCmd, attrs: ElemAttr*): Elem = ajaxCheckbox_*(value, Empty, LFuncHolder(in => func(in.exists(toBoolean(_)))), attrs: _*) def ajaxCheckboxElem(settable: Settable{type ValueType = Boolean}, jsFunc: Call, attrs: ElemAttr*): Elem = ajaxCheckbox_*(settable.get, Full(jsFunc), LFuncHolder(in => {settable.set(in.exists(toBoolean( _))); Noop}), attrs: _*) def ajaxCheckbox(value: Boolean, jsFunc: Call, func: Boolean => JsCmd, attrs: ElemAttr*): Elem = ajaxCheckbox_*(value, Full(jsFunc), LFuncHolder(in => func(in.exists(toBoolean(_)))), attrs: _*) private def ajaxCheckbox_*(value: Boolean, jsFunc: Box[Call], func: AFuncHolder, attrs: ElemAttr*): Elem = { val raw = (funcName: String, value: String) => JsRaw("'" + funcName + "=' + " + value + ".checked") val key = formFuncName fmapFunc((func)) { funcName => (attrs.foldLeft()(_ % _)) % checked(value) % ("onclick" -> (jsFunc match { case Full(f) => JsCrVar(key, JsRaw("this")) & deferCall(raw(funcName, key), f) case _ => makeAjaxCall(raw(funcName, "this")) })) } } /** * Make a set of Ajax radio buttons. When the buttons are pressed, * the function is called * * @param opts -- The possible values. These are not revealed to the browser * @param deflt -- the default button * @param ajaxFunc -- the function to invoke when the button is pressed */ def ajaxRadio[T](opts: Seq[T], deflt: Box[T], ajaxFunc: T => JsCmd, attrs: ElemAttr*): ChoiceHolder[T] = { val groupName = Helpers.nextFuncName val itemList = opts.map{ v => { ChoiceItem(v, attrs.foldLeft()(_ % _) % checked(deflt == Full(v)) % ("onclick" -> ajaxCall(Str(""), ignore => ajaxFunc(v))._2.toJsCmd)) } } ChoiceHolder(itemList) } /** * 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 ajaxSelectElem[T](options: Seq[T], default: Box[T], attrs: ElemAttr*) (onSubmit: T => JsCmd)(implicit f: PairStringPromoter[T]): Elem = { ajaxSelectObj[T](options.map(v => (v -> f(v))), default, onSubmit, 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 ajaxSelectObj[T](options: Seq[SelectableOption[T]], default: Box[T], onSubmit: T => JsCmd, attrs: ElemAttr*): Elem = { val secure = options.map { selectableOption => SelectableOptionWithNonce(selectableOption.value, randomString(20), selectableOption.label, selectableOption.attrs: _*) } val defaultNonce = default.flatMap { d => secure.find(_.value == d).map(_.nonce) } val nonces = secure.map { selectableOptionWithNonce => SelectableOption(selectableOptionWithNonce.nonce, selectableOptionWithNonce.label, selectableOptionWithNonce.attrs: _*) } def process(nonce: String): JsCmd = secure.find(_.nonce == nonce).map(x => onSubmit(x.value)) getOrElse Noop // (nonces, defaultNonce, SFuncHolder(process)) ajaxSelect_*(nonces, defaultNonce, Empty, SFuncHolder(process _), 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 ajaxSelectElem[T](options: Seq[T], default: Box[T], jsFunc: Call, attrs: ElemAttr*) (onSubmit: T => JsCmd) (implicit f: PairStringPromoter[T]): Elem = { ajaxSelectObj[T](options.map(v => (v, f(v))), default, jsFunc, onSubmit) } /** * 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 ajaxSelectObj[T](options: Seq[SelectableOption[T]], default: Box[T], jsFunc: Call, onSubmit: T => JsCmd, attrs: ElemAttr*): Elem = { val secure = options.map { selectableOption => SelectableOptionWithNonce(selectableOption.value, randomString(20), selectableOption.label, selectableOption.attrs: _*) } val defaultNonce = default.flatMap { d => secure.find(_.value == d).map(_.nonce) } val nonces = secure.map { selectableOptionWithNonce => SelectableOption(selectableOptionWithNonce.nonce, selectableOptionWithNonce.label, selectableOptionWithNonce.attrs: _*) } def process(nonce: String): JsCmd = secure.find(_.nonce == nonce).map(x => onSubmit(x.value)) getOrElse Noop // (nonces, defaultNonce, SFuncHolder(process)) ajaxSelect_*(nonces, defaultNonce, Full(jsFunc), SFuncHolder(process _), attrs: _*) } /** * Create a select box based on the list with a default value and the function * to be executed on form submission with an additional selection that * transforms select into an ajaxText allowing the user to add a new select option * * @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 func -- the function to execute when a new selection is made */ def ajaxEditableSelect(opts: Seq[SelectableOption[String]], deflt: Box[String], f: String => JsCmd, attrs: ElemAttr*): Elem = { val id = attrs.collectFirst { case BasicElemAttr(name, value) if name == "id" => value } getOrElse nextFuncName val attributes = if(attrs.contains(BasicElemAttr("id", id))) attrs else BasicElemAttr("id", id) +: attrs val textOpt = nextFuncName val options = opts :+ SelectableOption(textOpt , "New Element") var _options = options lazy val func: (String) => JsCmd = (select: String) => { def text(in: String): JsCmd = { _options = SelectableOption(in, in) +: _options Replace(id, ajaxSelect(_options, Some(in), func, attributes: _*)) } if (select == textOpt) Replace(id, ajaxText("", text(_), attributes: _*)) & Focus(id) else f(select) } ajaxSelect(options, deflt, func, attributes: _*) } def ajaxSelect(opts: Seq[SelectableOption[String]], deflt: Box[String], func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxSelect_*(opts, deflt, Empty, SFuncHolder(func), attrs: _*) def ajaxSelect(opts: Seq[SelectableOption[String]], deflt: Box[String], jsFunc: Call, func: String => JsCmd, attrs: ElemAttr*): Elem = ajaxSelect_*(opts, deflt, Full(jsFunc), SFuncHolder(func), attrs: _*) private def ajaxSelect_*(opts: Seq[SelectableOption[String]], deflt: Box[String], jsFunc: Box[Call], func: AFuncHolder, attrs: ElemAttr*): Elem = { val raw = (funcName: String, value: String) => JsRaw("'" + funcName + "=' + encodeURIComponent(" + value + ".options[" + value + ".selectedIndex].value)") val key = formFuncName val vals = opts.map(_.value) 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()(_ % _)) % ("onchange" -> (jsFunc match { case Full(f) => JsCrVar(key, JsRaw("this")) & deferCall(raw(funcName, key), f) case _ => makeAjaxCall(raw(funcName, "this")) })) } } def ajaxInvoke(func: () => JsCmd): GUIDJsExp = fmapFunc((NFuncHolder(func)))(name => (name, makeAjaxCall(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 ({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))}) } def swappable(shown: Elem, hidden: String => Elem): Elem = { val (rs, sid) = findOrAddId(shown) val hid = formFuncName val ui = LiftRules.jsArtifacts val rh = {hidden(ui.show(sid).toJsCmd + ";" + ui.hide(hid).toJsCmd + ";")} ({rs % ("onclick" -> (ui.hide(sid).toJsCmd + ";" + ui.show(hid).toJsCmd + "; return false;"))}{(rh % ("style" -> "display: none"))}) } 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.copy(attributes = new UnprefixedAttribute("onblur", Text(blurCmd + x.text), attrs)) } } /** * create an anchor tag around a body * * @param to the target * @param func the function to invoke when the link is clicked * @param body the NodeSeq to wrap in the anchor tag * @param attrs the (optional) attributes for the HTML element */ def link(to: String, func: () => Any, body: NodeSeq, attrs: ElemAttr*): Elem = { fmapFunc((a: List[String]) => {func(); true})(key => attrs.foldLeft({body})(_ % _)) } private def makeFormElement(name: String, func: AFuncHolder, attrs: ElemAttr*): Elem = fmapFunc(func)(funcName => attrs.foldLeft()(_ % _)) def text_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = text_*(value, func, Empty, attrs: _*) def text_*(value: String, func: AFuncHolder, ajaxTest: String => JsCmd, attrs: ElemAttr*): Elem = text_*(value, func, Full(ajaxTest), attrs: _*) private def buildOnBlur(bf: Box[String => JsCmd]): MetaData = bf match { case Full(func) => new UnprefixedAttribute("onblur", Text(ajaxCall(JsRaw("this.value"), func)._2.toJsCmd), Null) case _ => Null } def text_*(value: String, ignoreBlur: Boolean, func: AFuncHolder, ajaxTest: Box[String => JsCmd], attrs: ElemAttr*): Elem = makeFormElement("text", func, attrs: _*) % new UnprefixedAttribute("value", Text(value match {case null => "" case s => s}), Null) % ( if (ignoreBlur) Null else buildOnBlur(ajaxTest)) def text_*(value: String, func: AFuncHolder, ajaxTest: Box[String => JsCmd], attrs: ElemAttr*): Elem = text_*(value, false, func, ajaxTest, attrs :_*) def password_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = makeFormElement("password", func, attrs: _*) % ("value" -> value) def hidden_*(func: AFuncHolder, attrs: ElemAttr*): Elem = makeFormElement("hidden", func, attrs: _*) % ("value" -> "true") def submit_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = { def doit = makeFormElement("submit", func, attrs: _*) % ("value" -> value) _formGroup.is match { case Empty => formGroup(1)(doit) case _ => doit } } private def dupWithName(elem: Elem, name: String): Elem = { elem.copy(attributes = new UnprefixedAttribute("name", name, elem.attributes.filter { case up: UnprefixedAttribute => up.key != "name" case _ => true }) ) } private def isRadio(in: MetaData): Boolean = in.get("type").map(_.text equalsIgnoreCase "radio") getOrElse false private def isCheckbox(in: MetaData): Boolean = in.get("type").map(_.text equalsIgnoreCase "checkbox") getOrElse false /** * If you want to update the href of an <a> tag, * this method returns a function that mutates the href * by adding a function that will be executed when the link * is clicked: * * "#my_link" #> SHtml.hrefFunc(() => println("howdy")) * */ def hrefFunc(func: () => Any): NodeSeq => NodeSeq = { val allEvent = List("href") ns => { def runNodes(in: NodeSeq): NodeSeq = in.flatMap { case Group(g) => runNodes(g) // button case e: Elem => { val oldAttr: Map[String, String] = Map(allEvent. flatMap(a => e.attribute(a). map(v => a -> (v.text))) :_*) val newAttr = e.attributes.filter{ case up: UnprefixedAttribute => !oldAttr.contains(up.key) case _ => true } fmapFunc(func) { funcName => e.copy(attributes = allEvent.foldLeft(newAttr){ case (meta, attr) => new UnprefixedAttribute(attr, Helpers. appendFuncToURL(oldAttr.getOrElse(attr, ""), funcName+"=_"), meta) } ) } } case x => x } runNodes(ns) } } /** * Create something that's bindable to an event attribute * and when the event happens, the command will fire: * * "input [onblur]" #> SHtml.onEvent(s => Alert("Thanks: "+s)) * */ def onEvent(func: String => JsCmd): GUIDJsExp = ajaxCall(JsRaw("this.value"), func) /** * Create something that's bindable to an event attribute and * guarded by a confirm dialog. The fn passed in will only execute if * the user clicks the "OK" button in the confirm dialog. Useful for * things like delete buttons that you want a confirmation on. * * * ".deleteBtn [onclick]" #> SHtml.onEventIf("Are you sure you want to do that?", s => deleteUser()) * */ def onEventIf(question:String, fn:(String)=>JsCmd) : GUIDJsExp = { val eventExp = onEvent(fn) new GUIDJsExp(eventExp.guid, JsRaw(Confirm(question, eventExp.exp).toJsCmd)) } /** * Specify the events (e.g., onblur, onchange, etc.) * and the function to execute on those events. Returns * a NodeSeq => NodeSeq that will add the events to all * the Elements * * ":text" #> SHtml.onEvents("onchange", "onblur")(s => Alert("yikes "+s)) * */ def onEvents(event: String, events: String*)(func: String => JsCmd): NodeSeq => NodeSeq = { val allEvent = event :: events.toList ns => { def runNodes(in: NodeSeq): NodeSeq = in.flatMap { case Group(g) => runNodes(g) // button case e: Elem => { val oldAttr: Map[String, String] = Map(allEvent. flatMap(a => e.attribute(a). map(v => a -> (v.text+"; "))) :_*) val newAttr = e.attributes.filter{ case up: UnprefixedAttribute => !oldAttr.contains(up.key) case _ => true } val cmd = ajaxCall(JsRaw("this.value"), func)._2.toJsCmd e.copy(attributes = allEvent.foldLeft(newAttr) { case (meta, attr) => new UnprefixedAttribute(attr, oldAttr.getOrElse(attr, "") + cmd, meta) } ) } case x => x } runNodes(ns) } } /** * Execute the function when the form is submitted. * This method returns a function that can be applied to * form fields (input, button, textarea, select) and the * function is executed when the form containing the field is submitted. */ def onSubmitUnit(func: () => Any): NodeSeq => NodeSeq = onSubmitImpl(func: AFuncHolder) /** * Execute the String function when the form is submitted. * This method returns a function that can be applied to * form fields (input, button, textarea, select) and the * function is executed when the form containing the field is submitted. */ def onSubmit(func: String => Any): NodeSeq => NodeSeq = { onSubmitImpl(func: AFuncHolder) } /** * Execute the List[String] function when the form is submitted. * This method returns a function that can be applied to * form fields (input, button, textarea, select) and the * function is executed when the form containing the field is submitted. */ def onSubmitList(func: List[String] => Any): NodeSeq => NodeSeq = onSubmitImpl(func: AFuncHolder) /** * Execute the Boolean function when the form is submitted. * This method returns a function that can be applied to * form fields (input, button, textarea, select) and the * function is executed when the form containing the field is submitted. */ def onSubmitBoolean(func: Boolean => Any): NodeSeq => NodeSeq = onSubmitImpl(func: AFuncHolder) /** * Execute the function when the form is submitted. * This method returns a function that can be applied to * form fields (input, button, textarea, select) and the * function is executed when the form containing the field is submitted. */ def onSubmitImpl(func: AFuncHolder): NodeSeq => NodeSeq = { val fgSnap = S._formGroup.get (in: NodeSeq) => S._formGroup.doWith(fgSnap){ var radioName: Box[String] = Empty var checkBoxName: Box[String] = Empty var checkBoxCnt = 0 def runNodes(in: NodeSeq): NodeSeq = in.flatMap { case Group(g) => runNodes(g) // button case e: Elem if e.label == "button" => _formGroup.is match { case Empty => formGroup(1)(fmapFunc(func) {dupWithName(e, _)}) case _ => fmapFunc(func) {dupWithName(e, _)} } // textarea case e: Elem if e.label == "textarea" => fmapFunc(func) {dupWithName(e, _)} // select case e: Elem if e.label == "select" => fmapFunc(func) {dupWithName(e, _)} // radio case e: Elem if e.label == "input" && isRadio(e.attributes) => radioName match { case Full(name) => dupWithName(e, name) case _ => fmapFunc(func) { name => { radioName = Full(name) dupWithName(e, name) } } } // checkbox case e: Elem if e.label == "input" && isCheckbox(e.attributes) => checkBoxName match { case Full(name) => checkBoxCnt += 1 dupWithName(e, name) case _ => fmapFunc(func) { name => { checkBoxName = Full(name) checkBoxCnt += 1 dupWithName(e, name) } } } // submit case e: Elem if e.label == "input" && e.attribute("type").map(_.text) == Some("submit") => _formGroup.is match { case Empty => formGroup(1)(fmapFunc(func) {dupWithName(e, _)}) case _ => fmapFunc(func) {dupWithName(e, _)} } // generic input case e: Elem if e.label == "input" => fmapFunc(func) {dupWithName(e, _)} case x => x } val ret = runNodes(in) checkBoxName match { // if we've got a single checkbox, add a hidden false checkbox case Full(name) if checkBoxCnt == 1 => { ret ++ } case _ => ret } } } def text(value: String, func: String => Any, attrs: ElemAttr*): Elem = text_*(value, SFuncHolder(func), attrs: _*) /** * Generate an input element for the Settable */ def textElem(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = text_*(settable.get, SFuncHolder(s => settable.set(s)), attrs: _*) /** * Generate an input field with type email. At some point, * there will be graceful fallback for non-HTML5 browsers. FIXME */ def email(value: String, func: String => Any, attrs: ElemAttr*): Elem = email_*(value, SFuncHolder(func), attrs: _*) /** * Generate an email input element for the Settable. At some point * there will be graceful fallback for non-HTML5 browsers. FIXME */ def email(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = email_*(settable.get, SFuncHolder(s => settable.set(s)), attrs: _*) private def email_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = makeFormElement("email", func, attrs: _*) % new UnprefixedAttribute("value", Text(value), Null) /** * Generate an input field with type url. At some point, * there will be graceful fallback for non-HTML5 browsers. FIXME */ def url(value: String, func: String => Any, attrs: ElemAttr*): Elem = url_*(value, SFuncHolder(func), attrs: _*) /** * Generate a url input element for the Settable. At some point * there will be graceful fallback for non-HTML5 browsers. FIXME */ def url(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = url_*(settable.get, SFuncHolder(s => settable.set(s)), attrs: _*) private def url_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = makeFormElement("url", func, attrs: _*) % new UnprefixedAttribute("value", Text(value), Null) /** * Generate an input field with type number. At some point, * there will be graceful fallback for non-HTML5 browsers. FIXME */ def number(value: Int, func: Int => Any, min: Int, max: Int, attrs: ElemAttr*): Elem = number_*(value, min, max, SFuncHolder(s => Helpers.asInt(s).map(func)), attrs: _*) /** * Generate a number input element for the Settable. At some point * there will be graceful fallback for non-HTML5 browsers. FIXME */ def number(settable: Settable{type ValueType = Int}, min: Int, max: Int, attrs: ElemAttr*): Elem = number_*(settable.get, min, max, SFuncHolder(s => Helpers.asInt(s).map(s => settable.set(s))), attrs: _*) private def number_*(value: Int, min: Int, max: Int, func: AFuncHolder, attrs: ElemAttr*): Elem = { import Helpers._ makeFormElement("number", func, attrs: _*) % ("value" -> value.toString) % ("min" -> min.toString) % ("max" -> max.toString) } /** * Generate an input field with type number. It allows for Double if your step is * for example: 0.1 * At some point, * there will be graceful fallback for non-HTML5 browsers. FIXME */ def number(value: Double, func: Double => Any, min: Double, max: Double, step: Double , attrs: ElemAttr*): Elem = number_double_*(value, min, max, step, SFuncHolder(s => Helpers.asDouble(s).map(func)), attrs: _*) /** * Generate a number input element for the Settable. It allows for Double if your step is * for example: 0.1 * At some point * there will be graceful fallback for non-HTML5 browsers. FIXME */ def number(settable: Settable{type ValueType = Double}, min: Double, max: Double, step: Double, attrs: ElemAttr*): Elem = number_double_*(settable.get, min, max, step: Double, SFuncHolder(s => Helpers.asDouble(s).map(s => settable.set(s))), attrs: _*) private def number_double_*(value: Double, min: Double, max: Double, step: Double, func: AFuncHolder, attrs: ElemAttr*): Elem = { import Helpers._ import common.Full makeFormElement("number", func, attrs: _*) % ("value" -> value.toString) % ("min" -> min.toString) % ("min" -> min.toString) % ("step" -> step.toString) } /** * Generate an input field with type range. At some point, * there will be graceful fallback for non-HTML5 browsers. FIXME */ def range(value: Int, func: Int => Any, min: Int, max: Int, attrs: ElemAttr*): Elem = range_*(value, min, max, SFuncHolder(s => Helpers.asInt(s).map(func)), attrs: _*) /** * Generate a range input element for the Settable. At some point * there will be graceful fallback for non-HTML5 browsers. FIXME */ def range(settable: Settable{type ValueType = Int}, min: Int, max: Int, attrs: ElemAttr*): Elem = range_*(settable.get, min, max, SFuncHolder(s => Helpers.asInt(s).map(s => settable.set(s))), attrs: _*) private def range_*(value: Int, min: Int, max: Int, func: AFuncHolder, attrs: ElemAttr*): Elem = { import Helpers._ makeFormElement("range", func, attrs: _*) % ("value" -> value.toString) % ("min" -> min.toString) % ("max" -> max.toString) } def textAjaxTest(value: String, func: String => Any, ajaxTest: String => JsCmd, attrs: ElemAttr*): Elem = text_*(value, SFuncHolder(func), ajaxTest, attrs: _*) def textAjaxTest(value: String, func: String => Any, ajaxTest: Box[String => JsCmd], attrs: ElemAttr*): Elem = text_*(value, SFuncHolder(func), ajaxTest, attrs: _*) def password(value: String, func: String => Any, attrs: ElemAttr*): Elem = makeFormElement("password", SFuncHolder(func), attrs: _*) % new UnprefixedAttribute("value", Text(value), Null) def passwordElem(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = makeFormElement("password", SFuncHolder(s => settable.set(s)), attrs: _*) % new UnprefixedAttribute("value", Text(settable.get), Null) def hidden(func: () => Any, attrs: ElemAttr*): Elem = makeFormElement("hidden", NFuncHolder(func), attrs: _*) % ("value" -> "true") def hidden(func: (String) => Any, defaultlValue: String, attrs: ElemAttr*): Elem = makeFormElement("hidden", SFuncHolder(func), attrs: _*) % ("value" -> defaultlValue) /** * Create an HTML button with strOrNodeSeq as the body. The * button will be type submit. * * @param strOrNodeSeq -- the String or NodeSeq (either will work just fine) * to put into the body of the button * @param func -- the function to execute when the form containing the button * is posted * @param attrs -- the attributes to append to the button * @return a button HTML Element b */ def button(strOrNodeSeq: StringOrNodeSeq, func: () => Any, attrs: ElemAttr*): Elem = { def doit: Elem = { attrs.foldLeft(fmapFunc((func))(name => ))(_ % _) } _formGroup.is match { case Empty => formGroup(1)(doit) case _ => doit } } /** * Generates a form submission button. * * @param value The label for the button * @param func The function that will be executed on form submission * @param attrs Optional XHTML element attributes that will be applied to the button */ def submit(value: String, func: () => Any, attrs: ElemAttr*): Elem = { def doit = { makeFormElement("submit", NFuncHolder(func), attrs: _*) % new UnprefixedAttribute("value", Text(value), Null) } _formGroup.is match { case Empty => formGroup(1)(doit) case _ => doit } } /** * Constructs an Ajax submit button that can be used inside ajax forms. * Multiple buttons can be used in the same form. * * @param value - the button text * @param func - the ajax function to be called * @param attrs - button attributes * */ def ajaxSubmit(value: String, func: () => JsCmd, attrs: ElemAttr*): Elem = { val funcName = "z" + Helpers.nextFuncName addFunctionMap(funcName, (func)) (attrs.foldLeft()(_ % _)) % new UnprefixedAttribute("value", Text(value), Null) % ("onclick" -> ("lift.setUriSuffix('"+funcName+"=_'); return true;")) } /** * Add appropriate attributes to an input type="submit" or button * element to make it submit an ajaxForm correctly and return a JsCmd * to the client. Note that the key difference between this and ajaxSubmit * is that ajaxSubmit returns a complete input type="submit" element, while * ajaxOnSubmit applies the right attributes to any input type="submit" *or* * button element. * * Example: * *
"type=submit" #> ajaxOnSubmit(() => Alert("Done!"))
*/ def ajaxOnSubmit(func: () => JsCmd): (NodeSeq)=>NodeSeq = { val functionId = _formGroup.is match { case Empty => formGroup(1)(fmapFunc(func)(id => id)) case _ => fmapFunc(func)(id => id) } (in: NodeSeq) => { def runNodes(ns: NodeSeq): NodeSeq = { def addAttributes(elem: Elem, name: String) = { val clickJs = "lift.setUriSuffix('" + name + "=_'); return true;" elem % ("name" -> name) % ("onclick" -> clickJs) } ns.flatMap { case Group(nodes) => runNodes(nodes) case e: Elem if (e.label == "button") || (e.label == "input" && e.attribute("type").map(_.text) == Some("submit")) => addAttributes(e, functionId) } } runNodes(in) } } /** * Generates a form submission button with a default label. * * @param func The function that will be executed on form submission * @param attrs Optional XHTML element attributes that will be applied to the button */ def submitButton(func: () => Any, attrs: ElemAttr*): Elem = makeFormElement("submit", NFuncHolder(func), attrs: _*) /** * Takes a form and wraps it so that it will be submitted via AJAX. * * @param body The form body. This should not include the <form> tag. */ def ajaxForm(body: NodeSeq) = ({body}) /** * Takes a form and wraps it so that it will be submitted via AJAX. * * @param body The form body. This should not include the <form> tag. * @param onSubmit JavaScript code to execute on the client prior to submission */ def ajaxForm(body: NodeSeq, onSubmit: JsCmd) = ({body}) /** * Takes a form and wraps it so that it will be submitted via AJAX. This also * takes a parameter for script code that will be executed after the form has been submitted. * * @param body The form body. This should not include the <form> tag. * @param postSubmit Code that should be executed after a successful submission */ def ajaxForm(body : NodeSeq, onSubmit : JsCmd, postSubmit : JsCmd) = ({body}) /** * Having a regular form, this method can be used to send the serialized content of the form. * * @param formId - the id of the form */ def submitAjaxForm(formId: String):JsCmd = SHtml.makeAjaxCall(LiftRules.jsArtifacts.serialize(formId)) /** * Vend a function that will take all of the form elements and turns them * into Ajax forms */ def makeFormsAjax: NodeSeq => NodeSeq = "form" #> ((ns: NodeSeq) => (ns match { case e: Elem => { val id: String = e.attribute("id").map(_.text) getOrElse Helpers.nextFuncName val newMeta = e.attributes.filter{ case up: UnprefixedAttribute => up.key match { case "id" => false case "action" => false case "onsubmit" => false case "method" => false case _ => true } case _ => true } e.copy(attributes = newMeta) % ("id" -> id) % ("action" -> "javascript://") % ("onsubmit" -> (SHtml.makeAjaxCall(LiftRules.jsArtifacts.serialize(id)).toJsCmd + "; return false;")) } case x => x }): NodeSeq) /** * Submits a form denominated by a formId and execute the func function * after form fields functions are executed. */ def submitAjaxForm(formId: String, func: () => JsCmd): JsCmd = { val funcName = "Z" + Helpers.nextFuncName addFunctionMap(funcName, (func)) makeAjaxCall(JsRaw( LiftRules.jsArtifacts.serialize(formId).toJsCmd + " + " + Str("&" + funcName + "=true").toJsCmd)) } /** * Having a regular form, this method can be used to send the serialized content of the form. * * @param formId - the id of the form * @param postSubmit - the function that needs to be called after a successfull request */ def submitAjaxForm(formId: String, postSubmit: Call):JsCmd = SHtml.makeAjaxCall(LiftRules.jsArtifacts.serialize(formId), AjaxContext.js(Full(postSubmit.toJsCmd))) private def secureOptions[T](options: Seq[SelectableOption[T]], default: Box[T], onSubmit: T => Any): (Seq[SelectableOption[String]], Box[String], AFuncHolder) = { val secure = options.map { selectableOption => SelectableOptionWithNonce(selectableOption.value, randomString(20), selectableOption.label, selectableOption.attrs: _*) } val defaultNonce = default.flatMap { default => secure.find(_.value == default).map(_.nonce) } val nonces = secure.map { selectableOptionWithNonce => SelectableOption(selectableOptionWithNonce.nonce, selectableOptionWithNonce.label, selectableOptionWithNonce.attrs: _*) } def process(nonce: String): Unit = secure.find(_.nonce == nonce).map(x => onSubmit(x.value)) (nonces, defaultNonce, SFuncHolder(process)) } final case class SelectableOption[+T](value: T, label: String, attrs: ElemAttr*) object SelectableOption { implicit def tupleSeqToSelectableOptionSeq[T](seq: Seq[(T, String)]): Seq[SelectableOption[T]] = seq.collect { case (value, label) => SelectableOption(value, label) } implicit def tupleToSelectableOption[T](tuple: (T, String)): SelectableOption[T] = SelectableOption(tuple._1, tuple._2) } private def optionToElem(option: SelectableOption[String]): Elem = option.attrs.foldLeft()(_ % _) private final case class SelectableOptionWithNonce[+T](value: T, nonce: String, label: String, attrs: ElemAttr*) /** * 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[SelectableOption[String]], deflt: Box[String], func: String => Any, attrs: ElemAttr*): 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 values * @param default -- the default value (or Empty if no default value) * @param attrs -- the attributes to append to the resulting Elem, * these may be name-value pairs (static attributes) or special * HTML5 ElemAtts * @param onSubmit -- the function to execute on form submission * @param f -- the function that converts a T to a Display String. */ def selectElem[T](options: Seq[T], default: Box[T], attrs: ElemAttr*) (onSubmit: T => Any) (implicit f: PairStringPromoter[T]): Elem = { selectObj[T](options.map(v => SelectableOption(v, f(v))), default, onSubmit, 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 values * @param default -- the default value (or Empty if no default value) * @param attrs -- the attributes to append to the resulting Elem, * these may be name-value pairs (static attributes) or special * HTML5 ElemAtts * @param onSubmit -- the function to execute on form submission * @param f -- the function that converts a T to a Display String. */ def selectElem[T](options: Seq[T], settable: LiftValue[T], attrs: ElemAttr*) (implicit f: PairStringPromoter[T]): Elem = { selectObj[T](options.map(v => SelectableOption(v, f(v))), Full(settable.get), s => settable.set(s), 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[SelectableOption[T]], default: Box[T], onSubmit: T => Any, attrs: ElemAttr*): 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[SelectableOption[String]], deflt: Box[String], func: AFuncHolder, attrs: ElemAttr*): Elem = { val vals = opts.map(_.value) 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 => })(_ % _) } /** * 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[SelectableOption[String]], deflt: Box[String], func: String => Any, attrs: ElemAttr*): Elem = untrustedSelect_*(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. 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[SelectableOption[String]], deflt: Box[String], func: AFuncHolder, attrs: ElemAttr*): Elem = fmapFunc(func) { funcName => attrs.foldLeft()(_ % _) } /** * Create a multiple 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 untrustedMultiSelect(opts: Seq[SelectableOption[String]], deflt: Seq[String], func: List[String] => Any, attrs: ElemAttr*): NodeSeq = untrustedMultiSelect_*(opts, deflt, LFuncHolder(func), attrs: _*) /** * Create a multiple 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 untrustedMultiSelect_*(opts: Seq[SelectableOption[String]], deflt: Seq[String], lf: AFuncHolder, attrs: ElemAttr*): NodeSeq = { val hiddenId = Helpers.nextFuncName fmapFunc(LFuncHolder(l => lf(l.filter(_ != hiddenId)))) { funcName => NodeSeq.fromSeq( List( attrs.foldLeft()(_ % _), ) ) } } /** * 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 * @param attrs -- select box attributes */ def ajaxUntrustedSelect(opts: Seq[SelectableOption[String]], deflt: Box[String], func: String => JsCmd, attrs: (String, String)*): Elem = ajaxUntrustedSelect_*(opts, deflt, Empty, SFuncHolder(func), attrs: _*) /** * 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 jsFunc -- user provided function * @param deflt -- the default value (or Empty if no default value) * @param func -- the function to execute on form submission * @param attrs -- select box attributes */ def ajaxUntrustedSelect(opts: Seq[SelectableOption[String]], deflt: Box[String], jsFunc: Call, func: String => JsCmd, attrs: (String, String)*): Elem = ajaxUntrustedSelect_*(opts, deflt, Full(jsFunc), SFuncHolder(func), attrs: _*) /** * 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 jsFunc -- user provided function * @param func -- the function to execute on form submission * @param attrs -- select box attributes */ private def ajaxUntrustedSelect_*(opts: Seq[SelectableOption[String]], deflt: Box[String], jsFunc: Box[Call], func: AFuncHolder, attrs: (String, String)*): Elem = { val raw = (funcName: String, value: String) => JsRaw("'" + funcName + "=' + this.options[" + value + ".selectedIndex].value") val key = formFuncName val vals = opts.map(_.value) val testFunc = LFuncHolder(in => in match { case Nil => false case xs => func(xs) }, func.owner) fmapFunc(contextFuncBuilder(testFunc)) { import net.liftweb.http.js.JsCmds.JsCrVar funcName => (attrs.foldLeft()(_ % _)) % ("onchange" -> (jsFunc match { case Full(f) => JsCrVar(key, JsRaw("this")) & deferCall(raw(funcName, key), f) case _ => makeAjaxCall(raw(funcName, "this")) })) } } private def selected(in: Boolean) = if (in) new UnprefixedAttribute("selected", "selected", Null) else Null def multiSelect(opts: Seq[SelectableOption[String]], deflt: Seq[String], func: List[String] => Any, attrs: ElemAttr*): Elem = multiSelect_*(opts, deflt, LFuncHolder(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 multiSelectElem[T](options: Seq[T], default: Seq[T], attrs: ElemAttr*) (onSubmit: List[T] => Any) (implicit f: PairStringPromoter[T]): Elem = { multiSelectObj[T](options.map(v => SelectableOption(v, f(v))), default, onSubmit, 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 multiSelectObj[T](options: Seq[SelectableOption[T]], default: Seq[T], onSubmit: List[T] => Any, attrs: ElemAttr*): Elem = { val (nonces, defaultNonce, secureOnSubmit) = secureMultiOptions(options, default, onSubmit) multiSelect_*(nonces, defaultNonce, secureOnSubmit, attrs: _*) } private[http] def secureMultiOptions[T](options: Seq[SelectableOption[T]], default: Seq[T], onSubmit: List[T] => Any): (Seq[SelectableOption[String]], Seq[String], AFuncHolder) = { val o2 = options.toList val secure: List[SelectableOptionWithNonce[T]] = o2.map { selectableOption => SelectableOptionWithNonce(selectableOption.value, randomString(20), selectableOption.label, selectableOption.attrs: _*) } val sm: Map[String, T] = Map(secure.map(v => (v.nonce, v.value)): _*) val defaultNonce: Seq[String] = default.flatMap { defaultOption => secure.find(_.value == defaultOption).map(_.nonce) } val nonces: List[SelectableOption[String]] = secure.map { selectableOptionWithNonce => SelectableOption(selectableOptionWithNonce.nonce, selectableOptionWithNonce.label, selectableOptionWithNonce.attrs: _*) }.toList def process(info: List[String]): Unit = onSubmit(info.flatMap(sm.get)) (nonces, defaultNonce, LFuncHolder(process)) } def multiSelect_*(opts: Seq[SelectableOption[String]], deflt: Seq[String], func: AFuncHolder, attrs: ElemAttr*): Elem = fmapFunc(func)(funcName => attrs.foldLeft()(_ % _)) def textarea(value: String, func: String => Any, attrs: ElemAttr*): Elem = textarea_*(value, SFuncHolder(func), attrs: _*) def textareaElem(settable: Settable{type ValueType = String}, attrs: ElemAttr*): Elem = textarea_*(settable.get, SFuncHolder(s => settable.set(s)), attrs: _*) def textarea_*(value: String, func: AFuncHolder, attrs: ElemAttr*): Elem = fmapFunc(func)(funcName => attrs.foldLeft()(_ % _)) def radio(opts: Seq[String], deflt: Box[String], func: String => Any, attrs: ElemAttr*): ChoiceHolder[String] = radio_*(opts, deflt, SFuncHolder(func), attrs: _*) /** * Generate a collection or radio box items from a sequence of * things */ def radioElem[T](opts: Seq[T], deflt: Box[T], attrs: ElemAttr*) (onSubmit: Box[T] => Any): ChoiceHolder[T] = { val possible = opts.map(v => Helpers.nextFuncName -> v).toList val hiddenId = Helpers.nextFuncName fmapFunc(LFuncHolder(lst => lst.filter(_ != hiddenId) match { case Nil => onSubmit(Empty) case x :: _ => onSubmit(possible.filter(_._1 == x). headOption.map(_._2)) })) { name => { val items = possible.zipWithIndex.map { case ((id, value), idx) => { val radio = attrs.foldLeft()(_ % _) % checked(deflt.filter(_ == value).isDefined) val elem = if (idx == 0) { radio ++ } else { radio } ChoiceItem(value, elem) } } ChoiceHolder(items) } } } def radio_*(opts: Seq[String], deflt: Box[String], func: AFuncHolder, attrs: ElemAttr*): ChoiceHolder[String] = { fmapFunc(func) { name => val itemList = opts.map(v => ChoiceItem(v, attrs.foldLeft()(_ % _) % checked(deflt.filter((s: String) => s == v).isDefined))) ChoiceHolder(itemList) } } /** * Generate a CSS transform that will bind to a template containing radio buttons without overwriting * the radio buttons in the template with entirely new elements. * * This is a change in technique from the normal `radio` handlers in that it doesn't involve the * creation of a new element or injection of hidden elements. That means that labels designed to * target a partiular radio by ID and CSS styles that target elements following radios using the * `+` CSS selector won't stop working once a form is bound by Lift. * * As an example, given the following template: * * {{{ * * * * * * }}} * * You might invoke this function like so: * * {{{ * def render = { * SHtml.radioCssSel[String](Empty, submitHandler) ( * "#all-emails" -> "All emails", * "#some-emails" -> "Some emails" * ) * } * * val submitHandler:Box[String] => Unit = { box => * println("We got a box!! "+box) * } * }}} * * @param initialValue initial value or Empty if no initial value * @param onSubmit function to execute on form submission * @param cssSelToValue mapping between CSS selectors of radio input nodes and values assigned to them */ def radioCssSel[T](initialValue: Box[T], onSubmit: Box[T] => Any)(cssSelToValue: (String, T)*): CssSel = { val radioOptions = cssSelToValue.map(_._2 -> nextFuncName).toMap def selectionHandler(selection: String) = { onSubmit(radioOptions.find(_._2 == selection).map(_._1)) } S.fmapFunc(selectionHandler _)(funcName => { cssSelToValue.map { case (cssSel, value) => s"$cssSel [name]" #> funcName & s"$cssSel [value]" #> radioOptions(value) & s"$cssSel [checked]" #> { if (initialValue === value) Some("true") else None } }.reduceLeft(_ & _) }) } /** * Defines a form element for a file upload that will call the * specified function when the file is uploaded if the file size * is greater than zero. Note that in order to use the fileUpload * element you need to specify the multipart attribute on your * snippet tag: * *
   * <lift:Some.snippet form="POST" multipart="true">
   * ...
   * </lift:Some.snippet>
   * 
*/ def fileUpload(func: FileParamHolder => Any, attrs: ElemAttr*): Elem = { val f2: FileParamHolder => Any = fp => if (fp.length > 0) func(fp) fmapFunc(BinFuncHolder(f2)) { name => attrs.foldLeft() { _ % _ } } } /** Holds a form control as HTML along with some user defined value */ final case class ChoiceItem[T](key: T, xhtml: NodeSeq) /** Holds a series of choices: HTML for input controls alongside some user defined value */ final case class ChoiceHolder[T](items: Seq[ChoiceItem[T]]) { /** Retrieve the ChoiceItem that has the given key, throwing NoSuchElementException if there is no matching ChoiceItem */ def apply(in: T): NodeSeq = items.filter(_.key == in).head.xhtml /** Retrieve the nth ChoiceItem, 0-based */ def apply(in: Int): NodeSeq = items(in).xhtml /** Apply a function to each ChoiceItem, collecting the results */ def map[A](f: ChoiceItem[T] => A) = items.map(f) /** Apply a function to each ChoiceItem, concatenating the results */ def flatMap[A](f: ChoiceItem[T] => Iterable[A]) = items.flatMap(f) /** Return the ChoiceItems that the given function returns true for */ def filter(f: ChoiceItem[T] => Boolean) = items.filter(f) /** Generate a simple form by calling ChoiceItem.htmlize on each ChoiceItem and concatenating the resulting HTML */ def toForm: NodeSeq = flatMap(ChoiceHolder.htmlize) } object ChoiceHolder { /** Convert a ChoiceItem into a span containing the control and the toString of the key */ var htmlize: ChoiceItem[_] => NodeSeq = c => ({c.xhtml} {c.key.toString}
) } 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} /** * Generate a ChoiceHolder of possible checkbox type inputs that calls back to the given function when the form is submitted. * * @param possible complete sequence of possible values, each a separate checkbox when rendered * @param actual values to be preselected * @param func function to receive all values corresponding to the checked boxes * @param attrs sequence of attributes to apply to each checkbox input element * @return ChoiceHolder containing the checkboxes and values in order */ def checkbox[T](possible: Seq[T], actual: Seq[T], func: Seq[T] => Any, attrs: ElemAttr*): ChoiceHolder[T] = { fmapFunc { LFuncHolder((selectedChoiceValues: List[String]) => { val validSelectedIndicies = selectedChoiceValues.map(_.toInt).filter(possible.isDefinedAt(_)) val selectedValues = validSelectedIndicies.map(possible(_)) func(selectedValues) true }) } { name => ChoiceHolder(possible.toList.zipWithIndex.map { possibleChoice => ChoiceItem( possibleChoice._1, attrs.foldLeft()(_ % _) % checked(actual.contains(possibleChoice._1)) ++ { if (possibleChoice._2 == 0) else Nil } ) }) } } /** * Defines a new checkbox for the Settable */ def checkboxElem(settable: Settable{type ValueType = Boolean}, attrs: ElemAttr*): NodeSeq = { checkbox_id(settable.get, s => settable.set(s), Empty, attrs: _*) } /** * 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: ElemAttr*): NodeSeq = { checkbox_id(value, func, Empty, attrs: _*) } /** * Defines a new checkbox for the Settable */ def checkbox_id(settable: Settable{type ValueType = Boolean}, id: Box[String], attrs: ElemAttr*): NodeSeq = { def from(f: Boolean => Any): List[String] => Boolean = (in: List[String]) => { f(in.exists(toBoolean(_))) true } checkbox_*(settable.get, LFuncHolder(from(s => settable.set(s))), id, 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: ElemAttr*): 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: ElemAttr*): NodeSeq = { fmapFunc(func)(name => (attrs.foldLeft()(_ % _) % checked(value) % setId(id)) ++ () ) } } object AjaxType extends Enumeration { val JavaScript = Value("javascript") val JSON = Value("json") } object AjaxContext { def js(success: Box[String], failure: Box[String]) = new JsContext(success, failure) def js(success: Box[String]) = new JsContext(success, Empty) def json(success: Box[String], failure: Box[String]) = new JsonContext(success, failure) def json(success: Box[String]) = new JsonContext(success, Empty) } case class AjaxContext(success: Box[String], failure: Box[String], responseType: AjaxType.Value) class JsContext(override val success: Box[String], override val failure: Box[String]) extends AjaxContext(success, failure, AjaxType.JavaScript) class JsonContext(override val success: Box[String], override val failure: Box[String]) extends AjaxContext(success, failure, AjaxType.JSON) object Html5ElemAttr { /** * The autofocus attribute */ final case object Autofocus extends SHtml.ElemAttr { // FIXME detect HTML5 browser and do the right thing def apply(in: Elem): Elem = in % ("autofocus" -> "true") } /** * The required attribute */ final case object Required extends SHtml.ElemAttr { // FIXME detect HTML5 browser and do the right thing def apply(in: Elem): Elem = in % ("required" -> "true") } /** * The placeholder attribute for HTML5. * * @param text - a String or () => String that will be the * placeholder property in the attribute */ final case class Placeholder(text: StringFunc) extends SHtml.ElemAttr { // FIXME detect HTML5 browser and do the right thing def apply(in: Elem): Elem = in % ("placeholder" -> text.func()) } } /** * Mix this trait into a snippet class so that you have a convenient * value to redirect back to (whence). * When you're done with the snippet, S.redirectTo(whence) */ trait Whence { protected val whence = S.referer openOr "/" } /** * Memoize the CSS Selector Transform and the most recent * NodeSeq sent to the NodeSeq => NodeSeq so that when * applyAgain() is called, the NodeSeq most recently used * in apply() is used. */ trait MemoizeTransform extends Function1[NodeSeq, NodeSeq] { def applyAgain(): NodeSeq } /** * A mechanism to memoize a transformation and then * re-use the most recent html and ID to redraw * the content or even use an Ajax call to update the content */ trait IdMemoizeTransform extends Function1[NodeSeq, NodeSeq] { /** * The latest ID of the outer */ def latestId: String /** * The outer Elem */ def latestElem: Elem /** * The children of the Elem */ def latestKids: NodeSeq def applyAgain(): NodeSeq def setHtml(): JsCmd } sealed trait NodeSeqFuncOrSeqNodeSeqFunc extends Function1[NodeSeq, NodeSeq] object NodeSeqFuncOrSeqNodeSeqFunc { implicit def promoteNodeSeq(in: NodeSeq => NodeSeq): NodeSeqFuncOrSeqNodeSeqFunc = NodeSeqFunc(in) implicit def promoteSeqNodeSeq(in: Seq[NodeSeq => NodeSeq]): NodeSeqFuncOrSeqNodeSeqFunc = SeqNodeSeqFunc(in) } final case class NodeSeqFunc(f: NodeSeq => NodeSeq) extends NodeSeqFuncOrSeqNodeSeqFunc { def apply(ns: NodeSeq): NodeSeq = f(ns) } final case class SeqNodeSeqFunc(f: Seq[NodeSeq => NodeSeq]) extends NodeSeqFuncOrSeqNodeSeqFunc { def apply(ns: NodeSeq): NodeSeq = f.flatMap(_(ns)) } /** * A long time ago, Lift was going to track every function/GUID combination vended to * a web page with extreme granularity. This meant that for every function/GUID vended, * Lift would put that GUID in an attribute associated with the element on the page. In * order to capture the GUIDs, some methods like SHtml.ajaxCall() returned a Tuple containing * the GUID and the JsExp. This caused confusion and ugly code. So, the GUIDJsExp * came into being. Basically, it's backward compatible with the Tuple (String, JsExp), but * it functions like a JsExp (although you don't even have to call .toJsCmd because * the toString method returns the expresion itself). It should make the ajaxCall()._2.toJsCmd * thing into ajaxCall(). */ class GUIDJsExp(val guid: String,val exp: JsExp) extends JsExp { def product: (String, JsExp) = this def _1: String = guid def _2: JsExp = exp def toJsCmd: String = exp.toJsCmd override def toString = this.toJsCmd } /** * The companion object for GUIDJsExp that does helpful implicit conversions. */ object GUIDJsExp { implicit def guidToTuple(in: GUIDJsExp): (String, JsExp) = (in.guid, in.exp) implicit def tupleToGUIDJS(in: (String, JsExp)): GUIDJsExp = new GUIDJsExp(in._1, in._2) def unapply(in: GUIDJsExp): Option[(String, JsExp)] = Some(in) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy