2016-03-15 15:35:35 +03:00
|
|
|
|
{-# LANGUAGE
|
|
|
|
|
QuasiQuotes,
|
|
|
|
|
OverloadedStrings,
|
|
|
|
|
FlexibleContexts,
|
2016-03-16 02:17:08 +03:00
|
|
|
|
ViewPatterns,
|
2016-03-15 15:35:35 +03:00
|
|
|
|
NoImplicitPrelude
|
|
|
|
|
#-}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module View
|
|
|
|
|
(
|
|
|
|
|
-- * Pages
|
|
|
|
|
renderRoot,
|
|
|
|
|
renderDonate,
|
2016-03-19 02:40:00 +03:00
|
|
|
|
renderCategoryPage,
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
-- * Tracking
|
|
|
|
|
renderTracking,
|
|
|
|
|
|
|
|
|
|
-- * Methods
|
|
|
|
|
renderHelp,
|
|
|
|
|
-- ** Categories
|
|
|
|
|
renderCategoryList,
|
|
|
|
|
renderCategory,
|
|
|
|
|
renderCategoryTitle,
|
|
|
|
|
renderCategoryNotes,
|
|
|
|
|
-- ** Items
|
|
|
|
|
renderItem,
|
|
|
|
|
renderItemInfo,
|
|
|
|
|
renderItemDescription,
|
2016-03-17 02:52:40 +03:00
|
|
|
|
renderItemEcosystem,
|
2016-03-15 15:35:35 +03:00
|
|
|
|
renderItemTraits,
|
|
|
|
|
renderItemNotes,
|
|
|
|
|
-- ** Traits
|
|
|
|
|
renderTrait,
|
|
|
|
|
|
|
|
|
|
-- * Miscellaneous
|
|
|
|
|
getItemHue,
|
|
|
|
|
newGroupValue,
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- General
|
|
|
|
|
import BasePrelude hiding (Category)
|
|
|
|
|
-- Lenses
|
|
|
|
|
import Lens.Micro.Platform hiding ((&))
|
|
|
|
|
-- Monads and monad transformers
|
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
|
-- Containers
|
|
|
|
|
import qualified Data.Map as M
|
|
|
|
|
-- Text
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import qualified Data.Text.IO as T
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import NeatInterpolation
|
|
|
|
|
-- Web
|
|
|
|
|
import Lucid hiding (for_)
|
|
|
|
|
|
|
|
|
|
-- Local
|
|
|
|
|
import Types
|
|
|
|
|
import Utils
|
|
|
|
|
import JS (JS(..), JQuerySelector)
|
|
|
|
|
import qualified JS
|
|
|
|
|
import Markdown
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{- Note [autosize]
|
|
|
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
All textareas on the site are autosized – i.e. they grow when the user is typing. This is done by the autosize.js plugin, which is called on page load:
|
|
|
|
|
|
|
|
|
|
autosize($('textarea'));
|
|
|
|
|
|
|
|
|
|
A slight problem is that it doesn't compute the height of hidden elements correctly – thus, when something is shown and it happens to be a textarea or contain a textarea, we have to call autosize again. This is done in 'JS.switchSection'. So far there are no textboxes that are shown *without* switchSection being involved, and so there's no need to watch for elements being added to the DOM.
|
|
|
|
|
|
|
|
|
|
It would be nicer if we could watch for elements becoming visible without having to modify switchSection, but there doesn't seem to be an easy way to do this – MutationObserver doesn't let us find out when something becomes visible (i.e. when its clientHeight stops being 0).
|
|
|
|
|
|
|
|
|
|
In switchSection we use
|
|
|
|
|
|
|
|
|
|
autosize($('textarea'));
|
|
|
|
|
autosize.update($('textarea'));
|
|
|
|
|
|
|
|
|
|
instead of simple
|
|
|
|
|
|
|
|
|
|
autosize.update($('textarea'));
|
|
|
|
|
|
|
|
|
|
– this is done because the textarea could have appeared after the original `autosize($('textarea'));` was called on page load (which could happen if an item was added, for instance).
|
|
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
renderRoot :: GlobalState -> Maybe Text -> HtmlT IO ()
|
2016-03-19 02:40:00 +03:00
|
|
|
|
renderRoot globalState mbSearchQuery =
|
|
|
|
|
wrapPage "Aelve Guide" $ do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
renderHelp
|
|
|
|
|
onPageLoad $ JS.showOrHideHelp (JS.selectId "help", helpVersion)
|
|
|
|
|
form_ $ do
|
|
|
|
|
input_ [type_ "text", name_ "q", id_ "search", placeholder_ "search",
|
|
|
|
|
value_ (fromMaybe "" mbSearchQuery)]
|
|
|
|
|
textInput [
|
|
|
|
|
placeholder_ "add a category",
|
|
|
|
|
autocomplete_ "off",
|
|
|
|
|
onEnter $ JS.addCategory (JS.selectId "categories", inputValue) <>
|
|
|
|
|
clearInput ]
|
|
|
|
|
-- TODO: sort categories by popularity, somehow? or provide a list of
|
|
|
|
|
-- “commonly used categories” or even a nested catalog
|
|
|
|
|
case mbSearchQuery of
|
|
|
|
|
Nothing -> renderCategoryList (globalState^.categories)
|
|
|
|
|
Just query' -> do
|
|
|
|
|
let queryWords = T.words query'
|
|
|
|
|
let rank :: Category -> Int
|
|
|
|
|
rank cat = sum [
|
|
|
|
|
length (queryWords `intersect` (cat^..items.each.name)),
|
|
|
|
|
length (queryWords `intersect` T.words (cat^.title)) ]
|
|
|
|
|
let rankedCategories
|
|
|
|
|
| null queryWords = globalState^.categories
|
|
|
|
|
| otherwise = filter ((/= 0) . rank) .
|
|
|
|
|
reverse . sortOn rank
|
|
|
|
|
$ globalState^.categories
|
|
|
|
|
renderCategoryList rankedCategories
|
|
|
|
|
-- TODO: maybe add a button like “give me random category that is
|
|
|
|
|
-- unfinished”
|
|
|
|
|
|
|
|
|
|
-- TODO: when submitting a text field, gray it out (but leave it selectable)
|
|
|
|
|
-- until it's been submitted
|
|
|
|
|
|
|
|
|
|
renderTracking :: HtmlT IO ()
|
|
|
|
|
renderTracking = do
|
2016-03-16 23:47:59 +03:00
|
|
|
|
trackingEnabled <- (== Just "1") <$> liftIO (lookupEnv "GUIDE_TRACKING")
|
|
|
|
|
when trackingEnabled $ do
|
|
|
|
|
tracking <- liftIO $ T.readFile "static/tracking.html"
|
|
|
|
|
toHtmlRaw tracking
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
-- TODO: include jQuery locally so that it'd be possible to test the site
|
|
|
|
|
-- without internet
|
|
|
|
|
|
|
|
|
|
renderDonate :: HtmlT IO ()
|
|
|
|
|
renderDonate = doctypehtml_ $ do
|
|
|
|
|
head_ $ do
|
|
|
|
|
title_ "Donate to Artyom"
|
|
|
|
|
includeCSS "/css.css"
|
|
|
|
|
renderTracking
|
|
|
|
|
|
2016-03-17 01:02:37 +03:00
|
|
|
|
body_ $
|
|
|
|
|
toHtmlRaw =<< liftIO (readFile "static/donate.html")
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
2016-03-19 02:40:00 +03:00
|
|
|
|
-- Include all the necessary things
|
|
|
|
|
wrapPage :: Text -> HtmlT IO () -> HtmlT IO ()
|
|
|
|
|
wrapPage pageTitle page = doctypehtml_ $ do
|
|
|
|
|
head_ $ do
|
|
|
|
|
title_ (toHtml pageTitle)
|
2016-03-19 03:55:31 +03:00
|
|
|
|
meta_ [name_ "viewport",
|
|
|
|
|
content_ "width=device-width, initial-scale=1.0, user-scalable=yes"]
|
2016-03-19 02:40:00 +03:00
|
|
|
|
let cdnjs = "https://cdnjs.cloudflare.com/ajax/libs/"
|
|
|
|
|
includeJS (cdnjs <> "jquery/2.2.0/jquery.min.js")
|
|
|
|
|
-- See Note [autosize]
|
|
|
|
|
includeJS (cdnjs <> "autosize.js/3.0.15/autosize.min.js")
|
|
|
|
|
onPageLoad (JS "autosize($('textarea'));")
|
|
|
|
|
-- It's important that css.css comes second – it overwrites some rules
|
|
|
|
|
-- from highlight.css (see the rule for div.sourceCode)
|
|
|
|
|
includeCSS "/highlight.css"
|
|
|
|
|
includeCSS "/css.css"
|
|
|
|
|
-- Include definitions of all Javascript functions that we have defined
|
|
|
|
|
-- in this file. (This isn't an actual file, so don't look for it in the
|
|
|
|
|
-- static folder – it's generated and served in 'otherMethods'.)
|
|
|
|
|
includeJS "/js.js"
|
|
|
|
|
renderTracking
|
|
|
|
|
-- CSS that makes 'shown' and 'noScriptShown' work
|
|
|
|
|
noscript_ $ style_ [text|
|
|
|
|
|
.section:not(.noscript-shown) {display:none;}
|
|
|
|
|
|]
|
|
|
|
|
script_ [text|
|
|
|
|
|
var sheet = document.createElement('style');
|
|
|
|
|
sheet.innerHTML = '.section:not(.shown) {display:none;}';
|
|
|
|
|
// “head” instead of “body” because body isn't loaded yet
|
|
|
|
|
document.head.appendChild(sheet);
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
body_ $ do
|
|
|
|
|
-- TODO: [very-easy] this header looks bad when the page is narrow, it
|
|
|
|
|
-- should be fixed in css.css by adding line-height to it
|
|
|
|
|
h1_ "The Haskeller's guide"
|
|
|
|
|
noscript_ $ div_ [id_ "noscript-message"] $
|
|
|
|
|
toHtml $ renderMarkdownBlock [text|
|
|
|
|
|
You have Javascript disabled! This site works fine without
|
|
|
|
|
Javascript, but since all editing needs Javascript to work,
|
|
|
|
|
you won't be able to edit anything.
|
|
|
|
|
|]
|
|
|
|
|
|
|
|
|
|
page
|
|
|
|
|
|
|
|
|
|
div_ [id_ "footer"] $ do
|
|
|
|
|
"made by " >> a_ [href_ "https://artyom.me"] "Artyom"
|
|
|
|
|
emptySpan "2em"
|
|
|
|
|
a_ [href_ "https://github.com/aelve/guide"] "source"
|
|
|
|
|
emptySpan "2em"
|
|
|
|
|
a_ [href_ "https://github.com/aelve/guide/issues"] "report an issue"
|
|
|
|
|
emptySpan "2em"
|
|
|
|
|
a_ [href_ "/donate"] "donate"
|
|
|
|
|
sup_ [style_ "font-size:50%"] "I don't have a job"
|
|
|
|
|
|
|
|
|
|
renderCategoryPage :: Category -> HtmlT IO ()
|
|
|
|
|
renderCategoryPage category =
|
|
|
|
|
wrapPage ("Aelve Guide – " <> category^.title) $ do
|
|
|
|
|
renderCategory category
|
|
|
|
|
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- TODO: allow archiving items if they are in every way worse than the rest,
|
|
|
|
|
-- or something (but searching should still be possible)
|
|
|
|
|
|
|
|
|
|
-- TODO: add a list for “interesting libraries, but too lazy to describe, so
|
|
|
|
|
-- somebody describe them for me”
|
|
|
|
|
|
|
|
|
|
renderHelp :: HtmlT IO ()
|
|
|
|
|
renderHelp = do
|
|
|
|
|
div_ [id_ "help"] $ do
|
|
|
|
|
|
|
|
|
|
-- If you're going to change section names, look at 'JS.showHelp' and
|
|
|
|
|
-- 'JS.hideHelp'
|
|
|
|
|
section "collapsed" [shown] $ do
|
|
|
|
|
textButton "show help" $
|
|
|
|
|
JS.showHelp (JS.selectId "help", helpVersion)
|
|
|
|
|
|
|
|
|
|
section "expanded" [noScriptShown] $ do
|
|
|
|
|
textButton "hide help" $
|
|
|
|
|
JS.hideHelp (JS.selectId "help", helpVersion)
|
|
|
|
|
-- Don't forget to change 'helpVersion' when the text changes
|
|
|
|
|
-- substantially and you think the users should reread it
|
|
|
|
|
help <- liftIO $ T.readFile "static/help.md"
|
2016-03-16 02:17:08 +03:00
|
|
|
|
toHtml $ renderMarkdownBlock help
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
helpVersion :: Int
|
|
|
|
|
helpVersion = 2
|
|
|
|
|
|
|
|
|
|
-- TODO: when conflicts happen, maybe create an alert like “The thing you're
|
|
|
|
|
-- editing has been edited in the meantime. Here is a link with a diff of
|
|
|
|
|
-- your variant and the other person's variant. Please merge the changes
|
|
|
|
|
-- manually and submit them again, or press this button and we'll merge the
|
|
|
|
|
-- changes for you (don't worry, it's not a big deal for us). Thanks!”
|
|
|
|
|
|
|
|
|
|
-- TODO: automatic merge should be possible too (e.g. if the changes are in
|
|
|
|
|
-- different paragraphs)
|
|
|
|
|
|
|
|
|
|
renderCategoryList :: [Category] -> HtmlT IO ()
|
|
|
|
|
renderCategoryList cats =
|
|
|
|
|
div_ [id_ "categories"] $
|
|
|
|
|
mapM_ renderCategory cats
|
|
|
|
|
|
|
|
|
|
renderCategoryTitle :: Category -> HtmlT IO ()
|
|
|
|
|
renderCategoryTitle category = do
|
|
|
|
|
let thisId = "category-title-" <> uidToText (category^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
|
|
|
|
h2_ [id_ thisId] $ do
|
|
|
|
|
sectionSpan "normal" [shown, noScriptShown] $ do
|
2016-03-19 02:40:00 +03:00
|
|
|
|
let slug = makeSlug (category^.title)
|
|
|
|
|
a_ [href_ (format "/{}-{}" (slug, category^.uid))] $
|
|
|
|
|
toHtml (category^.title)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
emptySpan "1em"
|
|
|
|
|
textButton "edit" $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
|
|
|
|
|
sectionSpan "editing" [] $ do
|
|
|
|
|
textInput [
|
|
|
|
|
value_ (category^.title),
|
|
|
|
|
autocomplete_ "off",
|
|
|
|
|
onEnter $
|
|
|
|
|
JS.submitCategoryTitle (this, category^.uid, inputValue)]
|
|
|
|
|
emptySpan "1em"
|
|
|
|
|
textButton "cancel" $
|
|
|
|
|
JS.switchSection (this, "normal" :: Text)
|
|
|
|
|
|
|
|
|
|
renderCategoryNotes :: Category -> HtmlT IO ()
|
|
|
|
|
renderCategoryNotes category = do
|
|
|
|
|
let thisId = "category-notes-" <> uidToText (category^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
|
|
|
|
div_ [id_ thisId] $ do
|
|
|
|
|
|
|
|
|
|
section "normal" [shown, noScriptShown] $ do
|
2016-03-16 02:17:08 +03:00
|
|
|
|
if category^.notes == ""
|
2016-03-15 15:35:35 +03:00
|
|
|
|
then p_ "write something here!"
|
2016-03-16 02:17:08 +03:00
|
|
|
|
else toHtml (category^.notes)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
textButton "edit description" $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
|
|
|
|
|
section "editing" [] $
|
|
|
|
|
markdownEditor
|
2016-03-17 15:29:45 +03:00
|
|
|
|
[rows_ "10"]
|
2016-03-15 15:35:35 +03:00
|
|
|
|
(category^.notes)
|
|
|
|
|
(\val -> JS.submitCategoryNotes (this, category^.uid, val))
|
|
|
|
|
(JS.switchSection (this, "normal" :: Text))
|
|
|
|
|
|
|
|
|
|
renderCategory :: Category -> HtmlT IO ()
|
|
|
|
|
renderCategory category =
|
|
|
|
|
div_ [class_ "category", uid_ (category^.uid)] $ do
|
|
|
|
|
renderCategoryTitle category
|
|
|
|
|
renderCategoryNotes category
|
|
|
|
|
itemsNode <- div_ [class_ "items"] $ do
|
|
|
|
|
mapM_ (renderItem category) (category^.items)
|
|
|
|
|
thisNode
|
|
|
|
|
textInput [
|
|
|
|
|
placeholder_ "add an item",
|
|
|
|
|
autocomplete_ "off",
|
|
|
|
|
onEnter $ JS.addItem (itemsNode, category^.uid, inputValue) <>
|
|
|
|
|
clearInput ]
|
|
|
|
|
|
|
|
|
|
getItemHue :: Category -> Item -> Hue
|
|
|
|
|
getItemHue category item = case item^.group_ of
|
|
|
|
|
Nothing -> NoHue
|
|
|
|
|
Just s -> M.findWithDefault NoHue s (category^.groups)
|
|
|
|
|
|
|
|
|
|
-- TODO: perhaps use jQuery Touch Punch or something to allow dragging items
|
|
|
|
|
-- instead of using arrows? Touch Punch works on mobile, too
|
|
|
|
|
renderItem :: Category -> Item -> HtmlT IO ()
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItem category item =
|
2016-03-15 15:35:35 +03:00
|
|
|
|
div_ [id_ ("item-" <> uidToText (item^.uid)), class_ "item"] $ do
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItemInfo category item
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- TODO: replace “edit description” with a big half-transparent pencil
|
|
|
|
|
-- to the left of it
|
2016-03-17 03:35:56 +03:00
|
|
|
|
let bg = hueToLightColor $ getItemHue category item
|
|
|
|
|
div_ [class_ "item-body", style_ ("background-color:" <> bg)] $ do
|
|
|
|
|
renderItemDescription item
|
|
|
|
|
renderItemTraits item
|
2016-03-17 14:58:44 +03:00
|
|
|
|
renderItemEcosystem item
|
2016-03-17 03:35:56 +03:00
|
|
|
|
-- TODO: add a separator here? [very-easy]
|
|
|
|
|
renderItemNotes item
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
-- TODO: some spinning thingy that spins in the corner of the page while a
|
|
|
|
|
-- request is happening
|
|
|
|
|
|
|
|
|
|
-- TODO: find some way to give all functions access to category and item (or
|
|
|
|
|
-- category, item and trait) without passing everything explicitly?
|
|
|
|
|
|
|
|
|
|
-- TODO: warn when a library isn't on Hackage but is supposed to be
|
2016-03-17 03:35:56 +03:00
|
|
|
|
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- TODO: give a link to oldest available docs when the new docs aren't there
|
|
|
|
|
renderItemInfo :: Category -> Item -> HtmlT IO ()
|
|
|
|
|
renderItemInfo cat item = do
|
|
|
|
|
let bg = hueToDarkColor $ getItemHue cat item
|
|
|
|
|
let thisId = "item-info-" <> uidToText (item^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
|
|
|
|
div_ [id_ thisId, class_ "item-info",
|
|
|
|
|
style_ ("background-color:" <> bg)] $ do
|
|
|
|
|
|
|
|
|
|
section "normal" [shown, noScriptShown] $ do
|
|
|
|
|
-- TODO: [very-easy] move this style_ into css.css
|
|
|
|
|
span_ [style_ "font-size:150%"] $ do
|
|
|
|
|
let hackageLink x = "https://hackage.haskell.org/package/" <> x
|
|
|
|
|
case item^.kind of
|
|
|
|
|
-- If the library is on Hackage, the title links to its Hackage
|
|
|
|
|
-- page; otherwise, it doesn't link anywhere. Even if the link
|
|
|
|
|
-- field is present, it's going to be rendered as “(site)”, not
|
|
|
|
|
-- linked in the title.
|
|
|
|
|
Library hackageName' -> do
|
|
|
|
|
case hackageName' of
|
|
|
|
|
Just x -> a_ [href_ (hackageLink x)] (toHtml (item^.name))
|
|
|
|
|
Nothing -> toHtml (item^.name)
|
|
|
|
|
case item^.link of
|
|
|
|
|
Just l -> " (" >> a_ [href_ l] "site" >> ")"
|
|
|
|
|
Nothing -> return ()
|
|
|
|
|
-- For tools, it's the opposite – the title links to the item site
|
|
|
|
|
-- (if present), and there's a separate “(Hackage)” link if the
|
|
|
|
|
-- tool is on Hackage.
|
|
|
|
|
Tool hackageName' -> do
|
|
|
|
|
case item^.link of
|
|
|
|
|
Just l -> a_ [href_ l] (toHtml (item^.name))
|
|
|
|
|
Nothing -> toHtml (item^.name)
|
|
|
|
|
case hackageName' of
|
|
|
|
|
Just x -> " (" >> a_ [href_ (hackageLink x)] "Hackage" >> ")"
|
|
|
|
|
Nothing -> return ()
|
|
|
|
|
-- And now everything else
|
|
|
|
|
Other -> do
|
|
|
|
|
case item^.link of
|
|
|
|
|
Just l -> a_ [href_ l] (toHtml (item^.name))
|
|
|
|
|
Nothing -> toHtml (item^.name)
|
|
|
|
|
emptySpan "2em"
|
|
|
|
|
toHtml (fromMaybe "other" (item^.group_))
|
|
|
|
|
span_ [class_ "controls"] $ do
|
|
|
|
|
let itemNode = JS.selectId ("item-" <> uidToText (item^.uid))
|
|
|
|
|
imgButton "move item up" "/arrow-thick-top.svg" [] $
|
|
|
|
|
-- TODO: [easy] the item should blink or somehow else show where it
|
|
|
|
|
-- has been moved
|
|
|
|
|
JS.moveItemUp (item^.uid, itemNode)
|
|
|
|
|
imgButton "move item down" "/arrow-thick-bottom.svg" [] $
|
|
|
|
|
JS.moveItemDown (item^.uid, itemNode)
|
|
|
|
|
emptySpan "1.5em"
|
|
|
|
|
imgButton "edit item info" "/pencil.svg" [] $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
emptySpan "0.5em"
|
|
|
|
|
imgButton "delete item" "/x.svg" [] $
|
|
|
|
|
JS.deleteItem (item^.uid, itemNode)
|
|
|
|
|
-- TODO: link to Stackage too
|
|
|
|
|
-- TODO: should check for Stackage automatically
|
|
|
|
|
|
|
|
|
|
section "editing" [] $ do
|
|
|
|
|
let selectedIf p x = if p then with x [selected_ "selected"] else x
|
2016-03-17 03:35:56 +03:00
|
|
|
|
-- When the info/header node changes its group (and is hence
|
|
|
|
|
-- recolored), item's body has to be recolored too
|
|
|
|
|
let bodyNode = JS.selectChildren (JS.selectParent this)
|
|
|
|
|
(JS.selectClass "item-body")
|
2016-03-15 15:35:35 +03:00
|
|
|
|
let formSubmitHandler formNode =
|
2016-03-17 03:35:56 +03:00
|
|
|
|
JS.submitItemInfo (this, bodyNode, item^.uid, formNode)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
form_ [onFormSubmit formSubmitHandler] $ do
|
|
|
|
|
-- All inputs have "autocomplete = off" thanks to
|
|
|
|
|
-- <http://stackoverflow.com/q/8311455>
|
|
|
|
|
label_ $ do
|
|
|
|
|
"Name" >> br_ []
|
|
|
|
|
input_ [type_ "text", name_ "name",
|
|
|
|
|
autocomplete_ "off",
|
|
|
|
|
value_ (item^.name)]
|
|
|
|
|
br_ []
|
|
|
|
|
label_ $ do
|
|
|
|
|
"Kind" >> br_ []
|
|
|
|
|
select_ [name_ "kind"] $ do
|
|
|
|
|
option_ [value_ "library"] "Library"
|
|
|
|
|
& selectedIf (case item^.kind of Library{} -> True; _ -> False)
|
|
|
|
|
option_ [value_ "tool"] "Tool"
|
|
|
|
|
& selectedIf (case item^.kind of Tool{} -> True; _ -> False)
|
|
|
|
|
option_ [value_ "other"] "Other"
|
|
|
|
|
& selectedIf (case item^.kind of Other{} -> True; _ -> False)
|
|
|
|
|
br_ []
|
|
|
|
|
label_ $ do
|
|
|
|
|
"Name on Hackage" >> br_ []
|
|
|
|
|
input_ [type_ "text", name_ "hackage-name", autocomplete_ "off",
|
|
|
|
|
value_ (fromMaybe "" (item^?kind.hackageName._Just))]
|
|
|
|
|
br_ []
|
|
|
|
|
label_ $ do
|
|
|
|
|
"Site (optional)" >> br_ []
|
|
|
|
|
input_ [type_ "text", name_ "link", autocomplete_ "off",
|
|
|
|
|
value_ (fromMaybe "" (item^.link))]
|
|
|
|
|
br_ []
|
2016-03-19 00:05:01 +03:00
|
|
|
|
newGroupInputId <- randomLongUid
|
2016-03-15 15:35:35 +03:00
|
|
|
|
label_ $ do
|
|
|
|
|
"Group" >> br_ []
|
|
|
|
|
-- When “new group” is selected in the list, we show a field for
|
|
|
|
|
-- entering new group's name
|
|
|
|
|
--
|
|
|
|
|
-- TODO: when a new group is created, add it to all other lists in
|
|
|
|
|
-- forms in the category
|
|
|
|
|
let selectHandler = [text|
|
|
|
|
|
if (this.value == "$newGroupValue") {
|
|
|
|
|
$("#$idText").show();
|
|
|
|
|
$("#$idText").focus(); }
|
|
|
|
|
else $("#$idText").hide(); |]
|
|
|
|
|
where idText = uidToText newGroupInputId
|
|
|
|
|
select_ [name_ "group", autocomplete_ "off",
|
|
|
|
|
onchange_ selectHandler] $ do
|
|
|
|
|
let gs = Nothing : map Just (M.keys (cat^.groups))
|
|
|
|
|
for_ gs $ \group' -> do
|
|
|
|
|
-- Text that will be shown in the list (“-” stands for “no
|
|
|
|
|
-- group”)
|
|
|
|
|
let txt = fromMaybe "-" group'
|
|
|
|
|
-- If the element corresponds to the current group of the
|
|
|
|
|
-- item (or the element is “-”, i.e. Nothing, and the group
|
|
|
|
|
-- is Nothing too), mark it as selected, thus making it the
|
|
|
|
|
-- element that will be chosen by default when the form is
|
|
|
|
|
-- rendered
|
|
|
|
|
option_ [value_ txt] (toHtml txt)
|
|
|
|
|
& selectedIf (group' == item^.group_)
|
|
|
|
|
option_ [value_ newGroupValue] "New group..."
|
|
|
|
|
input_ [uid_ newGroupInputId, type_ "text", autocomplete_ "off",
|
|
|
|
|
name_ "custom-group", hidden_ "hidden"]
|
|
|
|
|
br_ []
|
|
|
|
|
input_ [type_ "submit", value_ "Save"]
|
|
|
|
|
button "Cancel" [] $
|
|
|
|
|
JS.switchSection (this, "normal" :: Text)
|
|
|
|
|
|
|
|
|
|
-- TODO: use triangle icons instead of arrows [very-easy]
|
|
|
|
|
|
|
|
|
|
-- TODO: categories that don't directly compare libraries but just list all
|
|
|
|
|
-- libraries about something (e.g. Yesod plugins, or whatever)
|
|
|
|
|
|
|
|
|
|
-- TODO: categories without items (e.g. “web dev”) that list links to other
|
|
|
|
|
-- categories
|
|
|
|
|
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItemDescription :: Item -> HtmlT IO ()
|
|
|
|
|
renderItemDescription item = do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
let thisId = "item-description-" <> uidToText (item^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
2016-03-17 03:35:56 +03:00
|
|
|
|
div_ [id_ thisId, class_ "item-description"] $ do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
section "normal" [shown, noScriptShown] $ do
|
2016-03-16 02:17:08 +03:00
|
|
|
|
if item^.description == ""
|
2016-03-15 15:35:35 +03:00
|
|
|
|
then p_ "write something here!"
|
2016-03-16 02:17:08 +03:00
|
|
|
|
else toHtml (item^.description)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
textButton "edit description" $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
|
|
|
|
|
section "editing" [] $
|
|
|
|
|
markdownEditor
|
2016-03-17 15:29:45 +03:00
|
|
|
|
[rows_ "10"]
|
2016-03-15 15:35:35 +03:00
|
|
|
|
(item^.description)
|
|
|
|
|
(\val -> JS.submitItemDescription (this, item^.uid, val))
|
|
|
|
|
(JS.switchSection (this, "normal" :: Text))
|
|
|
|
|
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItemEcosystem :: Item -> HtmlT IO ()
|
|
|
|
|
renderItemEcosystem item = do
|
2016-03-17 02:52:40 +03:00
|
|
|
|
let thisId = "item-ecosystem-" <> uidToText (item^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
2016-03-17 03:35:56 +03:00
|
|
|
|
div_ [id_ thisId, class_ "item-ecosystem"] $ do
|
2016-03-17 02:52:40 +03:00
|
|
|
|
strong_ "Ecosystem"
|
|
|
|
|
emptySpan "0.5em"
|
|
|
|
|
imgButton "edit ecosystem" "/pencil.svg"
|
|
|
|
|
[style_ "width:12px;opacity:0.5"] $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
|
|
|
|
|
section "normal" [shown, noScriptShown] $ do
|
2016-03-17 14:52:32 +03:00
|
|
|
|
unless (item^.ecosystem == "") $
|
2016-03-17 15:29:45 +03:00
|
|
|
|
toHtml (item^.ecosystem)
|
2016-03-17 02:52:40 +03:00
|
|
|
|
|
|
|
|
|
section "editing" [] $
|
2016-03-17 15:29:45 +03:00
|
|
|
|
markdownEditor
|
2016-03-17 02:52:40 +03:00
|
|
|
|
[rows_ "3"]
|
|
|
|
|
(item^.ecosystem)
|
|
|
|
|
(\val -> JS.submitItemEcosystem (this, item^.uid, val))
|
2016-03-17 15:29:45 +03:00
|
|
|
|
(JS.switchSection (this, "normal" :: Text))
|
2016-03-17 02:52:40 +03:00
|
|
|
|
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItemTraits :: Item -> HtmlT IO ()
|
|
|
|
|
renderItemTraits item = do
|
|
|
|
|
div_ [class_ "item-traits"] $ do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
this <- thisNode
|
|
|
|
|
div_ [class_ "traits-groups-container"] $ do
|
|
|
|
|
div_ [class_ "traits-group"] $ do
|
2016-03-17 03:02:51 +03:00
|
|
|
|
strong_ "Pros"
|
2016-03-16 15:46:29 +03:00
|
|
|
|
-- We can't use 'thisNode' inside <ul> because it creates a <span>
|
|
|
|
|
-- and only <li> elements can be children of <ul>
|
2016-03-19 00:05:01 +03:00
|
|
|
|
listUid <- randomLongUid
|
2016-03-16 15:46:29 +03:00
|
|
|
|
ul_ [uid_ listUid] $
|
2016-03-15 15:35:35 +03:00
|
|
|
|
mapM_ (renderTrait (item^.uid)) (item^.pros)
|
|
|
|
|
section "editable" [] $
|
|
|
|
|
smallMarkdownEditor
|
|
|
|
|
[rows_ "3", placeholder_ "add pro"]
|
|
|
|
|
""
|
2016-03-16 15:46:29 +03:00
|
|
|
|
(\val -> JS.addPro (JS.selectUid listUid, item^.uid, val) <>
|
2016-03-15 15:35:35 +03:00
|
|
|
|
JS.assign val ("" :: Text))
|
|
|
|
|
Nothing
|
2016-03-17 03:02:51 +03:00
|
|
|
|
div_ (emptySpan "1em")
|
2016-03-15 15:35:35 +03:00
|
|
|
|
div_ [class_ "traits-group"] $ do
|
2016-03-17 03:02:51 +03:00
|
|
|
|
strong_ "Cons"
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- TODO: [easy] maybe add a line here?
|
2016-03-19 00:05:01 +03:00
|
|
|
|
listUid <- randomLongUid
|
2016-03-16 15:46:29 +03:00
|
|
|
|
ul_ [uid_ listUid] $
|
2016-03-15 15:35:35 +03:00
|
|
|
|
mapM_ (renderTrait (item^.uid)) (item^.cons)
|
|
|
|
|
section "editable" [] $
|
|
|
|
|
smallMarkdownEditor
|
|
|
|
|
[rows_ "3", placeholder_ "add con"]
|
|
|
|
|
""
|
2016-03-16 15:46:29 +03:00
|
|
|
|
(\val -> JS.addCon (JS.selectUid listUid, item^.uid, val) <>
|
2016-03-15 15:35:35 +03:00
|
|
|
|
JS.assign val ("" :: Text))
|
|
|
|
|
Nothing
|
|
|
|
|
section "normal" [shown, noScriptShown] $ do
|
|
|
|
|
textButton "edit pros/cons" $
|
|
|
|
|
JS.switchSectionsEverywhere(this, "editable" :: Text)
|
|
|
|
|
section "editable" [] $ do
|
|
|
|
|
textButton "edit off" $
|
|
|
|
|
JS.switchSectionsEverywhere(this, "normal" :: Text)
|
|
|
|
|
|
|
|
|
|
renderTrait :: Uid -> Trait -> HtmlT IO ()
|
|
|
|
|
-- TODO: probably use renderMarkdownBlock here as well
|
|
|
|
|
renderTrait itemId trait = do
|
|
|
|
|
let thisId = "trait-" <> uidToText (trait^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
|
|
|
|
li_ [id_ thisId] $ do
|
|
|
|
|
|
|
|
|
|
sectionSpan "normal" [shown, noScriptShown] $ do
|
2016-03-16 02:17:08 +03:00
|
|
|
|
toHtml (trait^.content)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
|
|
|
|
|
section "editable" [] $ do
|
2016-03-16 02:17:08 +03:00
|
|
|
|
toHtml (trait^.content)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
br_ []
|
|
|
|
|
imgButton "move trait up" "/arrow-thick-top.svg" [width_ "12"] $
|
|
|
|
|
JS.moveTraitUp (itemId, trait^.uid, this)
|
|
|
|
|
imgButton "move trait down" "/arrow-thick-bottom.svg" [width_ "12"] $
|
|
|
|
|
JS.moveTraitDown (itemId, trait^.uid, this)
|
|
|
|
|
-- TODO: these 3 icons in a row don't look nice
|
|
|
|
|
-- TODO: there should be some way to undelete things (e.g. a list of
|
|
|
|
|
-- deleted traits under each item)
|
|
|
|
|
imgButton "delete trait" "/x.svg" [width_ "12"] $
|
|
|
|
|
JS.deleteTrait (itemId, trait^.uid, this)
|
|
|
|
|
textButton "edit" $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
|
|
|
|
|
|
|
|
|
section "editing" [] $ do
|
|
|
|
|
smallMarkdownEditor
|
|
|
|
|
[rows_ "5"]
|
|
|
|
|
(trait^.content)
|
|
|
|
|
(\val -> JS.submitTrait (this, itemId, trait^.uid, val))
|
|
|
|
|
(Just (JS.switchSection (this, "editable" :: Text)))
|
|
|
|
|
|
|
|
|
|
-- TODO: automatically provide links to modules in Markdown (and have a
|
|
|
|
|
-- database of modules or something)
|
|
|
|
|
|
|
|
|
|
-- TODO: [very-easy] write about the all-is-text extension
|
|
|
|
|
-- TODO: [easy] add a button to make default editor font monospace
|
|
|
|
|
-- TODO: [easy] write that arrows are for arranging stuff, not upvoting
|
|
|
|
|
|
|
|
|
|
-- TODO: record IPs in the acid-state transaction log
|
|
|
|
|
|
|
|
|
|
-- TODO: [easy] add Hayoo search, Hoogle search, and Hackage search shortcut
|
|
|
|
|
-- boxes
|
|
|
|
|
|
|
|
|
|
-- TODO: when searching, show links to package, main modules, etc before all
|
|
|
|
|
-- categories
|
|
|
|
|
|
|
|
|
|
-- TODO: attach TODOs (“fix grammar”, etc) to items and categories (or should
|
|
|
|
|
-- people instead just write “TODO fix grammar” in description and then such
|
|
|
|
|
-- things could be displayed in gray font and also there'd be an
|
|
|
|
|
-- automatically updated list of TODOs somewhere?)
|
|
|
|
|
|
|
|
|
|
-- TODO: make it possible to link to notes (and automatically expand when
|
|
|
|
|
-- linked)
|
|
|
|
|
|
|
|
|
|
-- TODO: [very-easy] focus the notes textarea on edit (can use jQuery's
|
|
|
|
|
-- .focus() on it)
|
2016-03-17 03:35:56 +03:00
|
|
|
|
renderItemNotes :: Item -> HtmlT IO ()
|
|
|
|
|
renderItemNotes item = do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
let thisId = "item-notes-" <> uidToText (item^.uid)
|
|
|
|
|
this = JS.selectId thisId
|
2016-03-17 03:35:56 +03:00
|
|
|
|
div_ [id_ thisId, class_ "item-notes"] $ do
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- TODO: this duplicates code from renderCategoryNotes, try to reduce
|
|
|
|
|
-- duplication
|
|
|
|
|
|
|
|
|
|
section "collapsed" [shown] $ do
|
|
|
|
|
-- TODO: when notes are hidden, show a list of headers in the notes
|
|
|
|
|
textButton "show notes/examples" $
|
|
|
|
|
JS.switchSection (this, "expanded" :: Text)
|
|
|
|
|
|
|
|
|
|
section "expanded" [noScriptShown] $ do
|
|
|
|
|
let buttons = do
|
|
|
|
|
textButton "hide notes" $
|
|
|
|
|
JS.switchSection (this, "collapsed" :: Text)
|
2016-03-15 15:41:09 +03:00
|
|
|
|
emptySpan "1em"
|
|
|
|
|
textButton "edit notes" $
|
|
|
|
|
JS.switchSection (this, "editing" :: Text)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
buttons
|
2016-03-16 02:17:08 +03:00
|
|
|
|
if item^.notes == ""
|
2016-03-15 15:35:35 +03:00
|
|
|
|
then p_ "add something!"
|
2016-03-16 02:17:08 +03:00
|
|
|
|
else toHtml (item^.notes)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
buttons
|
|
|
|
|
-- TODO: [easy] the lower “hide notes” should scroll back to item when
|
|
|
|
|
-- the notes are closed (but don't scroll if it's already visible after
|
|
|
|
|
-- the notes have been hidden)
|
|
|
|
|
|
|
|
|
|
section "editing" [] $ do
|
2016-03-16 02:17:08 +03:00
|
|
|
|
contents <- if item^.notes == ""
|
|
|
|
|
then liftIO $ renderMarkdownBlock <$>
|
|
|
|
|
T.readFile "static/item-notes-template.md"
|
|
|
|
|
else return (item^.notes)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
markdownEditor
|
2016-03-17 15:29:45 +03:00
|
|
|
|
[rows_ "10"]
|
2016-03-15 15:35:35 +03:00
|
|
|
|
contents
|
|
|
|
|
(\val -> JS.submitItemNotes (this, item^.uid, val))
|
|
|
|
|
(JS.switchSection (this, "expanded" :: Text))
|
|
|
|
|
|
|
|
|
|
-- TODO: a shortcut for editing (when you press Ctrl-something, whatever was
|
|
|
|
|
-- selected becomes editable)
|
|
|
|
|
|
|
|
|
|
-- Utils
|
|
|
|
|
|
|
|
|
|
onPageLoad :: JS -> HtmlT IO ()
|
|
|
|
|
onPageLoad js = script_ $ format "$(document).ready(function(){{}});" [js]
|
|
|
|
|
|
|
|
|
|
emptySpan :: Text -> HtmlT IO ()
|
|
|
|
|
emptySpan w = span_ [style_ ("margin-left:" <> w)] mempty
|
|
|
|
|
|
|
|
|
|
-- Use inputValue to get the value (works with input_ and textarea_)
|
|
|
|
|
onEnter :: JS -> Attribute
|
|
|
|
|
onEnter handler = onkeydown_ $
|
|
|
|
|
format "if (event.keyCode == 13) {{} return false;}" [handler]
|
|
|
|
|
|
|
|
|
|
textInput :: [Attribute] -> HtmlT IO ()
|
|
|
|
|
textInput attrs = input_ (type_ "text" : attrs)
|
|
|
|
|
|
|
|
|
|
inputValue :: JS
|
|
|
|
|
inputValue = JS "this.value"
|
|
|
|
|
|
|
|
|
|
clearInput :: JS
|
|
|
|
|
clearInput = JS "this.value = '';"
|
|
|
|
|
|
|
|
|
|
onFormSubmit :: (JS -> JS) -> Attribute
|
|
|
|
|
onFormSubmit f = onsubmit_ $ format "{} return false;" [f (JS "this")]
|
|
|
|
|
|
|
|
|
|
button :: Text -> [Attribute] -> JS -> HtmlT IO ()
|
|
|
|
|
button value attrs handler =
|
|
|
|
|
input_ (type_ "button" : value_ value : onclick_ handler' : attrs)
|
|
|
|
|
where
|
|
|
|
|
handler' = fromJS handler
|
|
|
|
|
|
|
|
|
|
-- A text button looks like “[cancel]”
|
|
|
|
|
--
|
|
|
|
|
-- TODO: consider dotted links instead?
|
|
|
|
|
textButton
|
|
|
|
|
:: Text -- ^ Button text
|
|
|
|
|
-> JS -- ^ Onclick handler
|
|
|
|
|
-> HtmlT IO ()
|
|
|
|
|
textButton caption (JS handler) =
|
|
|
|
|
span_ [class_ "text-button"] $
|
|
|
|
|
-- “#” is used instead of javascript:void(0) because the latter is slow
|
|
|
|
|
-- in Firefox (at least for me – tested with Firefox 43 on Arch Linux)
|
|
|
|
|
a_ [href_ "#", onclick_ (handler <> "return false;")]
|
|
|
|
|
(toHtml caption)
|
|
|
|
|
|
|
|
|
|
-- So far all icons used here have been from <https://useiconic.com/open/>
|
|
|
|
|
imgButton :: Text -> Url -> [Attribute] -> JS -> HtmlT IO ()
|
|
|
|
|
imgButton alt src attrs (JS handler) =
|
|
|
|
|
a_ [href_ "#", onclick_ (handler <> "return false;")]
|
|
|
|
|
(img_ (src_ src : alt_ alt : attrs))
|
|
|
|
|
|
|
|
|
|
markdownEditor
|
2016-03-17 15:29:45 +03:00
|
|
|
|
:: [Attribute]
|
|
|
|
|
-> MarkdownBlock -- ^ Default text
|
2016-03-16 02:17:08 +03:00
|
|
|
|
-> (JS -> JS) -- ^ “Submit” handler, receiving the contents of the editor
|
|
|
|
|
-> JS -- ^ “Cancel” handler
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-> HtmlT IO ()
|
2016-03-17 15:29:45 +03:00
|
|
|
|
markdownEditor attr (markdownBlockText -> s) submit cancel = do
|
2016-03-19 00:05:01 +03:00
|
|
|
|
textareaId <- randomLongUid
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- Autocomplete has to be turned off thanks to
|
|
|
|
|
-- <http://stackoverflow.com/q/8311455>.
|
2016-03-17 15:29:45 +03:00
|
|
|
|
textarea_ ([uid_ textareaId, autocomplete_ "off", class_ "big fullwidth"]
|
|
|
|
|
++ attr) $
|
2016-03-15 15:35:35 +03:00
|
|
|
|
toHtml s
|
|
|
|
|
let val = JS $ format "document.getElementById(\"{}\").value" [textareaId]
|
|
|
|
|
button "Save" [] $
|
|
|
|
|
submit val
|
|
|
|
|
emptySpan "6px"
|
|
|
|
|
button "Cancel" [] $
|
|
|
|
|
JS.assign val s <>
|
|
|
|
|
cancel
|
|
|
|
|
emptySpan "6px"
|
|
|
|
|
"Markdown"
|
|
|
|
|
|
|
|
|
|
smallMarkdownEditor
|
|
|
|
|
:: [Attribute]
|
2016-03-16 02:17:08 +03:00
|
|
|
|
-> MarkdownInline -- ^ Default text
|
|
|
|
|
-> (JS -> JS) -- ^ “Submit” handler, receiving the contents of the editor
|
|
|
|
|
-> Maybe JS -- ^ “Cancel” handler (if “Cancel” is needed)
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-> HtmlT IO ()
|
2016-03-17 15:29:45 +03:00
|
|
|
|
smallMarkdownEditor attr (markdownInlineText -> s) submit mbCancel = do
|
2016-03-19 00:05:01 +03:00
|
|
|
|
textareaId <- randomLongUid
|
2016-03-15 15:35:35 +03:00
|
|
|
|
let val = JS $ format "document.getElementById(\"{}\").value" [textareaId]
|
|
|
|
|
textarea_ ([class_ "fullwidth", uid_ textareaId, autocomplete_ "off",
|
2016-03-17 15:29:45 +03:00
|
|
|
|
onEnter (submit val)] ++ attr) $
|
2016-03-15 15:35:35 +03:00
|
|
|
|
toHtml s
|
|
|
|
|
case mbCancel of
|
|
|
|
|
Nothing -> return ()
|
|
|
|
|
Just cancel -> do
|
|
|
|
|
br_ []
|
|
|
|
|
textButton "cancel" $
|
|
|
|
|
JS.assign val s <>
|
|
|
|
|
cancel
|
|
|
|
|
|
|
|
|
|
thisNode :: HtmlT IO JQuerySelector
|
|
|
|
|
thisNode = do
|
2016-03-19 00:05:01 +03:00
|
|
|
|
uid' <- randomLongUid
|
2016-03-15 15:35:35 +03:00
|
|
|
|
-- If the class name ever changes, fix 'JS.moveNodeUp' and
|
|
|
|
|
-- 'JS.moveNodeDown'.
|
|
|
|
|
span_ [uid_ uid', class_ "dummy"] mempty
|
|
|
|
|
return (JS.selectParent (JS.selectUid uid'))
|
|
|
|
|
|
|
|
|
|
-- Wheh changing these, also look at 'JS.switchSection'.
|
|
|
|
|
|
|
|
|
|
shown, noScriptShown :: Attribute
|
|
|
|
|
shown = class_ " shown "
|
|
|
|
|
noScriptShown = class_ " noscript-shown "
|
|
|
|
|
|
|
|
|
|
section :: Monad m => Text -> [Attribute] -> HtmlT m () -> HtmlT m ()
|
|
|
|
|
section t attrs = div_ (class_ (t <> " section ") : attrs)
|
|
|
|
|
|
|
|
|
|
sectionSpan :: Monad m => Text -> [Attribute] -> HtmlT m () -> HtmlT m ()
|
|
|
|
|
sectionSpan t attrs = span_ (class_ (t <> " section ") : attrs)
|
|
|
|
|
|
|
|
|
|
-- TODO: add something to edit a particular paragraph of the notes
|
|
|
|
|
|
|
|
|
|
newGroupValue :: Text
|
|
|
|
|
newGroupValue = "-new-group-"
|