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} -
- $forall (status, msg) <- mmsgs - $with status2 <- bool status "info" (status == "") -
-
- #{msg} + ^{alerts} $if not isModal diff --git a/templates/standalone/alerts.hamlet b/templates/standalone/alerts.hamlet deleted file mode 100644 index 163f415b4..000000000 --- a/templates/standalone/alerts.hamlet +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/standalone/alerts.julius b/templates/standalone/alerts.julius deleted file mode 100644 index c8c04dc14..000000000 --- a/templates/standalone/alerts.julius +++ /dev/null @@ -1,101 +0,0 @@ -(function() { - 'use strict'; - - window.utils = window.utils || {}; - - var ALERT_INVISIBLE_CLASS = 'alert--invisible'; - var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible'; - var ALERT_AUTO_DISAPPEAR_DELAY = 10; - - var alertsShowingToggler = false; - - window.utils.alerts = function(alertsEl) { - - var toggler = alertsEl.querySelector('.alerts__toggler'); - - function makeToggler() { - toggler = document.createElement('DIV'); - toggler.classList.add('alerts__toggler'); - toggler.addEventListener('click', function() { - Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) { - alert.classList.remove(ALERT_INVISIBLE_CLASS); - toggler.classList.remove(TOGGLER_INVISIBLE_CLASS); - }); - checkToggler(); - }); - alertsEl.appendChild(toggler); - alertsEl.classList.add('js-initialized'); - } - - function makeAlert(alertEl) { - var iconEl = document.createElement('DIV'); - var closeEl = document.createElement('DIV'); - var dataDecay = alertEl.dataset.decay; - var autoDecay = ALERT_AUTO_DISAPPEAR_DELAY; - if (dataDecay) { - autoDecay = parseInt(dataDecay, 10); - } - iconEl.classList.add('alert__icon'); - closeEl.classList.add('alert__close'); - closeEl.addEventListener('click', function(event) { - closeAlert(alertEl); - }); - alertEl.insertBefore(iconEl, alertEl.children[0]); - alertEl.insertBefore(closeEl, alertEl.children[0]); - - // auto-hide info and success-alerts after 3 seconds - if (autoDecay > 0 && !alertEl.matches('.alert-warning, .alert-error')) { - window.setTimeout(function() { - closeAlert(alertEl); - }, autoDecay * 1000); - } - - alertEl.classList.add('js-initialized'); - } - - function closeAlert(alertEl) { - alertEl.classList.add(ALERT_INVISIBLE_CLASS); - checkToggler(); - } - - function checkToggler() { - var hidden = true; - Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) { - if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) { - hidden = false; - } - }); - if (!alertsShowingToggler) { - alertsShowingToggler = true; - window.setTimeout(function() { - toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden); - alertsShowingToggler = false; - }, 120); - } - } - - if (!alertsEl.classList.contains('js-initialized') || !toggler) - makeToggler(); - Array.from(alertsEl.querySelectorAll('.alert:not(.js-initialized)')).map(makeAlert); - } - -})(); - -document.addEventListener('setup', function(e) { - if (e.detail.module && e.detail.module !== 'alerts') - return; - - - // setup alerts - if (e.detail.scope.classList.contains('alerts')) { - window.utils.alerts(e.detail.scope); - } else { - var alertsEl = e.detail.scope.querySelector('.alerts'); - if (alertsEl) - window.utils.alerts(alertsEl); - } -}); - -document.addEventListener('DOMContentLoaded', function() { - document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true })) -}); diff --git a/templates/widgets/alerts/alerts.hamlet b/templates/widgets/alerts/alerts.hamlet new file mode 100644 index 000000000..aa7169ca1 --- /dev/null +++ b/templates/widgets/alerts/alerts.hamlet @@ -0,0 +1,9 @@ +
+
+ $forall (status, msg) <- mmsgs + $with status2 <- bool status "info" (status == "") +
+
+
+
+ #{msg} diff --git a/templates/widgets/alerts/alerts.julius b/templates/widgets/alerts/alerts.julius new file mode 100644 index 000000000..14e679f61 --- /dev/null +++ b/templates/widgets/alerts/alerts.julius @@ -0,0 +1,18 @@ +document.addEventListener('setup', function(e) { + if (!e.detail.module || e.detail.module !== 'alerts') { + return; + } + + // setup alerts + if (e.detail.scope.classList.contains('alerts')) { + window.utils.alerts(e.detail.scope); + } else { + var alertsEl = e.detail.scope.querySelector('.alerts'); + if (alertsEl) + window.utils.alerts(alertsEl); + } +}); + +document.addEventListener('DOMContentLoaded', function() { + document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true })) +});