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 {

  /**
   * 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))

        new Elem(updated.prefix, updated.label,
                               updated.attributes, updated.scope,
                               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 = "liftAjax.lift_ajaxHandler(" + 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 = "liftAjax.lift_ajaxHandler(" + 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))

  /**
   * 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 => parseOpt(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 => parseOpt(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 => parseOpt(s).map(func) getOrElse JsonAST.JNothing))

  /**
   * Build a JavaScript function that will perform a JSON 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
   */
  @deprecated("Use jsonCall with a function that takes JValue => JsCmd", "2.5")
  def jsonCall(jsCalcValue: JsExp, func: Any => JsCmd)(implicit d: AvoidTypeErasureIssues1): GUIDJsExp =
    jsonCall_*(jsCalcValue, SFuncHolder(s => JSONParser.parse(s).map(func) openOr Noop))

  /**
   * Build a JavaScript function that will perform a JSON call based on a value calculated in JavaScript
   *
   * @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
   */
  @deprecated("Use jsonCall with a function that takes JValue => JsCmd", "2.5")
  def jsonCall(jsCalcValue: JsExp, jsContext: JsContext, func: Any => JsCmd)(implicit d: AvoidTypeErasureIssues1): GUIDJsExp =
    jsonCall_*(jsCalcValue, jsContext, SFuncHolder(s => JSONParser.parse(s).map(func) openOr Noop))


  /**
   * 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)
  }

  @deprecated("Use jsonCall with a function that takes JValue => JsCmd", "2.5")
  def jsonCall(jsCalcValue: JsExp, jsonContext: JsonContext, func: String => JsObj)(implicit d: AvoidTypeErasureIssues1): GUIDJsExp = 
    ajaxCall_*(jsCalcValue, jsonContext, SFuncHolder(func))

  @deprecated("Use jsonCall with a function that takes JValue => JValue and its GUIDJsExp to manipulate the guid and JsExp it produces. This function will go away altogether in Lift 3.", "2.6")
  def fjsonCall[T](jsCalcValue: JsExp, jsonContext: JsonContext, func: String => JsObj)(f: (String, JsExp) => T): T = {
    val (name, js) = jsonCall(jsCalcValue, jsonContext, 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 =
        new Elem(latestElem.prefix,
                 latestElem.label,
                 latestElem.attributes,
                 latestElem.scope,
                 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, 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, jsExp: JsExp, func: JValue => JsCmd, ajaxContext: JsonContext, attrs: ElemAttr*)(implicit dummy: AvoidTypeErasureIssues1): Elem = {
    attrs.foldLeft(jsonFmapFunc(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" -> ("liftAjax.lift_uriSuffix = '"+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 fgSnap = S._formGroup.get (in: NodeSeq) => S._formGroup.doWith(fgSnap) { def runNodes(ns: NodeSeq): NodeSeq = { def addAttributes(elem: Elem, name: String) = { val clickJs = "liftAjax.lift_uriSuffix = '" + 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")) => _formGroup.is match { case Empty => formGroup(1)(fmapFunc(func)(addAttributes(e, _))) case _ => fmapFunc(func)(addAttributes(e, _)) } } } 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 * * @deprecated Use ajaxForm(NodeSeq,JsCmd) instead */ @deprecated("Use ajaxForm(NodeSeq, JsCmd) instead.", "2.6") def ajaxForm(onSubmit: JsCmd, 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}) /** * Takes a form and wraps it so that it will be submitted via AJAX and processed by * a JSON handler. This can be useful if you may have dynamic client-side modification * of the form (addition or removal). * * @param jsonHandler The handler that will process the form * @param body The form body. This should not include the <form> tag. */ @deprecated("Use JValue=>JsCmd bindings in SHtml (such as jsonCall) and SHtml.makeFormsAjax instead of this.", "2.6") def jsonForm(jsonHandler: JsonHandler, body: NodeSeq): NodeSeq = jsonForm(jsonHandler, Noop, body) /** * Takes a form and wraps it so that it will be submitted via AJAX and processed by * a JSON handler. This can be useful if you may have dynamic client-side modification * of the form (addition or removal). * * @param jsonHandler The handler that will process the form * @param onSubmit JavaScript code that will be executed on the client prior to submitting * the form * @param body The form body. This should not include the <form> tag. */ @deprecated("Use JValue=>JsCmd bindings in SHtml (such as jsonCall) and SHtml.makeFormsAjax instead of this.", "2.6") def jsonForm(jsonHandler: JsonHandler, onSubmit: JsCmd, body: NodeSeq): NodeSeq = { val id = formFuncName
{body}
} /** * Having a regular form, this method can be used to send the content of the form as JSON. * the request will be processed by the jsonHandler * * @param jsonHandler - the handler that process this request * @param formId - the id of the form */ @deprecated("Use JValue=>JsCmd bindings in SHtml (such as jsonCall) and SHtml.submitAjaxForm instead of this.", "2.6") def submitJsonForm(jsonHandler: JsonHandler, formId: String):JsCmd = jsonHandler.call("processForm", FormToJSON(formId)) /** * 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 } new Elem(e.prefix, e.label, newMeta, e.scope, e.child :_*) % ("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) } } /** * 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 - 2025 Weber Informatics LLC | Privacy Policy