2016-02-24 19:32:12 +03:00
{- # LANGUAGE
FlexibleInstances ,
GeneralizedNewtypeDeriving ,
OverloadedStrings ,
QuasiQuotes ,
NoImplicitPrelude
#- }
-- TODO: try to make it more type-safe somehow?
module JS where
-- General
import BasePrelude
-- Text
import qualified Data.Text as T
import Data.Text ( Text )
-- Formatting and interpolation
import qualified Data.Text.Buildable as Format
import NeatInterpolation
-- Local
import Utils
-- | Javascript code.
newtype JS = JS { fromJS :: Text }
2016-02-25 15:57:50 +03:00
deriving ( Show , Format . Buildable , Monoid )
2016-02-24 19:32:12 +03:00
-- | A concatenation of all Javascript functions defined in this module.
allJSFunctions :: JS
allJSFunctions = JS . T . unlines . map fromJS $ [
-- Utilities
2016-02-25 17:49:17 +03:00
replaceWithData , prependData , appendData ,
2016-02-24 19:32:12 +03:00
moveNodeUp , moveNodeDown ,
2016-03-07 21:50:53 +03:00
switchSection ,
2016-02-26 15:59:27 +03:00
-- Help
showOrHideHelp , showHelp , hideHelp ,
2016-02-25 15:25:00 +03:00
-- Search
search ,
2016-02-24 19:32:12 +03:00
-- Add methods
addLibrary , addCategory ,
addPro , addCon ,
2016-03-05 00:40:51 +03:00
-- “Render this in a different way” methods
2016-03-08 13:32:56 +03:00
setItemTraitsMode ,
2016-02-24 19:32:12 +03:00
setTraitMode ,
-- Set methods
submitCategoryTitle , submitCategoryNotes ,
2016-03-05 00:40:51 +03:00
-- TODO: rename this to submitItemHeader or something?
submitItemInfo , submitItemNotes ,
2016-02-24 19:32:12 +03:00
submitTrait ,
-- Other things
2016-02-26 20:45:28 +03:00
moveTraitUp , moveTraitDown , deleteTrait ,
moveItemUp , moveItemDown , deleteItem ]
2016-02-24 19:32:12 +03:00
-- | A class for things that can be converted to Javascript syntax.
class ToJS a where toJS :: a -> JS
instance ToJS Bool where
toJS True = JS " true "
toJS False = JS " false "
instance ToJS JS where
toJS = id
instance ToJS Text where
toJS = JS . tshow
instance ToJS Integer where
toJS = JS . tshow
instance ToJS Int where
toJS = JS . tshow
-- | A helper class for calling Javascript functions.
class JSParams a where
jsParams :: a -> [ JS ]
instance JSParams () where
jsParams () = []
instance ToJS a => JSParams [ a ] where
jsParams = map toJS
instance ( ToJS a , ToJS b ) => JSParams ( a , b ) where
jsParams ( a , b ) = [ toJS a , toJS b ]
instance ( ToJS a , ToJS b , ToJS c ) => JSParams ( a , b , c ) where
jsParams ( a , b , c ) = [ toJS a , toJS b , toJS c ]
instance ( ToJS a , ToJS b , ToJS c , ToJS d ) => JSParams ( a , b , c , d ) where
jsParams ( a , b , c , d ) = [ toJS a , toJS b , toJS c , toJS d ]
{- | This hacky class lets you construct and use Javascript functions; you give 'makeJSFunction' function name, function parameters, and function body, and you get a polymorphic value of type @JSFunction a => a@, which you can use either as a complete function definition (if you set @a@ to be @JS@), or as a function that you can give some parameters and it would return a Javascript call:
> plus = makeJSFunction " plus " [ " a " , " b " ] " return a+b; "
>>> plus :: JS
JS " function plus(a,b) { \ n return a+b;} \ n "
>>> plus ( 3 , 5 ) :: JS
JS " plus(3,5); "
- }
class JSFunction a where
makeJSFunction
:: Text -- ^ Name
-> [ Text ] -- ^ Parameter names
-> Text -- ^ Definition
-> a
-- This generates function definition
instance JSFunction JS where
makeJSFunction fName fParams fDef =
JS $ format " function {}({}) { \ n {}} \ n "
( fName , T . intercalate " , " fParams , fDef )
-- This generates a function that takes arguments and produces a Javascript
-- function call
instance JSParams a => JSFunction ( a -> JS ) where
makeJSFunction fName _fParams _fDef = \ args ->
JS $ format " {}({}); "
( fName , T . intercalate " , " ( map fromJS ( jsParams args ) ) )
replaceWithData :: JSFunction a => a
replaceWithData =
makeJSFunction " replaceWithData " [ " node " ]
[ text |
return function ( data ) { $ ( node ) . replaceWith ( data ) ; } ;
| ]
2016-02-25 17:49:17 +03:00
prependData :: JSFunction a => a
prependData =
makeJSFunction " prependData " [ " node " ]
[ text |
return function ( data ) { $ ( node ) . prepend ( data ) ; } ;
| ]
2016-02-24 19:32:12 +03:00
appendData :: JSFunction a => a
appendData =
makeJSFunction " appendData " [ " node " ]
[ text |
return function ( data ) { $ ( node ) . append ( data ) ; } ;
| ]
-- | Move node up (in a list of sibling nodes), ignoring anchor elements
-- inserted by 'thisNode'.
moveNodeUp :: JSFunction a => a
moveNodeUp =
makeJSFunction " moveNodeUp " [ " node " ]
[ text |
var el = $ ( node ) ;
while ( el . prev () . is ( " .dummy " ) )
el . prev () . before ( el ) ;
if ( el . not ( ': first - child' ) )
el . prev () . before ( el ) ;
| ]
-- | Move node down (in a list of sibling nodes), ignoring anchor elements
-- inserted by 'thisNode'.
moveNodeDown :: JSFunction a => a
moveNodeDown =
makeJSFunction " moveNodeDown " [ " node " ]
[ text |
var el = $ ( node ) ;
while ( el . next () . is ( " .dummy " ) )
el . next () . after ( el ) ;
if ( el . not ( ': last - child' ) )
el . next () . after ( el ) ;
| ]
2016-03-07 21:50:53 +03:00
-- TODO: document the way hiding/showing works
-- | Given something that contains section divs (or spans), show one and
-- hide the rest. The div/span with the given @class@ will be chosen.
switchSection :: JSFunction a => a
switchSection =
makeJSFunction " switchSection " [ " node " , " section " ]
[ text |
$ ( node ) . children ( " .section " ) . removeClass ( " shown " ) ;
$ ( node ) . children ( " .section. " + section ) . addClass ( " shown " ) ;
| ]
2016-02-26 15:59:27 +03:00
showHelp :: JSFunction a => a
showHelp =
makeJSFunction " showHelp " [ " node " , " version " ]
[ text |
localStorage . removeItem ( " help-hidden- " + version ) ;
2016-03-07 21:50:53 +03:00
switchSection ( node , " expanded " ) ;
2016-02-26 15:59:27 +03:00
| ]
hideHelp :: JSFunction a => a
hideHelp =
makeJSFunction " hideHelp " [ " node " , " version " ]
[ text |
localStorage . setItem ( " help-hidden- " + version , " " ) ;
2016-03-07 21:50:53 +03:00
switchSection ( node , " collapsed " ) ;
2016-02-26 15:59:27 +03:00
| ]
2016-03-07 21:50:53 +03:00
-- TODO: find a better name for this (to distinguish it from 'showHelp' and
-- 'hideHelp')
2016-02-26 15:59:27 +03:00
showOrHideHelp :: JSFunction a => a
showOrHideHelp =
makeJSFunction " showOrHideHelp " [ " node " , " version " ]
[ text |
if ( localStorage . getItem ( " help-hidden- " + version ) === null )
2016-03-07 21:50:53 +03:00
showHelp ( node , version )
2016-02-26 15:59:27 +03:00
else
2016-03-07 21:50:53 +03:00
hideHelp ( node , version ) ;
2016-02-26 15:59:27 +03:00
| ]
2016-02-25 15:25:00 +03:00
search :: JSFunction a => a
search =
makeJSFunction " search " [ " node " , " s " ]
-- TODO: set address bar to “/?query=...” so that the user would be able to
-- see/share the search URL
[ text |
$. post ( " /search " , { query : s } )
. done ( replaceWithData ( node ) ) ;
| ]
2016-02-24 19:32:12 +03:00
-- | Create a new category.
addCategory :: JSFunction a => a
addCategory =
makeJSFunction " addCategory " [ " node " , " s " ]
[ text |
$. post ( " /add/category " , { content : s } )
2016-02-25 17:49:17 +03:00
. done ( prependData ( node ) ) ;
2016-02-24 19:32:12 +03:00
| ]
-- | Add a new library to some category.
addLibrary :: JSFunction a => a
addLibrary =
makeJSFunction " addLibrary " [ " node " , " catId " , " s " ]
[ text |
$. post ( " /add/category/ " + catId + " /library " , { name : s } )
. done ( appendData ( node ) ) ;
| ]
{- |
Finish category title editing ( this happens when you submit the field ) .
This turns the title with the editbox back into a simple text title .
- }
submitCategoryTitle :: JSFunction a => a
submitCategoryTitle =
makeJSFunction " submitCategoryTitle " [ " node " , " catId " , " s " ]
[ text |
$. post ( " /set/category/ " + catId + " /title " , { content : s } )
. done ( replaceWithData ( node ) ) ;
| ]
submitCategoryNotes :: JSFunction a => a
submitCategoryNotes =
makeJSFunction " submitCategoryNotes " [ " node " , " catId " , " s " ]
[ text |
$. post ( " /set/category/ " + catId + " /notes " , { content : s } )
. done ( replaceWithData ( node ) ) ;
| ]
2016-03-05 00:40:51 +03:00
submitItemNotes :: JSFunction a => a
submitItemNotes =
makeJSFunction " submitItemNotes " [ " node " , " itemId " , " s " ]
[ text |
$. post ( " /set/item/ " + itemId + " /notes " , { content : s } )
2016-03-08 13:32:56 +03:00
. done ( function ( data ) {
$ ( node ) . replaceWith ( data ) ;
switchSection ( node , " expanded " ) ;
} ) ;
// Switching has to be done here and not in 'Main . renderItemNotes'
// because $. post is asynchronous and will be done * after *
// switchSection has worked .
2016-03-05 00:40:51 +03:00
| ]
2016-02-24 19:32:12 +03:00
-- | Add a pro to some item.
addPro :: JSFunction a => a
addPro =
makeJSFunction " addPro " [ " node " , " itemId " , " s " ]
[ text |
$. post ( " /add/item/ " + itemId + " /pro " , { content : s } )
. done ( appendData ( node ) ) ;
| ]
-- | Add a con to some item.
addCon :: JSFunction a => a
addCon =
makeJSFunction " addCon " [ " node " , " itemId " , " s " ]
[ text |
$. post ( " /add/item/ " + itemId + " /con " , { content : s } )
. done ( appendData ( node ) ) ;
| ]
setItemTraitsMode :: JSFunction a => a
setItemTraitsMode =
makeJSFunction " setItemTraitsMode " [ " node " , " itemId " , " mode " ]
[ text |
$. get ( " /render/item/ " + itemId + " /traits " , { mode : mode } )
. done ( replaceWithData ( node ) ) ;
| ]
setTraitMode :: JSFunction a => a
setTraitMode =
makeJSFunction " setTraitMode " [ " node " , " itemId " , " traitId " , " mode " ]
[ text |
$. get ( " /render/item/ " + itemId + " /trait/ " + traitId , { mode : mode } )
. done ( replaceWithData ( node ) ) ;
| ]
submitTrait :: JSFunction a => a
submitTrait =
makeJSFunction " submitTrait " [ " node " , " itemId " , " traitId " , " s " ]
[ text |
$. post ( " /set/item/ " + itemId + " /trait/ " + traitId , { content : s } )
. done ( replaceWithData ( node ) ) ;
| ]
submitItemInfo :: JSFunction a => a
submitItemInfo =
2016-03-05 00:40:51 +03:00
makeJSFunction " submitItemInfo " [ " infoNode " , " otherNodes " , " itemId " , " form " ]
2016-02-24 19:32:12 +03:00
[ text |
2016-03-04 12:35:36 +03:00
// If the group was changed , we need to recolor the whole item ,
// but we don't want to rerender the item on the server because
// it would lose the item's state ( e . g . what if the traits were
// being edited ? etc ) . So , instead we query colors from the server
2016-03-05 00:40:51 +03:00
// and change the color of the other divs ( traits , notes , etc )
// manually .
2016-02-24 19:32:12 +03:00
$. post ( " /set/item/ " + itemId + " /info " , $ ( form ) . serialize () )
2016-03-04 12:35:36 +03:00
. done ( function ( data ) {
// Note the order – first we change the color , then we replace
2016-03-05 00:40:51 +03:00
// the info node . The reason is that otherwise the otherNodes
2016-03-04 12:35:36 +03:00
// selector might become invalid ( if it depends on the infoNode
// selector ) .
$. get ( " /render/item/ " + itemId + " /colors " )
. done ( function ( colors ) {
2016-03-05 00:40:51 +03:00
$ ( otherNodes ) . css ( " background-color " , colors . light ) ;
2016-03-04 12:35:36 +03:00
replaceWithData ( infoNode ) ( data ) ;
} ) ;
} ) ;
2016-02-24 19:32:12 +03:00
| ]
moveTraitUp :: JSFunction a => a
moveTraitUp =
makeJSFunction " moveTraitUp " [ " itemId " , " traitId " , " traitNode " ]
[ text |
$. post ( " /move/item/ " + itemId + " /trait/ " + traitId , { direction : " up " } ) ;
moveNodeUp ( traitNode ) ;
| ]
moveTraitDown :: JSFunction a => a
moveTraitDown =
makeJSFunction " moveTraitDown " [ " itemId " , " traitId " , " traitNode " ]
[ text |
$. post ( " /move/item/ " + itemId + " /trait/ " + traitId , { direction : " down " } ) ;
moveNodeDown ( traitNode ) ;
| ]
deleteTrait :: JSFunction a => a
deleteTrait =
2016-02-24 21:08:45 +03:00
makeJSFunction " deleteTrait " [ " itemId " , " traitId " , " traitNode " , " traitText " ]
2016-02-24 19:32:12 +03:00
[ text |
2016-02-24 21:08:45 +03:00
if ( confirm ( " Confirm deletion: “ " + traitText + " ” " ) ) {
$. post ( " /delete/item/ " + itemId + " /trait/ " + traitId ) ;
$ ( traitNode ) . remove () ;
}
2016-02-24 19:32:12 +03:00
| ]
2016-02-26 20:45:28 +03:00
moveItemUp :: JSFunction a => a
moveItemUp =
makeJSFunction " moveItemUp " [ " itemId " , " itemNode " ]
[ text |
$. post ( " /move/item/ " + itemId , { direction : " up " } ) ;
moveNodeUp ( itemNode ) ;
| ]
moveItemDown :: JSFunction a => a
moveItemDown =
makeJSFunction " moveItemDown " [ " itemId " , " itemNode " ]
[ text |
$. post ( " /move/item/ " + itemId , { direction : " down " } ) ;
moveNodeDown ( itemNode ) ;
| ]
deleteItem :: JSFunction a => a
deleteItem =
makeJSFunction " deleteItem " [ " itemId " , " itemNode " , " itemText " ]
[ text |
if ( confirm ( " Confirm deletion: “ " + itemText + " ” " ) ) {
$. post ( " /delete/item/ " + itemId ) ;
$ ( itemNode ) . remove () ;
}
| ]
2016-02-24 19:32:12 +03:00
-- When adding a function, don't forget to add it to 'allJSFunctions'!