diff --git a/package.yaml b/package.yaml index 46af6eab8..0aadd7a3f 100644 --- a/package.yaml +++ b/package.yaml @@ -114,6 +114,7 @@ dependencies: - memcached-binary - directory-tree - lifted-base + - hsass other-extensions: - GeneralizedNewtypeDeriving diff --git a/src/Foundation.hs b/src/Foundation.hs index 0b56d439f..951b4fb41 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -974,6 +974,8 @@ siteLayout' headingOverride widget = do asidenav = $(widgetFile "widgets/asidenav") footer :: Widget footer = $(widgetFile "widgets/footer") + alerts :: Widget + alerts = $(widgetFile "widgets/alerts/alerts") contentHeadline :: Maybe Widget contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute) breadcrumbsWgt :: Widget @@ -993,17 +995,18 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_polyfills_urlPolyfill_js addScript $ StaticR js_utils_featureChecker_js addScript $ StaticR js_utils_tabber_js + addScript $ StaticR js_utils_alerts_js addStylesheet $ StaticR css_vendor_flatpickr_css addStylesheet $ StaticR css_vendor_fontawesome_css addStylesheet $ StaticR css_fonts_css addStylesheet $ StaticR css_utils_tabber_css + addStylesheet $ StaticR css_utils_alerts_scss $(widgetFile "default-layout") $(widgetFile "standalone/modal") $(widgetFile "standalone/showHide") $(widgetFile "standalone/inputs") $(widgetFile "standalone/tooltip") $(widgetFile "standalone/tabber") - $(widgetFile "standalone/alerts") $(widgetFile "standalone/datepicker") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") diff --git a/src/Settings/StaticFiles.hs b/src/Settings/StaticFiles.hs index c7bd88255..d2375a8e5 100644 --- a/src/Settings/StaticFiles.hs +++ b/src/Settings/StaticFiles.hs @@ -6,6 +6,7 @@ module Settings.StaticFiles import ClassyPrelude import Settings (appStaticDir, compileTimeAppSettings) +import Settings.StaticFiles.Generator import Yesod.EmbeddedStatic -- This generates easy references to files in the static directory at compile time, @@ -23,4 +24,4 @@ import Yesod.EmbeddedStatic #define DEV_BOOL False #endif -mkEmbeddedStatic DEV_BOOL "embeddedStatic" [embedDir $ appStaticDir compileTimeAppSettings] +mkEmbeddedStatic DEV_BOOL "embeddedStatic" . pure . staticGenerator $ appStaticDir compileTimeAppSettings diff --git a/src/Settings/StaticFiles/Generator.hs b/src/Settings/StaticFiles/Generator.hs new file mode 100644 index 000000000..b60bcef2c --- /dev/null +++ b/src/Settings/StaticFiles/Generator.hs @@ -0,0 +1,72 @@ +module Settings.StaticFiles.Generator + ( staticGenerator + ) where + +import ClassyPrelude +import Yesod.EmbeddedStatic.Types +import Yesod.EmbeddedStatic + +import System.FilePath +import System.Directory.Tree +import Network.Mime + +import Language.Haskell.TH +import Language.Haskell.TH.Syntax + +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as LBS + +import qualified Data.Map as Map + +import qualified Text.Sass.Compilation as Sass +import Text.Sass.Options + +import Data.Default + +import qualified Data.Foldable as Fold + + +staticGenerator :: FilePath -> Generator +staticGenerator staticDir = do + dirTree' <- runIO $ readDirectoryWith toEntries staticDir + Fold.forM_ (fst <$> zipPaths dirTree') addDependentFile + return . Fold.fold $ dirTree dirTree' + where + toEntries :: FilePath -- ^ Absolute path + -> IO [Entry] + toEntries loc = compile (mimeByExt mimeMap defaultMimeType $ pack loc) (makeRelative staticDir loc) loc + + mimeMap = defaultMimeMap `mappend` Map.fromList + [ ("sass", "text/x-sass") + , ("scss", "text/x-scss") + ] + +compile :: MimeType + -> Location -- ^ Relative location + -> FilePath -- ^ Absolute filepath + -> IO [Entry] +compile "text/x-scss" sassLoc fp = return . pure $ def + { ebHaskellName = Just $ pathToName sassLoc + , ebLocation + , ebMimeType = "text/css" + , ebProductionContent = either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile fp def + , ebDevelReload = [| either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile $(litE $ stringL fp) def |] + } + where + ebLocation = sassLoc -<.> "css" +compile "text/x-sass" sassLoc fp = return . pure $ def + { ebHaskellName = Just $ pathToName sassLoc + , ebLocation + , ebMimeType = "text/css" + , ebProductionContent = either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile fp (def { sassIsIndentedSyntax = True }) + , ebDevelReload = [| either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile $(litE $ stringL fp) (def { sassIsIndentedSyntax = True }) |] + } + where + ebLocation = sassLoc -<.> "css" +compile ebMimeType ebLocation fp = return . pure $ def + { ebHaskellName = Just $ pathToName ebLocation + , ebLocation + , ebMimeType + , ebProductionContent = LBS.fromStrict <$> BS.readFile fp + , ebDevelReload = [| LBS.fromStrict <$> BS.readFile $(litE $ stringL fp) |] + } diff --git a/templates/standalone/alerts.lucius b/static/css/utils/alerts.scss similarity index 99% rename from templates/standalone/alerts.lucius rename to static/css/utils/alerts.scss index bc5603220..3256eef96 100644 --- a/templates/standalone/alerts.lucius +++ b/static/css/utils/alerts.scss @@ -156,7 +156,7 @@ } } -.alert__close { +.alert__closer { cursor: pointer; text-align: right; position: absolute; @@ -196,7 +196,7 @@ @media (max-width: 768px) { - .alert__close { + .alert__closer { width: 40px; } } diff --git a/static/js/utils/alerts.js b/static/js/utils/alerts.js new file mode 100644 index 000000000..eec2fb73a --- /dev/null +++ b/static/js/utils/alerts.js @@ -0,0 +1,86 @@ +(function() { + 'use strict'; + + window.utils = window.utils || {}; + + var ALERTS_TOGGLER_CLASS = 'alerts__toggler'; + var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; + var ALERTS_TOGGLER_APPEAR_DELAY = 120; + + var ALERT_CLASS = 'alert'; + var ALERT_CLOSER_CLASS = 'alert__closer'; + var ALERT_INVISIBLE_CLASS = 'alert--invisible'; + var ALERT_AUTO_HIDE_DELAY = 10; + var ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success'; + + var JS_INITIALIZED_CLASS = 'js-initialized'; + + window.utils.alerts = function(alertsEl) { + + if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) { + return; + } + + var togglerCheckRequested = false; + + var togglerEl = alertsEl.querySelector('.' + ALERTS_TOGGLER_CLASS); + + var alertElements = Array.from(alertsEl.querySelectorAll('.' + ALERT_CLASS)) + .filter(function(alert) { + return !alert.classList.contains(JS_INITIALIZED_CLASS); + }); + + function initToggler() { + togglerEl.addEventListener('click', function() { + alertElements.forEach(function(alertEl) { + toggleAlert(alertEl, true); + }); + togglerEl.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); + }); + alertsEl.classList.add(JS_INITIALIZED_CLASS); + } + + function initAlert(alertEl) { + var autoHideDelay = ALERT_AUTO_HIDE_DELAY; + if (alertEl.dataset.decay) { + autoHideDelay = parseInt(alertEl.dataset.decay, 10); + } + + var closeEl = alertEl.querySelector('.' + ALERT_CLOSER_CLASS); + closeEl.addEventListener('click', function() { + toggleAlert(alertEl); + }); + + if (autoHideDelay > 0 && alertEl.matches(ALERT_AUTOCLOSING_MATCHER)) { + window.setTimeout(function() { + toggleAlert(alertEl); + }, autoHideDelay * 1000); + } + + alertEl.classList.add(JS_INITIALIZED_CLASS); + } + + function toggleAlert(alertEl, visible) { + alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible); + checkToggler(); + } + + function checkToggler() { + if (togglerCheckRequested) { + return; + } + + var alertsHidden = alertElements.reduce(function(acc, alert) { + return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS); + }, true); + + window.setTimeout(function() { + togglerEl.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden); + togglerCheckRequested = false; + }, ALERTS_TOGGLER_APPEAR_DELAY); + } + + initToggler(); + alertElements.forEach(initAlert); + }; +})(); diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index bf936affd..87870650b 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -30,12 +30,7 @@ $if not isModal ^{widget} -