{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving, OverloadedStrings, QuasiQuotes, BangPatterns, 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) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as B -- Formatting and interpolation import qualified Data.Text.Buildable as Format import NeatInterpolation -- Local import Utils -- | Javascript code. newtype JS = JS {fromJS :: Text} deriving (Show, Format.Buildable, Monoid) -- | A concatenation of all Javascript functions defined in this module. allJSFunctions :: JS allJSFunctions = JS . T.unlines . map fromJS $ [ -- Utilities replaceWithData, prependData, appendData, moveNodeUp, moveNodeDown, switchSection, switchSectionsEverywhere, fadeIn, fadeOutAndRemove, setMonospace, -- Help showOrHideHelp, showHelp, hideHelp, -- Misc createAjaxIndicator, autosizeTextarea, expandHash, expandItemNotes, -- Creating parts of interface makeTraitEditor, makeItemNotesEditor, -- Add methods addCategory, addItem, addPro, addCon, -- Set methods submitCategoryTitle, submitItemDescription, submitCategoryNotes, submitItemInfo, submitItemNotes, submitItemEcosystem, submitTrait, -- Other things deleteCategory, moveTraitUp, moveTraitDown, deleteTrait, moveItemUp, moveItemDown, deleteItem, -- Admin things acceptEdit, undoEdit, acceptBlock, undoBlock, createCheckpoint ] -- | 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 . escapeJSString instance ToJS Integer where toJS = JS . tshow instance ToJS Int where toJS = JS . tshow instance ToJS (Uid a) where toJS = toJS . uidToText -- | 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] instance (ToJS a,ToJS b,ToJS c,ToJS d,ToJS e) => JSParams (a,b,c,d,e) where jsParams (a,b,c,d,e) = [toJS a, toJS b, toJS c, toJS d, toJS e] instance (ToJS a,ToJS b,ToJS c,ToJS d,ToJS e,ToJS f) => JSParams (a,b,c,d,e,f) where jsParams (a,b,c,d,e,f) = [toJS a, toJS b, toJS c, toJS d, toJS e, toJS f] {- | 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) {\nreturn 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))) -- This isn't a standalone function and so it doesn't have to be listed in -- 'allJSFunctions'. assign :: ToJS x => JS -> x -> JS assign v x = JS $ format "{} = {};" (v, toJS x) -- TODO: all links here shouldn't be absolute [absolute-links] replaceWithData :: JSFunction a => a replaceWithData = makeJSFunction "replaceWithData" ["node"] [text| return function(data) {$(node).replaceWith(data);}; |] prependData :: JSFunction a => a prependData = makeJSFunction "prependData" ["node"] [text| return function(data) {$(node).prepend(data);}; |] 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); |] -- | Given something that contains section divs (or spans), show one and -- hide the rest. The div/span with the given @class@ will be chosen. -- -- See Note [show-hide] switchSection :: JSFunction a => a switchSection = makeJSFunction "switchSection" ["node", "section"] [text| $(node).children(".section").removeClass("shown"); $(node).children(".section."+section).addClass("shown"); // See Note [autosize] autosize($('textarea')); autosize.update($('textarea')); |] -- | Switch sections /everywhere/ inside the container. -- -- See Note [show-hide] switchSectionsEverywhere :: JSFunction a => a switchSectionsEverywhere = makeJSFunction "switchSectionsEverywhere" ["node", "section"] [text| $(node).find(".section").removeClass("shown"); $(node).find(".section."+section).addClass("shown"); // See Note [autosize] autosize($('textarea')); autosize.update($('textarea')); |] -- | This function makes the node half-transparent and then animates it to -- full opaqueness. It's useful when e.g. something has been moved and you -- want to “flash” the item to draw user's attention to it. fadeIn :: JSFunction a => a fadeIn = makeJSFunction "fadeIn" ["node"] [text| $(node).fadeTo(0,0.2).fadeTo(600,1); |] -- | This function animates the node to half-transparency and then removes it -- completely. It's useful when you're removing something and you want to -- draw user's attention to the fact that it's being removed. -- -- The reason there isn't a simple @fadeOut@ utility function here is that -- removal has to be done by passing a callback to @fadeTo@. In jQuery you -- can't simply wait until the animation has stopped. fadeOutAndRemove :: JSFunction a => a fadeOutAndRemove = makeJSFunction "fadeOutAndRemove" ["node"] [text| $(node).fadeTo(400,0.2,function(){$(node).remove()}); |] setMonospace :: JSFunction a => a setMonospace = makeJSFunction "setMonospace" ["node", "p"] [text| if (p) $(node).css("font-family", "monospace") else $(node).css("font-family", ""); // See Note [autosize]; the size of the textarea will definitely change // after the font has been changed autosize.update($(node)); |] showHelp :: JSFunction a => a showHelp = makeJSFunction "showHelp" ["node", "version"] [text| localStorage.removeItem("help-hidden-"+version); switchSection(node, "expanded"); |] hideHelp :: JSFunction a => a hideHelp = makeJSFunction "hideHelp" ["node", "version"] [text| localStorage.setItem("help-hidden-"+version, ""); switchSection(node, "collapsed"); |] -- TODO: find a better name for this (to distinguish it from 'showHelp' and -- 'hideHelp') showOrHideHelp :: JSFunction a => a showOrHideHelp = makeJSFunction "showOrHideHelp" ["node", "version"] [text| if (localStorage.getItem("help-hidden-"+version) === null) showHelp(node, version) else hideHelp(node, version); |] createAjaxIndicator :: JSFunction a => a createAjaxIndicator = makeJSFunction "createAjaxIndicator" [] [text| $("body").prepend('
'); $(document).ajaxStart(function() { $("#ajax-indicator").show(); }); $(document).ajaxStop(function() { $("#ajax-indicator").hide(); }); $("#ajax-indicator").hide(); |] autosizeTextarea :: JSFunction a => a autosizeTextarea = makeJSFunction "autosizeTextarea" ["textareaNode"] [text| autosize(textareaNode); autosize.update(textareaNode); |] -- | Read the anchor from the address bar (i.e. the thing after #) and use it -- to expand something (e.g. notes). It's needed to implement linking -- properly – e.g. notes are usually unexpanded, but when you're giving -- someone a direct link to notes, it makes sense to expand them. If you call -- 'expandHash' after the page has loaded, it will do just that. expandHash :: JSFunction a => a expandHash = makeJSFunction "expandHash" [] [text| hash = $(location).attr('hash'); if (hash.slice(0,12) == "#item-notes-") { itemId = hash.slice(12); expandItemNotes(itemId); } else if (hash.slice(0,6) == "#item-") { itemId = hash.slice(6); expandItemNotes(itemId); } |] expandItemNotes :: JSFunction a => a expandItemNotes = makeJSFunction "expandItemNotes" ["itemId"] [text| switchSection("#item-notes-"+itemId, "expanded"); |] makeTraitEditor :: JSFunction a => a makeTraitEditor = makeJSFunction "makeTraitEditor" ["traitNode", "sectionNode", "textareaUid", "content", "itemId", "traitId"] [text| $(sectionNode).html(""); area = $("