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

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

The newest version!
/*
 * Copyright 2009-2014 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 net.liftweb._
import http._
import js._
import JsCmds._

import common._
import util._
import Helpers._
import scala.xml._
import scala.reflect.Manifest

object WizardRules extends Factory with FormVendor {
  val dbConnectionsForTransaction: FactoryMaker[List[LoanWrapper]] =
    new FactoryMaker[List[LoanWrapper]](() => Nil) {}

  private def m[T](implicit man: Manifest[T]): Manifest[T] = man

  val allTemplatePath: FactoryMaker[List[String]] = new FactoryMaker[List[String]](() => List("templates-hidden", "wizard-all")) {}

  private object currentWizards extends SessionVar[Set[String]](Set())

  private[http] def registerWizardSession(): String = {
    S.synchronizeForSession {
      val ret = Helpers.nextFuncName
      currentWizards.set(currentWizards.is + ret)
      ret
    }
  }

  private[http] def isValidWizardSession(id: String): Boolean =
    S.synchronizeForSession {
      currentWizards.is.contains(id)
    }

  private[http] def deregisterWizardSession(id: String): Unit = {
    S.synchronizeForSession {
      currentWizards.set(currentWizards.is - id)
    }
  }
}

/**
 * A wizard allows you to create a multi-screen set of input forms
 * with back-button support and state support
 */
trait Wizard extends StatefulSnippet with Factory with ScreenWizardRendered {
  def dispatch = {
    case _ => template => {
      _defaultXml.set(template)
      this.toForm
    }
  }

  /**
   * Holds the template passed via the snippet for the duration
   * of the request
   */
  protected object _defaultXml extends TransientRequestVar[NodeSeq](NodeSeq.Empty) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  /**
   * the NodeSeq passed as a parameter when the snippet was invoked
   */
  protected def defaultXml: NodeSeq = _defaultXml.get

  implicit def elemInABox(in: Elem): Box[Elem] = Full(in)

  @volatile private[this] var _screenList: List[Screen] = Nil

  private object ScreenVars extends TransientRequestVar[Map[String, (NonCleanAnyVar[_], Any)]](Map()) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  protected object CurrentScreen extends WizardVar[Box[Screen]]({
    val screen = calcFirstScreen
    screen.foreach(_.transitionIntoFrom(Empty))
    screen.foreach(_.enterScreen())
    screen
  }) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  private object PrevSnapshot extends TransientRequestVar[Box[WizardSnapshot]](Empty) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  protected object Referer extends WizardVar[String](calcReferer) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  protected object Ajax_? extends WizardVar[Boolean](calcAjax) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  /**
   * A unique GUID for the form... this allows us to do an Ajax SetHtml
   * to replace the form
   */
  protected object FormGUID extends WizardVar[String](Helpers.nextFuncName) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  /**
   * What to do when the Screen is done.  By default, will
   * do a redirect back to Whence, but you can change this behavior,
   * for example, put up some other Ajax thing or alternatively,
   * remove the form from the screen.
   */
  protected object AjaxOnDone extends WizardVar[JsCmd](calcAjaxOnDone) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  protected object OnFirstScreen extends TransientRequestVar[Boolean](true) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  private object FirstTime extends WizardVar[Boolean](true) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  private object CurrentSession extends WizardVar[String](WizardRules.registerWizardSession()) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }

  protected object VisitedScreens extends WizardVar[Vector[Screen]](Vector()) {
    override lazy val __nameSalt = Helpers.nextFuncName
  }


  def noticeTypeToAttr(screen: AbstractScreen): Box[NoticeType.Value => MetaData] = {
    screen.inject[NoticeType.Value => MetaData] or
      inject[NoticeType.Value => MetaData] or
      WizardRules.inject[NoticeType.Value => MetaData] or
      screen.noticeTypeToAttr(screen)
  }

  /**
   * Override this method to do setup the first time the
   * screen is entered
   */
  protected def localSetup(): Unit = {

  }

  override def formName: String = "wizard"

  def toForm: NodeSeq = {
    ScreenVars.is // initialize
    Referer.is // touch to capture the referer
    Ajax_?.is // capture the Ajax state
    CurrentSession.is
    FormGUID.is

    if (FirstTime) {
      FirstTime.set(false)

      localSetup()

      if (!ajaxForms_?) {
        val localSnapshot = createSnapshot
        // val notices = S.getAllNotices
        S.seeOther(S.uri, () => {
          // S.appendNotices(notices)
          localSnapshot.restore()
        })
      }
    }

    val form = renderHtml()
    if (ajaxForms_?) wrapInDiv(form) else form
  }

  protected def submitOrAjax(id: String): String =
    (if (ajaxForms_?) {
      SHtml.makeAjaxCall(LiftRules.jsArtifacts.serialize(id)).toJsCmd
    } else {
      "document.getElementById(" + id.encJs + ").submit()"
    })

  protected def renderHtml(): NodeSeq = {
    val nextId = Helpers.nextFuncName
    val prevId = Helpers.nextFuncName
    val cancelId = Helpers.nextFuncName

    val theScreen: Screen = currentScreen openOr {
      WizardRules.deregisterWizardSession(CurrentSession.is)
      S.seeOther(Referer.is) // this should never happen
    }

    val (nextButton, finishButton) =
      if (!theScreen.isLastScreen)
        (Full(theScreen.nextButton %
          ("onclick" -> submitOrAjax(nextId))), Empty)
      else
        (Empty, Full(theScreen.finishButton %
          ("onclick" -> submitOrAjax(nextId))))

    val prevButton: Box[Elem] = if (OnFirstScreen) Empty
    else
      Full(theScreen.prevButton % ("onclick" -> submitOrAjax(prevId)))

    val cancelButton: Elem = theScreen.cancelButton %
      ("onclick" -> submitOrAjax(cancelId))


    val url = S.uri

    val extraFields: List[ScreenFieldInfo] =
      if (theScreen.confirmScreen_?) {

        for {
          screen <- VisitedScreens.is.toList
          field <- screen.screenFields.collect {
            case c: ConfirmField => c
          } if field.show_? && field.onConfirm_?
        } yield ScreenFieldInfo(field, field.displayHtml, Empty,
          Full(field.asHtml))
      } else Nil

    renderAll(
      CurrentScreen.is.map(s => Text((s.myScreenNum + 1).toString)), //currentScreenNumber: Box[NodeSeq],
      Full(Text(screenCount.toString)), //screenCount: Box[NodeSeq],
      wizardTop, // wizardTop: Box[Elem],
      theScreen.screenTop, //screenTop: Box[Elem],
      extraFields :::
        theScreen.screenFields.flatMap(f =>
          if (f.show_?) List(ScreenFieldInfo(f, f.displayHtml, f.helpAsHtml, f.toForm)) else Nil), //fields: List[ScreenFieldInfo],
      prevButton, // prev: Box[Elem],
      Full(cancelButton), // cancel: Box[Elem],
      nextButton, // next: Box[Elem],
      finishButton, //finish: Box[Elem],
      theScreen.screenBottom, // screenBottom: Box[Elem],
      wizardBottom, //wizardBottom: Box[Elem],
      nextId -> (() => {
        this.nextScreen()
        // if (currentScreen.isEmpty) S.seeOther(Referer.is)
      }), // nextId: (String, () => Unit),
      Full(prevId -> (() => {
        this.prevScreen()
      })), // prevId: Box[(String, () => Unit)],
      cancelId -> (() => {
        WizardRules.deregisterWizardSession(CurrentSession.is); redirectBack()
      }), //cancelId: (String, () => Unit),
      theScreen, ajaxForms_?)
  }


  protected def allTemplatePath: List[String] = WizardRules.allTemplatePath.vend

  protected def allTemplate: NodeSeq = Templates(allTemplatePath) openOr allTemplateNodeSeq

  /**
   * What additional attributes should be put on the
   */
  protected def formAttrs: MetaData = scala.xml.Null

  protected def wizardTop: Box[Elem] = None

  protected def wizardBottom: Box[Elem] = None

  private def doTransition(from: Box[Screen], to: Box[Screen]): Unit = {
    to.foreach(_.enterScreen())

    (from, to) match {
      case (Full(old), Full(cur)) if old eq cur => {
        /* do nothing */
      }
      case (Full(old), Full(cur)) => {
        old.transitionOutOfTo(Full(cur))
        cur.transitionIntoFrom(Full(old))
      }

      case (Full(old), _) => old.transitionOutOfTo(Empty)
      case (_, Full(cur)) => cur.transitionIntoFrom(Empty)
      case _ =>
    }
  }

  class WizardSnapshot(private[http] val screenVars: Map[String, (NonCleanAnyVar[_], Any)],
                       val currentScreen: Box[Screen],
                       private[http] val snapshot: Box[WizardSnapshot],
                       private val firstScreen: Boolean) extends Snapshot {
    def restore(): Unit = {
      registerThisSnippet();
      ScreenVars.set(screenVars)
      if (CurrentScreen.set_?) {
        doTransition(CurrentScreen.get, currentScreen)
      } else {
        currentScreen.foreach(_.transitionIntoFrom(Empty))
        currentScreen.foreach(_.enterScreen())
      }

      CurrentScreen.set(currentScreen)
      PrevSnapshot.set(snapshot)
      OnFirstScreen.set(firstScreen)
      if (!WizardRules.isValidWizardSession(CurrentSession.is)) {
        S.seeOther(Referer.is) // FIXME Wizard
      }
    }
  }

  private def _register(screen: Screen): Unit = {
    _screenList = _screenList ::: List(screen)
  }

  def dbConnections: List[LoanWrapper] = WizardRules.dbConnectionsForTransaction.vend

  /**
   * The ordered list of Screens
   */
  def screens: List[Screen] = _screenList

  /**
   * Total number of screens in the wizard
   */
  lazy val screenCount = screens.size

  /**
   * Given the current screen, what's the next screen?
   */
  def calcScreenAfter(which: Screen): Box[Screen] =
    screens.dropWhile(_ ne which).drop(1).headOption


  /**
   * What's the first screen in this wizard
   */
  def calcFirstScreen: Box[Screen] = screens.headOption

  def nextButton: Elem = 

  def prevButton: Elem = 

  def cancelButton: Elem = 

  def finishButton: Elem = 

  def currentScreen: Box[Screen] = CurrentScreen.is

  def createSnapshot = {
    val cs = CurrentScreen.is
    val prev = PrevSnapshot.is
    val onFirst = OnFirstScreen.is
    new WizardSnapshot(ScreenVars.is, cs, prev, onFirst)
  }

  /**
   * This method will be called within a transactional block when the last screen is completed
   */
  protected def finish(): Unit

  def nextScreen(): JsCmd = {
    (for {
      screen <- CurrentScreen.is
    } yield {
      screen.validate match {
        case Nil => {
          val snapshot = createSnapshot
          PrevSnapshot.set(Full(snapshot))
          val nextScreen = screen.nextScreen
          CurrentScreen.is.foreach {
            s => VisitedScreens.set(VisitedScreens :+ s)
          }
          doTransition(CurrentScreen.get, nextScreen)
          CurrentScreen.set(nextScreen)
          OnFirstScreen.set(false)

          nextScreen match {
            case Empty =>
              def useAndFinish(in: List[LoanWrapper]): Unit = {
                in match {
                  case Nil => {
                    WizardRules.deregisterWizardSession(CurrentSession.is)
                    VisitedScreens.foreach {
                      s => s.finish()
                    }
                    finish()
                    VisitedScreens.foreach {
                      s => s.postFinish()
                    }
                  }

                  case x :: xs => x.apply {
                      useAndFinish(xs)
                  }
                }
              }
              useAndFinish(dbConnections)
              if (ajaxForms_?) {
                AjaxOnDone.is
              } else {
                Noop
              }

            case _ => SetHtml(FormGUID, renderHtml())
          }
        }
        case xs => {
          S.error(xs)
          SetHtml(FormGUID, renderHtml())
        }
      }
    }) openOr AjaxOnDone.is
  }

  def prevScreen(): JsCmd = {
    (for {
      snapshot <- PrevSnapshot.is
    } yield {
      val cur = if (CurrentScreen.set_?) CurrentScreen.get else Empty
      snapshot.restore()

      if (CurrentScreen.set_?) {
        doTransition(cur, CurrentScreen.get)
      } else {
        cur.foreach(_.transitionIntoFrom(Empty))
        cur.foreach(_.enterScreen())
      }

      SetHtml(FormGUID, renderHtml())
    }) openOr AjaxOnDone.is
  }

  protected def vendForm[T](implicit man: Manifest[T]): Box[(T, T => Any) => NodeSeq] = Empty

  /**
   * By default, are all the fields on all the screen in this wizard on the confirm screen?
   */
  def onConfirm_? = true

  /**
   * Define a screen within this wizard
   */
  trait Screen extends AbstractScreen {
    val myScreenNum = screens.length

    /**
     * The name of the screen.  Override this to change the screen name
     */
    override def screenName: String = "Screen " + (myScreenNum + 1)

    def nextButton: Elem = Wizard.this.nextButton

    def prevButton: Elem = Wizard.this.prevButton

    override def cancelButton: Elem = Wizard.this.cancelButton

    override def finishButton: Elem = Wizard.this.finishButton

    def nextScreen: Box[Screen] = calcScreenAfter(this)

    def isLastScreen = nextScreen.isEmpty

    /**
     * A notification that we are transitioning into
     * this screen.  Override this method to perform
     * some screen-specific actions
     *
     * @param from the screen we're coming from
     */
    def transitionIntoFrom(from: Box[Screen]): Unit = {
    }

    /**
     * A notification that we are transitioning out of
     * this screen.  Override this method to perform
     * some screen-specific actions
     *
     * @param to the screen we're transitioning to
     */
    def transitionOutOfTo(to: Box[Screen]): Unit = {
    }

    /**
     * By default, are all the fields on this screen on the confirm screen?
     */
    def onConfirm_? = Wizard.this.onConfirm_?


    /**
     * Is this screen a confirm screen?
     */
    def confirmScreen_? = false

    /**
     * Define a field within the screen
     */
    trait Field extends super.Field with ConfirmField {
      /**
       * Is this field on the confirm screen
       */
      override def onConfirm_? = Screen.this.onConfirm_?

      override protected def otherFuncVendors(what: Manifest[ValueType]):
      Box[(ValueType, ValueType => Any) => NodeSeq] =
        Wizard.this.vendForm(manifest) or WizardRules.vendForm(manifest)
    }

    Wizard.this._register(this)

    private object _touched extends WizardVar(false) {
      override lazy val __nameSalt = Helpers.nextFuncName
    }

    /**
     * override this method if there's a screen-specific thing
     * to do on finish.  This method is called before the main Wizard's
     * finish method
     */
    def finish(): Unit = {
    }

    /**
     * override this method if there's a screen-specific thing
     * to do on finish.  This method is executed after the main Wizards
     * finish() method.
     */
    def postFinish(): Unit = {
    }

    private[http] def enterScreen(): Unit = {
      if (!_touched) {
        _touched.set(true)
        this.localSetup()
      }
    }

    protected def vendAVar[T](dflt: => T): NonCleanAnyVar[T] = Wizard.this.vendAVar[T](dflt)
  }

  protected def vendAVar[T](dflt: => T): NonCleanAnyVar[T] = new WizardVar[T](dflt) {
    override protected def __nameSalt = randomString(20)
  }

  /**
   * Keep request-local information around without the nastiness of naming session variables
   * or the type-unsafety of casting the results.
   * RequestVars share their value through the scope of the current HTTP
   * request. They have no value at the beginning of request servicing
   * and their value is discarded at the end of request processing. They
   * are helpful to share values across many snippets.
   *
   * @param dflt - the default value of the session variable
   */
  abstract class WizardVar[T](dflt: => T) extends NonCleanAnyVar[T](dflt) {
    override protected def findFunc(name: String): Box[T] = WizardVarHandler.get(name)

    override protected def setFunc(name: String, value: T): Unit = WizardVarHandler.set(name, this, value)

    override protected def clearFunc(name: String): Unit = WizardVarHandler.clear(name)

    override protected def wasInitialized(name: String, bn: String): Boolean = {
      val old: Boolean = WizardVarHandler.get(bn) openOr false
      WizardVarHandler.set(bn, this, true)
      old
    }

    override protected def testWasSet(name: String, bn: String): Boolean = {
      WizardVarHandler.get(name).isDefined || (WizardVarHandler.get(bn) openOr false)
    }

    /**
     * Different Vars require different mechanisms for synchronization. This method implements
     * the Var specific synchronization mechanism
     */
    def doSync[F](f: => F): F = f

    // no sync necessary for RequestVars... always on the same thread
  }


  private[http] object WizardVarHandler {
    def get[T](name: String): Box[T] =
      ScreenVars.is.get(name).map(_._2.asInstanceOf[T])


    def set[T](name: String, from: WizardVar[_], value: T): Unit =
      ScreenVars.set(ScreenVars.is + (name -> (from, value)))

    def clear(name: String): Unit =
      ScreenVars.set(ScreenVars.is - name)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy