From e27bebac59e42857b82e567f07576167bafcf8e3 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:26:29 +0100 Subject: [PATCH 01/35] feat(foundation): move stuff out of Foundation --- src/Foundation.hs | 76 +------------------------ src/Foundation/Routes.hs | 84 ++++++++++++++++++++++++++-- src/Foundation/Routes/Definitions.hs | 10 ++++ src/Handler/Utils/Routes.hs | 4 +- 4 files changed, 94 insertions(+), 80 deletions(-) create mode 100644 src/Foundation/Routes/Definitions.hs diff --git a/src/Foundation.hs b/src/Foundation.hs index 3014250a6..1e7d8db89 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -71,6 +71,7 @@ import Handler.Utils.SchoolLdap import Handler.Utils.ExamOffice.Exam import Handler.Utils.ExamOffice.Course import Handler.Utils.Profile +import Handler.Utils.Routes import Utils.Form import Utils.Sheet import Utils.SystemMessage @@ -96,86 +97,12 @@ import qualified Ldap.Client as Ldap import UnliftIO.Pool - --- This is where we define all of the routes in our application. For a full --- explanation of the syntax, please see: --- http://www.yesodweb.com/book/routing-and-handlers --- --- Note that this is really half the story; in Application.hs, mkYesodDispatch --- generates the rest of the code. Please see the following documentation --- for an explanation for this split: --- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules --- --- This function also generates the following type synonyms: --- type Handler x = HandlerT UniWorX IO x --- type Widget = WidgetT UniWorX IO () -mkYesodData "UniWorX" uniworxRoutes - -deriving instance Generic CourseR -deriving instance Generic SheetR -deriving instance Generic SubmissionR -deriving instance Generic MaterialR -deriving instance Generic TutorialR -deriving instance Generic ExamR -deriving instance Generic CourseApplicationR -deriving instance Generic AllocationR -deriving instance Generic SchoolR -deriving instance Generic ExamOfficeR -deriving instance Generic CourseNewsR -deriving instance Generic CourseEventR -deriving instance Generic (Route UniWorX) - -data RouteChildren -type instance Children RouteChildren a = ChildrenRouteChildren a -type family ChildrenRouteChildren a where - ChildrenRouteChildren (Route EmbeddedStatic) = '[] - ChildrenRouteChildren (Route Auth) = '[] - ChildrenRouteChildren UUID = '[] - ChildrenRouteChildren (Key a) = '[] - ChildrenRouteChildren (CI a) = '[] - - ChildrenRouteChildren a = Children ChGeneric a - -- | Convenient Type Synonyms: type DB = YesodDB UniWorX type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, Widget) type MsgRenderer = MsgRendererS UniWorX -- see Utils type MailM a = MailT (HandlerFor UniWorX) a --- Pattern Synonyms for convenience -pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX -pattern CSheetR tid ssh csh shn ptn - = CourseR tid ssh csh (SheetR shn ptn) - -pattern CMaterialR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> MaterialR -> Route UniWorX -pattern CMaterialR tid ssh csh mnm ptn - = CourseR tid ssh csh (MaterialR mnm ptn) - -pattern CTutorialR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> TutorialR -> Route UniWorX -pattern CTutorialR tid ssh csh tnm ptn - = CourseR tid ssh csh (TutorialR tnm ptn) - -pattern CExamR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamR -> Route UniWorX -pattern CExamR tid ssh csh tnm ptn - = CourseR tid ssh csh (ExamR tnm ptn) - -pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX -pattern CSubmissionR tid ssh csh shn cid ptn - = CSheetR tid ssh csh shn (SubmissionR cid ptn) - -pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX -pattern CApplicationR tid ssh csh appId ptn - = CourseR tid ssh csh (CourseApplicationR appId ptn) - -pattern CNewsR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseNews -> CourseNewsR -> Route UniWorX -pattern CNewsR tid ssh csh nId ptn - = CourseR tid ssh csh (CourseNewsR nId ptn) - -pattern CEventR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> CourseEventR -> Route UniWorX -pattern CEventR tid ssh csh nId ptn - = CourseR tid ssh csh (CourseEventR nId ptn) - - -- Requires `rendeRoute`, thus cannot currently be moved to Foundation.I18n instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX)) where renderMessage f ls (UnsupportedAuthPredicate tag route) = mr . MsgUnsupportedAuthPredicate (mr tag) $ Text.intercalate "/" pieces @@ -1486,6 +1413,7 @@ siteLayout' headingOverride widget = do primaryLanguage <- unsafeHead . Text.splitOn "-" <$> selectLanguage appLanguages mcurrentRoute <- getCurrentRoute + let currentHandler = classifyHandler <$> mcurrentRoute -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. let diff --git a/src/Foundation/Routes.hs b/src/Foundation/Routes.hs index 614bdea6d..0e83a0734 100644 --- a/src/Foundation/Routes.hs +++ b/src/Foundation/Routes.hs @@ -1,10 +1,84 @@ +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + module Foundation.Routes - ( uniworxRoutes + ( module Foundation.Routes.Definitions + , module Foundation.Routes ) where -import ClassyPrelude.Yesod -import Yesod.Routes.TH.Types (ResourceTree) +import Import.NoFoundation +import Foundation.Type +import Foundation.Routes.Definitions -uniworxRoutes :: [ResourceTree String] -uniworxRoutes = $(parseRoutesFile "routes") +-- This is where we define all of the routes in our application. For a full +-- explanation of the syntax, please see: +-- http://www.yesodweb.com/book/routing-and-handlers +-- +-- Note that this is really half the story; in Application.hs, mkYesodDispatch +-- generates the rest of the code. Please see the following documentation +-- for an explanation for this split: +-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules +-- +-- This function also generates the following type synonyms: +-- type Handler x = HandlerT UniWorX IO x +-- type Widget = WidgetT UniWorX IO () +mkYesodData "UniWorX" uniworxRoutes + +deriving instance Generic CourseR +deriving instance Generic SheetR +deriving instance Generic SubmissionR +deriving instance Generic MaterialR +deriving instance Generic TutorialR +deriving instance Generic ExamR +deriving instance Generic CourseApplicationR +deriving instance Generic AllocationR +deriving instance Generic SchoolR +deriving instance Generic ExamOfficeR +deriving instance Generic CourseNewsR +deriving instance Generic CourseEventR +deriving instance Generic (Route UniWorX) + +data RouteChildren +type instance Children RouteChildren a = ChildrenRouteChildren a +type family ChildrenRouteChildren a where + ChildrenRouteChildren (Route EmbeddedStatic) = '[] + ChildrenRouteChildren (Route Auth) = '[] + ChildrenRouteChildren UUID = '[] + ChildrenRouteChildren (Key a) = '[] + ChildrenRouteChildren (CI a) = '[] + + ChildrenRouteChildren a = Children ChGeneric a + +-- Pattern Synonyms for convenience +pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX +pattern CSheetR tid ssh csh shn ptn + = CourseR tid ssh csh (SheetR shn ptn) + +pattern CMaterialR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> MaterialR -> Route UniWorX +pattern CMaterialR tid ssh csh mnm ptn + = CourseR tid ssh csh (MaterialR mnm ptn) + +pattern CTutorialR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> TutorialR -> Route UniWorX +pattern CTutorialR tid ssh csh tnm ptn + = CourseR tid ssh csh (TutorialR tnm ptn) + +pattern CExamR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamR -> Route UniWorX +pattern CExamR tid ssh csh tnm ptn + = CourseR tid ssh csh (ExamR tnm ptn) + +pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX +pattern CSubmissionR tid ssh csh shn cid ptn + = CSheetR tid ssh csh shn (SubmissionR cid ptn) + +pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX +pattern CApplicationR tid ssh csh appId ptn + = CourseR tid ssh csh (CourseApplicationR appId ptn) + +pattern CNewsR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseNews -> CourseNewsR -> Route UniWorX +pattern CNewsR tid ssh csh nId ptn + = CourseR tid ssh csh (CourseNewsR nId ptn) + +pattern CEventR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> CourseEventR -> Route UniWorX +pattern CEventR tid ssh csh nId ptn + = CourseR tid ssh csh (CourseEventR nId ptn) diff --git a/src/Foundation/Routes/Definitions.hs b/src/Foundation/Routes/Definitions.hs new file mode 100644 index 000000000..4908a25ee --- /dev/null +++ b/src/Foundation/Routes/Definitions.hs @@ -0,0 +1,10 @@ +module Foundation.Routes.Definitions + ( uniworxRoutes + ) where + +import ClassyPrelude.Yesod +import Yesod.Routes.TH.Types (ResourceTree) + + +uniworxRoutes :: [ResourceTree String] +uniworxRoutes = $(parseRoutesFile "routes") diff --git a/src/Handler/Utils/Routes.hs b/src/Handler/Utils/Routes.hs index 52a93dfed..345718bcb 100644 --- a/src/Handler/Utils/Routes.hs +++ b/src/Handler/Utils/Routes.hs @@ -2,7 +2,9 @@ module Handler.Utils.Routes ( classifyHandler ) where -import Import +import Import.NoFoundation +import Foundation.Routes +import Foundation.Type import Utils.TH.Routes From ba846be5aa99278ca00b64150165ccb825eb9ba0 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:28:15 +0100 Subject: [PATCH 02/35] feat(default-layout): save handler ident to main content --- templates/default-layout.hamlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index cdcffaad5..22fe5fa2d 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -11,7 +11,7 @@ $if not isModal
-
+
$if not isModal From 169a4799b4996c9474ee492e79dcd0a5040af04e Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:31:39 +0100 Subject: [PATCH 03/35] feat(hide-columns): set attributes for hide-columns and extra-stuff div --- templates/course-participants.hamlet | 2 +- templates/table/colonnade.hamlet | 2 ++ templates/table/layout-wrapper.hamlet | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/course-participants.hamlet b/templates/course-participants.hamlet index 37f9960b9..ff5847746 100644 --- a/templates/course-participants.hamlet +++ b/templates/course-participants.hamlet @@ -4,4 +4,4 @@ $# $# participantTable : widget table ^{participantTable} -_{MsgCourseMembersCountOf (fromIntegral numParticipants) (courseCapacity course)}. \ No newline at end of file +_{MsgCourseMembersCountOf (fromIntegral numParticipants) (courseCapacity course)}. diff --git a/templates/table/colonnade.hamlet b/templates/table/colonnade.hamlet index c756229c5..f13131530 100644 --- a/templates/table/colonnade.hamlet +++ b/templates/table/colonnade.hamlet @@ -1,4 +1,6 @@ $newline never +
+ Table Utils:
$maybe wHeaders' <- wHeaders diff --git a/templates/table/layout-wrapper.hamlet b/templates/table/layout-wrapper.hamlet index 791ccd656..c67a6ca3d 100644 --- a/templates/table/layout-wrapper.hamlet +++ b/templates/table/layout-wrapper.hamlet @@ -1,3 +1,3 @@ $newline never -
+
^{table} From 10232401369fa18fd7866a584b4f6d3eb1380e5c Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:39:47 +0100 Subject: [PATCH 04/35] feat(storage-manager): add storage manager library --- .../lib/storage-manager/storage-manager.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 frontend/src/lib/storage-manager/storage-manager.js diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js new file mode 100644 index 000000000..0735286c1 --- /dev/null +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -0,0 +1,62 @@ +const LIFETIME = { + INFINITE: 'infinite', +}; + +export class StorageManager { + + namespace; + + constructor(namespace) { + if (!namespace) { + throw new Error('Cannot setup StorageManager without namespace'); + } + + this.namespace = namespace; + } + + toNamespace(key) { + return this.namespace + '--' + key; + } + + save(key, value, options) { + if (!key) { + throw new Error('StorageManager.save called with invalid key'); + } + + if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { + throw new Error('StorageManager.save called with unsupported lifetime option'); + } + + const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; + + switch (lifetime) { + case LIFETIME.INFINITE: + localStorage.setItem(this.toNamespace(key), JSON.stringify(value)); + break; + default: + console.error('StorageManager.save cannot save item with unsupported lifetime'); + } + } + + load(key, options) { + if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { + throw new Error('StorageManager.load called with unsupported lifetime option'); + } + + const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; + + switch (lifetime) { + case LIFETIME.INFINITE: { + const value = JSON.parse(localStorage.getItem(this.toNamespace(key))); + if (value === null) { + // remove item from localStorage if it stores an invalid value (cannot be parsed) + localStorage.removeItem(this.toNamespace(key)); + } + return value; + } + default: + console.error('StorageManager.load cannot load item with unsupported lifetime'); + } + } + +} From 111821dcad4d0ffde6dfebbb62fb21b9b76ab9c5 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:49:15 +0100 Subject: [PATCH 05/35] feat(hide-columns): first stub of hide-column util with manual styling --- .../src/utils/hide-columns/hide-columns.js | 149 ++++++++++++++++++ frontend/src/utils/utils.js | 2 + 2 files changed, 151 insertions(+) create mode 100644 frontend/src/utils/hide-columns/hide-columns.js diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js new file mode 100644 index 000000000..d4cbf234e --- /dev/null +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -0,0 +1,149 @@ +import { Utility } from '../../core/utility'; +import { StorageManager } from '../../lib/storage-manager/storage-manager'; + +const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; +const TABLE_HEADER_IDENT = 'hide-column-header'; + +const TABLE_UTILS_ATTR = 'table-utils'; +const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; + + +const HIDER_TEXT_ATTR = 'header-text'; + +const HIDE_BUTTON_FADE_DELAY = 3000; + +@Utility({ + selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, +}) +export class HideColumns { + + _storageManager = new StorageManager('uw-hide-columns'); + + _element; + _tableUtilContainer; + + constructor(element) { + if (!element) { + throw new Error('Hide Columns utility cannot be setup without an element!'); + } + + // do not provide hide-column ability in tables inside modals or async forms with response + if (element.closest('[uw-modal], .async-form__response')) { + return false; + } + + this._element = element; + + const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`); + if (!hideColumnsContainer) { + throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!'); + } + + // get or create table utils container + this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR); + if (!this._tableUtilContainer) { + this._tableUtilContainer = document.createElement('div'); + this._tableUtilContainer.setAttribute(TABLE_UTILS_ATTR, ''); + const tableContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}] > *`); + hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); + } + + this._element.querySelectorAll('th').forEach(th => { + const storageKey = this.getStorageKey(th); + const previouslyHidden = this._storageManager.load(storageKey); + this.setupHideButton(th, previouslyHidden); + }); + } + + setupHideButton(th, prevHidden) { + console.log('setupHideButton', th, prevHidden); + + const hider = document.createElement('button'); + hider.setAttribute(HIDER_TEXT_ATTR, th.innerText); + + hider.addEventListener('click', (event) => { + console.log('click', th.cellIndex, hider, th.innerText); + event.preventDefault(); + this.toggleColumnVisibility(th, hider); + }); + + // TODO fade in / fade out animation in css + th.addEventListener('mouseover', () => { + hider.style.display = ''; + }); + th.addEventListener('mouseout', () => { + setTimeout(() => { + if (hider.style.position === 'absolute') hider.style.display = 'none'; + }, HIDE_BUTTON_FADE_DELAY); + }); + + this.updateColumnDisplay(th.cellIndex, prevHidden); + this.updateHider(hider, prevHidden); + + this._tableUtilContainer.appendChild(hider); + } + + // TODO better name + toggleColumnVisibility(th, hider) { + const storageKey = this.getStorageKey(th); + + const hidden = !this._storageManager.load(storageKey); + + // hide/unhide column + this.updateColumnDisplay(th.cellIndex, hidden); + + // tweak hider button + this.updateHider(hider, hidden); + + // persist new hidden setting for column + this._storageManager.save(storageKey, hidden); + } + + // TODO better name + updateColumnDisplay(columnIndex, hidden) { + this._element.getElementsByTagName('tr').forEach(row => { + if (row.cells[columnIndex]) { + row.cells[columnIndex].style.display = hidden ? 'none' : ''; + } + }); + } + + // TODO better name + updateHider(hider, hidden) { + // TODO set css classes instead + if (hidden) { + hider.innerHTML = hider.getAttribute(HIDER_TEXT_ATTR); + hider.style.position = 'relative'; + } else { + hider.innerHTML = 'hide'; + hider.style.position = 'absolute'; + hider.style.display = 'none'; + } + } + + getStorageKey(th) { + // get handler name + const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler'); + + // get hide-columns container ident (if not present, use table index in document as fallback) + let tIdent = th.getAttribute(TABLE_HEADER_IDENT); + if (!tIdent) { + const tablesInDocument = document.getElementsByTagName('TABLE'); + for (let i = 0; i < tablesInDocument.length; i++) { + if (tablesInDocument[i] === this._element) { + tIdent = i; + break; + } + } + } + + // check for unique table header ident from backend (if not present, use cell index as fallback) + let thIdent = th.getAttribute(TABLE_HEADER_IDENT); + if (!thIdent) { + thIdent = th.cellIndex; + } + + return `${handlerIdent}-${tIdent}-${thIdent}`; + } + +} diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index b539edb44..00f3e5ec0 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -11,6 +11,7 @@ import { Modal } from './modal/modal'; import { Tooltip } from './tooltips/tooltips'; import { CourseTeaser } from './course-teaser/course-teaser'; import { NavbarUtils } from './navbar/navbar'; +import { HideColumns } from './hide-columns/hide-columns'; export const Utils = [ Alerts, @@ -27,4 +28,5 @@ export const Utils = [ Tooltip, CourseTeaser, ...NavbarUtils, + HideColumns, ]; From 5cc88089b5e275bf37b64b95270991d3ef01fd04 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 16:51:44 +0100 Subject: [PATCH 06/35] chore(hide-columns): remove unnecessary console.logs --- frontend/src/utils/hide-columns/hide-columns.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index d4cbf234e..6e2836768 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -56,13 +56,10 @@ export class HideColumns { } setupHideButton(th, prevHidden) { - console.log('setupHideButton', th, prevHidden); - const hider = document.createElement('button'); hider.setAttribute(HIDER_TEXT_ATTR, th.innerText); hider.addEventListener('click', (event) => { - console.log('click', th.cellIndex, hider, th.innerText); event.preventDefault(); this.toggleColumnVisibility(th, hider); }); From d55d3ef4847f089970982e9554365f8699f2f9e6 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 17:00:38 +0100 Subject: [PATCH 07/35] feat(hide-columns): get table wrapper ident for storage ident --- frontend/src/utils/hide-columns/hide-columns.js | 4 +++- templates/table/layout-wrapper.hamlet | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 6e2836768..ea2da5195 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -20,6 +20,7 @@ export class HideColumns { _storageManager = new StorageManager('uw-hide-columns'); _element; + _elementWrapper; _tableUtilContainer; constructor(element) { @@ -38,6 +39,7 @@ export class HideColumns { if (!hideColumnsContainer) { throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!'); } + this._elementWrapper = hideColumnsContainer; // get or create table utils container this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR); @@ -123,7 +125,7 @@ export class HideColumns { const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler'); // get hide-columns container ident (if not present, use table index in document as fallback) - let tIdent = th.getAttribute(TABLE_HEADER_IDENT); + let tIdent = this._elementWrapper.getAttribute(HIDE_COLUMNS_CONTAINER_IDENT); if (!tIdent) { const tablesInDocument = document.getElementsByTagName('TABLE'); for (let i = 0; i < tablesInDocument.length; i++) { diff --git a/templates/table/layout-wrapper.hamlet b/templates/table/layout-wrapper.hamlet index c67a6ca3d..791694f14 100644 --- a/templates/table/layout-wrapper.hamlet +++ b/templates/table/layout-wrapper.hamlet @@ -1,3 +1,3 @@ $newline never -
+
^{table} From 610d13a7292f61e7892bc0f58a666011efbbbe71 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 17:15:59 +0100 Subject: [PATCH 08/35] feat(hide-columns): correct storage keys --- frontend/src/utils/hide-columns/hide-columns.js | 3 +-- templates/table/cell/header.hamlet | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index ea2da5195..744f2737f 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -2,12 +2,11 @@ import { Utility } from '../../core/utility'; import { StorageManager } from '../../lib/storage-manager/storage-manager'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; -const TABLE_HEADER_IDENT = 'hide-column-header'; +const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; - const HIDER_TEXT_ATTR = 'header-text'; const HIDE_BUTTON_FADE_DELAY = 3000; diff --git a/templates/table/cell/header.hamlet b/templates/table/cell/header.hamlet index ddea5960c..fca1c703c 100644 --- a/templates/table/cell/header.hamlet +++ b/templates/table/cell/header.hamlet @@ -1,5 +1,5 @@ $newline never -
+ $maybe flag <- sortableKey $case directions $of [SortAsc] From 42dd41f9d6109f891024c1f11cb637c246f8f315 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 17:53:03 +0100 Subject: [PATCH 09/35] refactor(storage-manager): namespace as key, values as object properties --- .../lib/storage-manager/storage-manager.js | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index 0735286c1..cb82a0006 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -14,10 +14,6 @@ export class StorageManager { this.namespace = namespace; } - toNamespace(key) { - return this.namespace + '--' + key; - } - save(key, value, options) { if (!key) { throw new Error('StorageManager.save called with invalid key'); @@ -30,9 +26,10 @@ export class StorageManager { const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; switch (lifetime) { - case LIFETIME.INFINITE: - localStorage.setItem(this.toNamespace(key), JSON.stringify(value)); + case LIFETIME.INFINITE: { + this.saveToLocalStorage({ ...this.getFromLocalStorage(), [key]: value }); break; + } default: console.error('StorageManager.save cannot save item with unsupported lifetime'); } @@ -46,17 +43,29 @@ export class StorageManager { const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; switch (lifetime) { - case LIFETIME.INFINITE: { - const value = JSON.parse(localStorage.getItem(this.toNamespace(key))); - if (value === null) { - // remove item from localStorage if it stores an invalid value (cannot be parsed) - localStorage.removeItem(this.toNamespace(key)); - } - return value; - } + case LIFETIME.INFINITE: + return this.getFromLocalStorage()[key]; default: console.error('StorageManager.load cannot load item with unsupported lifetime'); } } + getFromLocalStorage() { + const state = JSON.parse(window.localStorage.getItem(this.namespace)); + if (state === null) { + // remove item from localStorage if it stores an invalid value (cannot be parsed) + this.clearLocalStorage(); + return {}; + } + return state; + } + + saveToLocalStorage(value) { + window.localStorage.setItem(this.namespace, JSON.stringify(value)); + } + + clearLocalStorage() { + window.localStorage.removeItem(this.namespace); + } + } From bc023f5bfb55914af5bed9979e0f82b5757101c2 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 17:53:53 +0100 Subject: [PATCH 10/35] refactor(hide-columns): conform to LS naming scheme --- frontend/src/utils/hide-columns/hide-columns.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 744f2737f..33bf474b0 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -16,7 +16,7 @@ const HIDE_BUTTON_FADE_DELAY = 3000; }) export class HideColumns { - _storageManager = new StorageManager('uw-hide-columns'); + _storageManager = new StorageManager('HIDE_COLUMNS'); _element; _elementWrapper; @@ -141,7 +141,7 @@ export class HideColumns { thIdent = th.cellIndex; } - return `${handlerIdent}-${tIdent}-${thIdent}`; + return `${handlerIdent}__${tIdent}__${thIdent}`; } } From 68fc4e4c3a373d83b4557b8ae33f86c496c6933b Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Nov 2019 18:10:11 +0100 Subject: [PATCH 11/35] refactor(hide-columns): minor renaming and preparation for css work --- frontend/src/utils/hide-columns/hide-columns.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 33bf474b0..6bc0bc8af 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -57,12 +57,12 @@ export class HideColumns { } setupHideButton(th, prevHidden) { - const hider = document.createElement('button'); + const hider = document.createElement('span'); hider.setAttribute(HIDER_TEXT_ATTR, th.innerText); hider.addEventListener('click', (event) => { event.preventDefault(); - this.toggleColumnVisibility(th, hider); + this.switchColumnDisplay(th, hider); }); // TODO fade in / fade out animation in css @@ -81,23 +81,18 @@ export class HideColumns { this._tableUtilContainer.appendChild(hider); } - // TODO better name - toggleColumnVisibility(th, hider) { + switchColumnDisplay(th, hider) { const storageKey = this.getStorageKey(th); const hidden = !this._storageManager.load(storageKey); - // hide/unhide column this.updateColumnDisplay(th.cellIndex, hidden); - - // tweak hider button this.updateHider(hider, hidden); // persist new hidden setting for column this._storageManager.save(storageKey, hidden); } - // TODO better name updateColumnDisplay(columnIndex, hidden) { this._element.getElementsByTagName('tr').forEach(row => { if (row.cells[columnIndex]) { @@ -106,7 +101,6 @@ export class HideColumns { }); } - // TODO better name updateHider(hider, hidden) { // TODO set css classes instead if (hidden) { From a9c17d75fe2ec2aef65e4150d8f080797433fe0d Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 27 Nov 2019 17:01:49 +0100 Subject: [PATCH 12/35] feat(hide-columns): styling stub with repositioning --- .../src/utils/hide-columns/hide-columns.js | 65 +++++++++++++++---- .../src/utils/hide-columns/hide-columns.scss | 53 +++++++++++++++ 2 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 frontend/src/utils/hide-columns/hide-columns.scss diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 6bc0bc8af..242c51095 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { StorageManager } from '../../lib/storage-manager/storage-manager'; +import './hide-columns.scss'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; @@ -7,8 +8,6 @@ const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; -const HIDER_TEXT_ATTR = 'header-text'; - const HIDE_BUTTON_FADE_DELAY = 3000; @Utility({ @@ -22,6 +21,14 @@ export class HideColumns { _elementWrapper; _tableUtilContainer; + headerToHider = new Map(); + hiderToHeader = new Map(); + + addHeaderHider(th, hider) { + this.headerToHider.set(th, hider); + this.hiderToHeader.set(hider, th); + } + constructor(element) { if (!element) { throw new Error('Hide Columns utility cannot be setup without an element!'); @@ -58,7 +65,14 @@ export class HideColumns { setupHideButton(th, prevHidden) { const hider = document.createElement('span'); - hider.setAttribute(HIDER_TEXT_ATTR, th.innerText); + hider.classList.add('table-pill'); + const hiderIcon = document.createElement('i'); + hiderIcon.classList.add('fas', 'fa-fw'); + hider.appendChild(hiderIcon); + const hiderContent = document.createElement('span'); + hiderContent.classList.add('table-pill__content'); + hiderContent.innerHTML = th.innerText; + hider.appendChild(hiderContent); hider.addEventListener('click', (event) => { event.preventDefault(); @@ -67,17 +81,34 @@ export class HideColumns { // TODO fade in / fade out animation in css th.addEventListener('mouseover', () => { - hider.style.display = ''; + hider.classList.remove('table-pill--hidden'); + const posTH = th.getBoundingClientRect(); + const posH = hider.getBoundingClientRect(); + hider.style.left = (posTH.left + ((posTH.right-posTH.left)/2 - ((posH.right-posH.left)/2))) + 'px'; + hider.style.top = (posTH.top - 50) + 'px'; }); th.addEventListener('mouseout', () => { setTimeout(() => { - if (hider.style.position === 'absolute') hider.style.display = 'none'; + if (hider.classList.contains('table-pill--floating')) { + hider.classList.add('table-pill--hidden'); + } }, HIDE_BUTTON_FADE_DELAY); }); + hider.addEventListener('mouseover', () => { + const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); + this.updateHiderIcon(hider, !currentlyHidden); + }); + hider.addEventListener('mouseout', () => { + const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); + this.updateHiderIcon(hider, currentlyHidden); + }); + this.updateColumnDisplay(th.cellIndex, prevHidden); this.updateHider(hider, prevHidden); + this.addHeaderHider(th, hider); + this._tableUtilContainer.appendChild(hider); } @@ -95,22 +126,30 @@ export class HideColumns { updateColumnDisplay(columnIndex, hidden) { this._element.getElementsByTagName('tr').forEach(row => { - if (row.cells[columnIndex]) { - row.cells[columnIndex].style.display = hidden ? 'none' : ''; + const cell = row.cells[columnIndex]; + if (cell) { + if (hidden) { + cell.classList.add('hide-columns--hidden-cell'); + } else { + cell.classList.remove('hide-columns--hidden-cell'); + } } }); } updateHider(hider, hidden) { - // TODO set css classes instead if (hidden) { - hider.innerHTML = hider.getAttribute(HIDER_TEXT_ATTR); - hider.style.position = 'relative'; + hider.classList.remove('table-pill--hidden', 'table-pill--floating'); } else { - hider.innerHTML = 'hide'; - hider.style.position = 'absolute'; - hider.style.display = 'none'; + hider.classList.add('table-pill--hidden', 'table-pill--floating'); } + this.updateHiderIcon(hider, hidden); + } + + updateHiderIcon(hider, hidden) { + const hiderIcon = hider.getElementsByClassName('fas')[0]; + hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash'); + hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye'); } getStorageKey(th) { diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss new file mode 100644 index 000000000..82a39d97c --- /dev/null +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -0,0 +1,53 @@ +[table-utils] { + margin-bottom: 20px; + line-height: 1.4; + max-width: 85vw; + + .table-pill { + background-color: var(--color-dark); + color: #fff; + padding-top: 10px; + padding-bottom: 10px; + padding-right: 10px; + padding-left: 10px; + border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%; + margin-right: 20px; + cursor: pointer; + + .table-pill__content { + font-size: 16px; + font-weight: bold; + margin-left: 5px; + } + } + .table-pill.table-pill--hidden { + animation: fadeout 2s ease-in alternate infinite; + display: none; + } + .table-pill.table-pill--floating { + position: fixed; + .table-pill__content { display: none; } + } + + .table-pill.table-pill--hide { + padding-left: 10px; + } + + .table-pill--unhide { + padding-left: 10px; + } + +} + +.hide-columns--hidden-cell { + display: none; +} + +@keyframes fadeout { + from: { opacity: 1; } + to: { opacity: 0; display: none; } +} +@keyframes fadein { + from: { opacity: 0; } + to: { opacity: 1; } +} From e655bc6e700f1b9668633f6beaef7acf8db484f0 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 28 Nov 2019 12:33:44 +0100 Subject: [PATCH 13/35] feat(hide-columns): more (broken) styling; move hider elements in DOM --- .../src/utils/hide-columns/hide-columns.js | 82 ++++++++++++++----- .../src/utils/hide-columns/hide-columns.scss | 44 +++++----- 2 files changed, 85 insertions(+), 41 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 242c51095..0b2ee8c50 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -8,7 +8,13 @@ const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; -const HIDE_BUTTON_FADE_DELAY = 3000; +const TABLE_HIDER_CONTAINER_CLASS = 'table-hiders'; + +const HIDER_HIDE_CLASS = 'table-pill--hide'; +const HIDER_HIDDEN_CLASS = 'table-pill--hidden'; +const HIDER_FLOATING_CLASS = 'table-pill--floating'; + +const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; @Utility({ selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, @@ -20,6 +26,7 @@ export class HideColumns { _element; _elementWrapper; _tableUtilContainer; + _tableHiderContainer; headerToHider = new Map(); hiderToHeader = new Map(); @@ -56,6 +63,14 @@ export class HideColumns { hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); } + // get or create table hider container before the table + this._tableHiderContainer = this._element.previousSibling; + if (!this._tableHiderContainer || !this._tableHiderContainer.classList.contains(TABLE_HIDER_CONTAINER_CLASS)) { + this._tableHiderContainer = document.createElement('div'); + this._tableHiderContainer.classList.add(TABLE_HIDER_CONTAINER_CLASS); + this._element.parentElement.insertBefore(this._tableHiderContainer, this._element); + } + this._element.querySelectorAll('th').forEach(th => { const storageKey = this.getStorageKey(th); const previouslyHidden = this._storageManager.load(storageKey); @@ -63,17 +78,42 @@ export class HideColumns { }); } + hideHiderBehindHeader(hider) { + const th = this.hiderToHeader.get(hider); + // move hider right before table (inside any scrolltable element) + this._tableHiderContainer.appendChild(hider); + // reposition hider + const posTH = th.getBoundingClientRect(); + const posH = hider.getBoundingClientRect(); + // TODO move to aux function + let p = { + top: posTH.top, + left: posTH.left, + width: th.offsetWidth || 0, + height: th.offsetHeight || 0, + }; + const hiderCompStyle = window.getComputedStyle(hider); + const x = parseInt(hiderCompStyle.marginLeft) + parseInt(hiderCompStyle.marginRight); + const y = parseInt(hiderCompStyle.marginTop) + parseInt(hiderCompStyle.marginBottom); + const posHParent = hider.parentElement.getBoundingClientRect(); + p.top += -posHParent.top + hider.parentElement.scrollTop + hider.parentElement.offsetTop; + p.left += -posHParent.left - hider.parentElement.scrollLeft + hider.parentElement.offsetLeft; + hider.style.left = (p.left + (p.width/2) - (posH.width/2 + x)) + 'px'; + hider.style.top = (p.top - (hider.offsetHeight + y)) + 'px'; + } + setupHideButton(th, prevHidden) { const hider = document.createElement('span'); - hider.classList.add('table-pill'); const hiderIcon = document.createElement('i'); hiderIcon.classList.add('fas', 'fa-fw'); hider.appendChild(hiderIcon); const hiderContent = document.createElement('span'); - hiderContent.classList.add('table-pill__content'); + hiderContent.classList.add('table-hider__label'); hiderContent.innerHTML = th.innerText; hider.appendChild(hiderContent); + this.addHeaderHider(th, hider); + hider.addEventListener('click', (event) => { event.preventDefault(); this.switchColumnDisplay(th, hider); @@ -81,18 +121,13 @@ export class HideColumns { // TODO fade in / fade out animation in css th.addEventListener('mouseover', () => { - hider.classList.remove('table-pill--hidden'); - const posTH = th.getBoundingClientRect(); - const posH = hider.getBoundingClientRect(); - hider.style.left = (posTH.left + ((posTH.right-posTH.left)/2 - ((posH.right-posH.left)/2))) + 'px'; - hider.style.top = (posTH.top - 50) + 'px'; + // hider.classList.remove(HIDER_HIDDEN_CLASS); }); - th.addEventListener('mouseout', () => { - setTimeout(() => { - if (hider.classList.contains('table-pill--floating')) { - hider.classList.add('table-pill--hidden'); - } - }, HIDE_BUTTON_FADE_DELAY); + th.addEventListener('mouseout', (event) => { + console.log('th mouseout', event); + if (hider.classList.contains(HIDER_FLOATING_CLASS)) { + hider.classList.add(HIDER_HIDE_CLASS); + } }); hider.addEventListener('mouseover', () => { @@ -107,9 +142,12 @@ export class HideColumns { this.updateColumnDisplay(th.cellIndex, prevHidden); this.updateHider(hider, prevHidden); - this.addHeaderHider(th, hider); + if (prevHidden) { + this._tableUtilContainer.appendChild(hider); + } else { + this.hideHiderBehindHeader(hider); + } - this._tableUtilContainer.appendChild(hider); } switchColumnDisplay(th, hider) { @@ -129,9 +167,9 @@ export class HideColumns { const cell = row.cells[columnIndex]; if (cell) { if (hidden) { - cell.classList.add('hide-columns--hidden-cell'); + cell.classList.add(CELL_HIDDEN_CLASS); } else { - cell.classList.remove('hide-columns--hidden-cell'); + cell.classList.remove(CELL_HIDDEN_CLASS); } } }); @@ -139,9 +177,13 @@ export class HideColumns { updateHider(hider, hidden) { if (hidden) { - hider.classList.remove('table-pill--hidden', 'table-pill--floating'); + this._tableUtilContainer.appendChild(hider); + hider.classList.remove(HIDER_HIDDEN_CLASS, HIDER_FLOATING_CLASS, 'table-hider'); + hider.classList.add('table-pill'); } else { - hider.classList.add('table-pill--hidden', 'table-pill--floating'); + + hider.classList.remove('table-pill'); + hider.classList.add(HIDER_HIDDEN_CLASS, HIDER_FLOATING_CLASS, 'table-hider'); } this.updateHiderIcon(hider, hidden); } diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 82a39d97c..a6066283d 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -2,6 +2,21 @@ margin-bottom: 20px; line-height: 1.4; max-width: 85vw; +} + + .table-hider { + background-color: #fff; + color: var(--color-link); + padding: 10px; + cursor: pointer; + box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); + margin: 10px 10px 0 0; + position: absolute; + + .table-hider__label { + display: none; + } + } .table-pill { background-color: var(--color-dark); @@ -14,40 +29,27 @@ margin-right: 20px; cursor: pointer; - .table-pill__content { + .table-hider__label { font-size: 16px; font-weight: bold; margin-left: 5px; } } - .table-pill.table-pill--hidden { - animation: fadeout 2s ease-in alternate infinite; - display: none; - } - .table-pill.table-pill--floating { - position: fixed; - .table-pill__content { display: none; } - } - .table-pill.table-pill--hide { - padding-left: 10px; + /* -webkit-animation: fadeout 0.5s linear forwards; */ + /* animation: fadeout 0.5s linear forwards; */ + /* animation-delay: 2s; */ } - .table-pill--unhide { - padding-left: 10px; - } - -} - .hide-columns--hidden-cell { display: none; } @keyframes fadeout { - from: { opacity: 1; } - to: { opacity: 0; display: none; } + 0% { opacity: 1; } + 100% { opacity: 0; } } @keyframes fadein { - from: { opacity: 0; } - to: { opacity: 1; } + 0% { opacity: 0; } + 100% { opacity: 1; } } From 761c6d39a82a57e4bdddd193d25be459b12ca1f7 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 28 Nov 2019 13:14:34 +0100 Subject: [PATCH 14/35] feat(hide-columns): better positioning of hiders --- .../src/utils/hide-columns/hide-columns.js | 23 ++++--------------- .../src/utils/hide-columns/hide-columns.scss | 16 ++++++++----- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 0b2ee8c50..0cf01df51 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -79,27 +79,14 @@ export class HideColumns { } hideHiderBehindHeader(hider) { - const th = this.hiderToHeader.get(hider); // move hider right before table (inside any scrolltable element) this._tableHiderContainer.appendChild(hider); // reposition hider - const posTH = th.getBoundingClientRect(); - const posH = hider.getBoundingClientRect(); - // TODO move to aux function - let p = { - top: posTH.top, - left: posTH.left, - width: th.offsetWidth || 0, - height: th.offsetHeight || 0, - }; - const hiderCompStyle = window.getComputedStyle(hider); - const x = parseInt(hiderCompStyle.marginLeft) + parseInt(hiderCompStyle.marginRight); - const y = parseInt(hiderCompStyle.marginTop) + parseInt(hiderCompStyle.marginBottom); - const posHParent = hider.parentElement.getBoundingClientRect(); - p.top += -posHParent.top + hider.parentElement.scrollTop + hider.parentElement.offsetTop; - p.left += -posHParent.left - hider.parentElement.scrollLeft + hider.parentElement.offsetLeft; - hider.style.left = (p.left + (p.width/2) - (posH.width/2 + x)) + 'px'; - hider.style.top = (p.top - (hider.offsetHeight + y)) + 'px'; + const thR = this.hiderToHeader.get(hider).getBoundingClientRect(); + const hR = hider.getBoundingClientRect(); + const pR = this._tableHiderContainer.getBoundingClientRect(); + hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; + hider.style.top = (thR.top - pR.top + 5) + 'px'; } setupHideButton(th, prevHidden) { diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index a6066283d..a2e9ed957 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -1,8 +1,6 @@ -[table-utils] { - margin-bottom: 20px; - line-height: 1.4; - max-width: 85vw; -} +.table-hiders { + position: relative; + max-height: 0px; .table-hider { background-color: #fff; @@ -10,13 +8,18 @@ padding: 10px; cursor: pointer; box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); - margin: 10px 10px 0 0; position: absolute; .table-hider__label { display: none; } } +} + +[table-utils] { + margin-bottom: 20px; + line-height: 1.4; + max-width: 85vw; .table-pill { background-color: var(--color-dark); @@ -40,6 +43,7 @@ /* animation: fadeout 0.5s linear forwards; */ /* animation-delay: 2s; */ } +} .hide-columns--hidden-cell { display: none; From 49087027b250a8ce1212d508e1c9028f5122a09e Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 28 Nov 2019 15:44:53 +0100 Subject: [PATCH 15/35] feat(hide-columns): more styling --- .../src/utils/hide-columns/hide-columns.js | 28 ++++++++++--------- .../src/utils/hide-columns/hide-columns.scss | 15 +++++++++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 0cf01df51..976c24622 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -9,10 +9,9 @@ const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; const TABLE_HIDER_CONTAINER_CLASS = 'table-hiders'; +const TABLE_HIDER_CLASS = 'table-hider'; -const HIDER_HIDE_CLASS = 'table-pill--hide'; -const HIDER_HIDDEN_CLASS = 'table-pill--hidden'; -const HIDER_FLOATING_CLASS = 'table-pill--floating'; +const HIDER_POPUP_CLASS = 'table-hider--visible'; const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; @@ -86,7 +85,7 @@ export class HideColumns { const hR = hider.getBoundingClientRect(); const pR = this._tableHiderContainer.getBoundingClientRect(); hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; - hider.style.top = (thR.top - pR.top + 5) + 'px'; + hider.style.top = (thR.top - pR.top + 5 +50) + 'px'; } setupHideButton(th, prevHidden) { @@ -106,22 +105,24 @@ export class HideColumns { this.switchColumnDisplay(th, hider); }); - // TODO fade in / fade out animation in css th.addEventListener('mouseover', () => { - // hider.classList.remove(HIDER_HIDDEN_CLASS); + hider.classList.add(HIDER_POPUP_CLASS); }); - th.addEventListener('mouseout', (event) => { - console.log('th mouseout', event); - if (hider.classList.contains(HIDER_FLOATING_CLASS)) { - hider.classList.add(HIDER_HIDE_CLASS); + th.addEventListener('mouseout', () => { + if (hider.classList.contains(TABLE_HIDER_CLASS)) { + hider.classList.remove(HIDER_POPUP_CLASS); } }); hider.addEventListener('mouseover', () => { + hider.classList.add(HIDER_POPUP_CLASS); const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); this.updateHiderIcon(hider, !currentlyHidden); }); hider.addEventListener('mouseout', () => { + if (hider.classList.contains(TABLE_HIDER_CLASS)) { + hider.classList.remove(HIDER_POPUP_CLASS); + } const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); this.updateHiderIcon(hider, currentlyHidden); }); @@ -165,12 +166,13 @@ export class HideColumns { updateHider(hider, hidden) { if (hidden) { this._tableUtilContainer.appendChild(hider); - hider.classList.remove(HIDER_HIDDEN_CLASS, HIDER_FLOATING_CLASS, 'table-hider'); + hider.classList.remove(TABLE_HIDER_CLASS); hider.classList.add('table-pill'); } else { - + this.hideHiderBehindHeader(hider); + this._tableHiderContainer.appendChild(hider); hider.classList.remove('table-pill'); - hider.classList.add(HIDER_HIDDEN_CLASS, HIDER_FLOATING_CLASS, 'table-hider'); + hider.classList.add(TABLE_HIDER_CLASS); } this.updateHiderIcon(hider, hidden); } diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index a2e9ed957..070ff88d7 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -5,14 +5,27 @@ .table-hider { background-color: #fff; color: var(--color-link); - padding: 10px; + padding: 0 10px; + height: 0; cursor: pointer; box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); position: absolute; + overflow: hidden; + transition: .3s; + + &:hover { + background-color: var(--color-grey-light); + } .table-hider__label { display: none; } + + &.table-hider--visible { + display: flex; + padding: 10px; + height: auto; + } } } From 9e449ddaed31aba904d7a6bbd367cc256180d237 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 28 Nov 2019 15:45:36 +0100 Subject: [PATCH 16/35] fix(hide-columns): remove debug text from template --- templates/table/colonnade.hamlet | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/table/colonnade.hamlet b/templates/table/colonnade.hamlet index f13131530..081195f60 100644 --- a/templates/table/colonnade.hamlet +++ b/templates/table/colonnade.hamlet @@ -1,6 +1,5 @@ $newline never
- Table Utils:
$maybe wHeaders' <- wHeaders From 506f94e5d4afd2e2a955dab654f9a566a4152728 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 14:24:58 +0100 Subject: [PATCH 17/35] feat(hide-columns): fadein transformation --- .../src/utils/hide-columns/hide-columns.js | 1 + .../src/utils/hide-columns/hide-columns.scss | 26 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 976c24622..e8c826408 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -103,6 +103,7 @@ export class HideColumns { hider.addEventListener('click', (event) => { event.preventDefault(); this.switchColumnDisplay(th, hider); + this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); }); th.addEventListener('mouseover', () => { diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 070ff88d7..484e02b49 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -5,13 +5,15 @@ .table-hider { background-color: #fff; color: var(--color-link); - padding: 0 10px; - height: 0; + padding: 10px; cursor: pointer; box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); position: absolute; overflow: hidden; - transition: .3s; + + transition: transform .2s ease 1s; + transform: scaleY(0); + transform-origin: top; &:hover { background-color: var(--color-grey-light); @@ -22,25 +24,20 @@ } &.table-hider--visible { - display: flex; - padding: 10px; - height: auto; + transform: scaleY(1); } } } [table-utils] { - margin-bottom: 20px; - line-height: 1.4; max-width: 85vw; + margin-bottom: 20px; + min-height: 0; .table-pill { background-color: var(--color-dark); color: #fff; - padding-top: 10px; - padding-bottom: 10px; - padding-right: 10px; - padding-left: 10px; + padding: 10px; border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%; margin-right: 20px; cursor: pointer; @@ -51,11 +48,6 @@ margin-left: 5px; } } - .table-pill.table-pill--hide { - /* -webkit-animation: fadeout 0.5s linear forwards; */ - /* animation: fadeout 0.5s linear forwards; */ - /* animation-delay: 2s; */ - } } .hide-columns--hidden-cell { From 8e03a68e6d974b8b4157864fc93bf0ec5e984feb Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 14:41:23 +0100 Subject: [PATCH 18/35] style(hide-columns): no table margin-top with table-hider sibling above --- frontend/src/utils/hide-columns/hide-columns.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 484e02b49..5899dd08d 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -27,6 +27,10 @@ transform: scaleY(1); } } + + &+.table { + margin-top: 0px; + } } [table-utils] { From 9d8ca38f2e7016b753d7951a12b4b10ac3089c9f Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 15:06:28 +0100 Subject: [PATCH 19/35] fix(hide-columns): fix repositioning of table hiders onclick --- frontend/src/utils/hide-columns/hide-columns.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index e8c826408..fd3c94d32 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -78,9 +78,9 @@ export class HideColumns { } hideHiderBehindHeader(hider) { - // move hider right before table (inside any scrolltable element) - this._tableHiderContainer.appendChild(hider); - // reposition hider + if (!this._tableHiderContainer.contains(hider)) { + this._tableHiderContainer.appendChild(hider); + } const thR = this.hiderToHeader.get(hider).getBoundingClientRect(); const hR = hider.getBoundingClientRect(); const pR = this._tableHiderContainer.getBoundingClientRect(); @@ -103,7 +103,11 @@ export class HideColumns { hider.addEventListener('click', (event) => { event.preventDefault(); this.switchColumnDisplay(th, hider); - this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); + + // recompute position for every table hider + this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => { + this.hideHiderBehindHeader(hider); + }); }); th.addEventListener('mouseover', () => { From c2eb9fbc171a08c37a31bcaff3447824f0ab8ebf Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 16:51:33 +0100 Subject: [PATCH 20/35] style(hide-columns): fix line-breaking of table pills --- frontend/src/utils/hide-columns/hide-columns.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 5899dd08d..394078c1c 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -35,15 +35,18 @@ [table-utils] { max-width: 85vw; - margin-bottom: 20px; + margin-bottom: 10px; min-height: 0; + line-height: 1.4; .table-pill { background-color: var(--color-dark); + float: left; color: #fff; padding: 10px; border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%; margin-right: 20px; + margin-bottom: 10px; cursor: pointer; .table-hider__label { @@ -52,6 +55,12 @@ margin-left: 5px; } } + + &:after { + content: ""; + display: block; + clear: both; + } } .hide-columns--hidden-cell { From 364991c42bbb82301dd71cee8823d061f92da1ad Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 17:18:28 +0100 Subject: [PATCH 21/35] fix(hide-columns): correctly hide hiders of previously hidden columns --- frontend/src/utils/hide-columns/hide-columns.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index fd3c94d32..a6743bd8b 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -86,6 +86,9 @@ export class HideColumns { const pR = this._tableHiderContainer.getBoundingClientRect(); hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; hider.style.top = (thR.top - pR.top + 5 +50) + 'px'; + + // remove visible class if necessary + hider.classList.remove('table-hider--visible'); } setupHideButton(th, prevHidden) { @@ -105,9 +108,7 @@ export class HideColumns { this.switchColumnDisplay(th, hider); // recompute position for every table hider - this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => { - this.hideHiderBehindHeader(hider); - }); + this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); }); th.addEventListener('mouseover', () => { From 3fbb4db962c6493a1da2c243850d65e4f735ab4b Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 29 Nov 2019 17:52:32 +0100 Subject: [PATCH 22/35] fix(hide-columns): fix vertical positioning of hider and minor refactor --- .../src/utils/hide-columns/hide-columns.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index a6743bd8b..33de28005 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -10,8 +10,9 @@ const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; const TABLE_HIDER_CONTAINER_CLASS = 'table-hiders'; const TABLE_HIDER_CLASS = 'table-hider'; +const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible'; -const HIDER_POPUP_CLASS = 'table-hider--visible'; +const TABLE_PILL_CLASS = 'table-pill'; const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; @@ -85,10 +86,10 @@ export class HideColumns { const hR = hider.getBoundingClientRect(); const pR = this._tableHiderContainer.getBoundingClientRect(); hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; - hider.style.top = (thR.top - pR.top + 5 +50) + 'px'; + hider.style.top = (thR.top - pR.top + thR.height) + 'px'; // remove visible class if necessary - hider.classList.remove('table-hider--visible'); + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } setupHideButton(th, prevHidden) { @@ -112,22 +113,22 @@ export class HideColumns { }); th.addEventListener('mouseover', () => { - hider.classList.add(HIDER_POPUP_CLASS); + hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); }); th.addEventListener('mouseout', () => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { - hider.classList.remove(HIDER_POPUP_CLASS); + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } }); hider.addEventListener('mouseover', () => { - hider.classList.add(HIDER_POPUP_CLASS); + hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); this.updateHiderIcon(hider, !currentlyHidden); }); hider.addEventListener('mouseout', () => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { - hider.classList.remove(HIDER_POPUP_CLASS); + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); this.updateHiderIcon(hider, currentlyHidden); @@ -173,11 +174,11 @@ export class HideColumns { if (hidden) { this._tableUtilContainer.appendChild(hider); hider.classList.remove(TABLE_HIDER_CLASS); - hider.classList.add('table-pill'); + hider.classList.add(TABLE_PILL_CLASS); } else { this.hideHiderBehindHeader(hider); this._tableHiderContainer.appendChild(hider); - hider.classList.remove('table-pill'); + hider.classList.remove(TABLE_PILL_CLASS); hider.classList.add(TABLE_HIDER_CLASS); } this.updateHiderIcon(hider, hidden); From d1232ce72d45ddcabe14f9770a0f8117d8efe74d Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Dec 2019 11:19:50 +0100 Subject: [PATCH 23/35] feat(hide-columns): hide empty columns per default --- .../src/utils/hide-columns/hide-columns.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 33de28005..f8d2cd755 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -5,6 +5,8 @@ import './hide-columns.scss'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; +const ASYNC_TABLE_IDENT = 'uw-async-table'; + const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; @@ -158,6 +160,8 @@ export class HideColumns { } updateColumnDisplay(columnIndex, hidden) { + let isColumnWithContent = false; + this._element.getElementsByTagName('tr').forEach(row => { const cell = row.cells[columnIndex]; if (cell) { @@ -165,9 +169,26 @@ export class HideColumns { cell.classList.add(CELL_HIDDEN_CLASS); } else { cell.classList.remove(CELL_HIDDEN_CLASS); + + // determine if this cell has content + if (cell.nodeName === 'TD') { + let cellHasContent = false; + if (this._elementWrapper.hasAttribute(ASYNC_TABLE_IDENT)) { + cell.children.forEach(child => { + cellHasContent = cellHasContent || !isEmptyElement(child); + }); + } else { + cellHasContent = !isEmptyElement(cell); + } + isColumnWithContent = isColumnWithContent || cellHasContent; + } } } }); + + if (!hidden && !isColumnWithContent) { + this.updateColumnDisplay(columnIndex, true); + } } updateHider(hider, hidden) { @@ -216,3 +237,12 @@ export class HideColumns { } } + +function isEmptyElement(element) { + for (let child of element.childNodes) { + if (child.nodeName !== '#comment') + return false; + } + return true; +} + From 047c0a5787657be7f810b20599bb1015a99b4e9b Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Dec 2019 18:19:50 +0100 Subject: [PATCH 24/35] feat(hide-columns): refactor and auto-hide empty columns --- .../src/utils/hide-columns/hide-columns.js | 136 ++++++++++-------- .../src/utils/hide-columns/hide-columns.scss | 26 ++-- stack.yaml.lock | 43 +++++- templates/default-layout.lucius | 8 +- 4 files changed, 135 insertions(+), 78 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index f8d2cd755..c8599e17e 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -73,32 +73,18 @@ export class HideColumns { this._element.parentElement.insertBefore(this._tableHiderContainer, this._element); } - this._element.querySelectorAll('th').forEach(th => { - const storageKey = this.getStorageKey(th); - const previouslyHidden = this._storageManager.load(storageKey); - this.setupHideButton(th, previouslyHidden); - }); + this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th)); } - hideHiderBehindHeader(hider) { - if (!this._tableHiderContainer.contains(hider)) { - this._tableHiderContainer.appendChild(hider); - } - const thR = this.hiderToHeader.get(hider).getBoundingClientRect(); - const hR = hider.getBoundingClientRect(); - const pR = this._tableHiderContainer.getBoundingClientRect(); - hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; - hider.style.top = (thR.top - pR.top + thR.height) + 'px'; + setupHideButton(th) { + const preHidden = this.isHiddenColumn(th); - // remove visible class if necessary - hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); - } - - setupHideButton(th, prevHidden) { const hider = document.createElement('span'); + const hiderIcon = document.createElement('i'); hiderIcon.classList.add('fas', 'fa-fw'); hider.appendChild(hiderIcon); + const hiderContent = document.createElement('span'); hiderContent.classList.add('table-hider__label'); hiderContent.innerHTML = th.innerText; @@ -106,14 +92,6 @@ export class HideColumns { this.addHeaderHider(th, hider); - hider.addEventListener('click', (event) => { - event.preventDefault(); - this.switchColumnDisplay(th, hider); - - // recompute position for every table hider - this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); - }); - th.addEventListener('mouseover', () => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); }); @@ -123,45 +101,51 @@ export class HideColumns { } }); + hider.addEventListener('click', (event) => { + event.preventDefault(); + this.switchColumnDisplay(th, hider); + this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); + }); + hider.addEventListener('mouseover', () => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); - const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); + const currentlyHidden = this.isHiddenColumn(th); this.updateHiderIcon(hider, !currentlyHidden); }); hider.addEventListener('mouseout', () => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } - const currentlyHidden = this._storageManager.load(this.getStorageKey(th)); + const currentlyHidden = this.isHiddenColumn(th); this.updateHiderIcon(hider, currentlyHidden); }); - this.updateColumnDisplay(th.cellIndex, prevHidden); - this.updateHider(hider, prevHidden); + // reposition hider on each window resize event + window.addEventListener('resize', () => this.repositionHider(hider)); - if (prevHidden) { + if (preHidden) { this._tableUtilContainer.appendChild(hider); } else { this.hideHiderBehindHeader(hider); } + this.updateColumnDisplay(th.cellIndex, preHidden); + this.updateHider(hider, preHidden); + + this._tableHiderContainer.children.forEach(hider => this.repositionHider(hider)); } switchColumnDisplay(th, hider) { - const storageKey = this.getStorageKey(th); - - const hidden = !this._storageManager.load(storageKey); + const hidden = !this.isHiddenColumn(th); this.updateColumnDisplay(th.cellIndex, hidden); this.updateHider(hider, hidden); // persist new hidden setting for column - this._storageManager.save(storageKey, hidden); + this._storageManager.save(this.getStorageKey(th), hidden); } updateColumnDisplay(columnIndex, hidden) { - let isColumnWithContent = false; - this._element.getElementsByTagName('tr').forEach(row => { const cell = row.cells[columnIndex]; if (cell) { @@ -169,46 +153,49 @@ export class HideColumns { cell.classList.add(CELL_HIDDEN_CLASS); } else { cell.classList.remove(CELL_HIDDEN_CLASS); - - // determine if this cell has content - if (cell.nodeName === 'TD') { - let cellHasContent = false; - if (this._elementWrapper.hasAttribute(ASYNC_TABLE_IDENT)) { - cell.children.forEach(child => { - cellHasContent = cellHasContent || !isEmptyElement(child); - }); - } else { - cellHasContent = !isEmptyElement(cell); - } - isColumnWithContent = isColumnWithContent || cellHasContent; - } } } }); - - if (!hidden && !isColumnWithContent) { - this.updateColumnDisplay(columnIndex, true); - } } updateHider(hider, hidden) { if (hidden) { - this._tableUtilContainer.appendChild(hider); hider.classList.remove(TABLE_HIDER_CLASS); hider.classList.add(TABLE_PILL_CLASS); + this._tableUtilContainer.appendChild(hider); } else { - this.hideHiderBehindHeader(hider); - this._tableHiderContainer.appendChild(hider); hider.classList.remove(TABLE_PILL_CLASS); hider.classList.add(TABLE_HIDER_CLASS); + this.hideHiderBehindHeader(hider); } this.updateHiderIcon(hider, hidden); } updateHiderIcon(hider, hidden) { - const hiderIcon = hider.getElementsByClassName('fas')[0]; - hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash'); - hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye'); + hider.getElementsByClassName('fas').forEach(hiderIcon => { + hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash'); + hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye'); + }); + } + + hideHiderBehindHeader(hider) { + if (!this._tableHiderContainer.contains(hider)) { + this._tableHiderContainer.appendChild(hider); + } + + this.repositionHider(hider); + + // remove visible class if necessary + hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); + } + + repositionHider(hider) { + const thR = this.hiderToHeader.get(hider).getBoundingClientRect(), + hR = hider.getBoundingClientRect(), + pR = this._tableHiderContainer.getBoundingClientRect(); + + hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; + hider.style.top = (thR.top - pR.top + thR.height) + 'px'; } getStorageKey(th) { @@ -236,6 +223,33 @@ export class HideColumns { return `${handlerIdent}__${tIdent}__${thIdent}`; } + isEmptyColumn(columnIndex) { + for (let row of this._element.getElementsByTagName('TR')) { + if (row.children.length <= columnIndex) { + throw new Error('Invalid column index for table'); + } + + const cell = row.children[columnIndex]; + if (cell.tagName === 'TH') + continue; + if (this._element.closest(`[${ASYNC_TABLE_IDENT}]`)) { + for (let child of cell.children) { + if (!isEmptyElement(child)) + return false; + } + return true; + } else { + return isEmptyElement(cell); + } + } + } + + isHiddenColumn(th) { + const hidden = this._storageManager.load(this.getStorageKey(th)), + emptyColumn = this.isEmptyColumn(th.cellIndex); + return hidden === true || hidden === undefined && emptyColumn; + } + } function isEmptyElement(element) { diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 394078c1c..d3df5addc 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -11,7 +11,7 @@ position: absolute; overflow: hidden; - transition: transform .2s ease 1s; + /* transition: transform .2s ease; */ transform: scaleY(0); transform-origin: top; @@ -24,7 +24,20 @@ } &.table-hider--visible { - transform: scaleY(1); + transform: scaleY(.4); + transition: none; /* TODO find better way to prevent transition on icons */ + + .fas { + transform: scaleY(2.5); + } + + &:hover { + transform: scaleY(1); + + .fas { + transform: scaleY(1); + } + } } } @@ -66,12 +79,3 @@ .hide-columns--hidden-cell { display: none; } - -@keyframes fadeout { - 0% { opacity: 1; } - 100% { opacity: 0; } -} -@keyframes fadein { - 0% { opacity: 0; } - 100% { opacity: 1; } -} diff --git a/stack.yaml.lock b/stack.yaml.lock index 3758cb266..3a7017a21 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -138,12 +138,33 @@ packages: original: hackage: HaXml-1.25.5 - completed: - hackage: esqueleto-3.0.0@sha256:efd84fd11ceaf0ae4e1b0c6236122b1f213c2c6f2f4f58e30f03eddc2ec3f423,5248 + hackage: persistent-2.10.4@sha256:16c4c0823dd5e16bac4d607895ab0f4febd0626c020e5755ed1a52bf04068148,4738 pantry-tree: - size: 1127 - sha256: 74e43834d5cc468acc3cb6e8a81567ebfbb5350a3e07ae01dd7f30d6255274a1 + size: 2094 + sha256: b40d1783b539ddbbceaa827bf286d0b3bfcf76ca19e604c9d510b2a64008714e original: - hackage: esqueleto-3.0.0 + hackage: persistent-2.10.4 +- completed: + hackage: persistent-postgresql-2.10.1@sha256:ea53a0f1f4223b4884b5e19511325367879560d2432a02a976aa4da57c5fb760,2871 + pantry-tree: + size: 740 + sha256: 3cdbc757b1cebb65542fb919369be238b3f120adc45f023084a8b64c214d9675 + original: + hackage: persistent-postgresql-2.10.1 +- completed: + hackage: persistent-template-2.7.3@sha256:ac3e5e8c48e968b927bbf4e97162c52e7e417d69b05efeb1c581d7c682e043d2,2703 + pantry-tree: + size: 560 + sha256: fdfb2a721eb9c9831d7381d36bc52de0808a008ed3d553b6490080f337249684 + original: + hackage: persistent-template-2.7.3 +- completed: + hackage: esqueleto-3.2.3@sha256:5e1e0a8600e2744127ef4bb5956fa84ae6bc1fc337c7b8726fabb7ca53e2d9b3,5466 + pantry-tree: + size: 1461 + sha256: f6215274a43addd339f8bc89f1ca0e8fdfb08180b13d779ae8f7e360acc4c473 + original: + hackage: esqueleto-3.2.3 - completed: hackage: HaskellNet-SSL-0.3.4.1@sha256:3ca14dd69460a380cf69aed40654fb10c4c03e344632b6a9986568c87feda157,1843 pantry-tree: @@ -242,6 +263,20 @@ packages: sha256: 9ed161eadfda5b1eb36cfcf077146f7b66db1da69f1041fc720aea287ec021b0 original: hackage: generic-lens-1.2.0.0 +- completed: + hackage: prometheus-metrics-ghc-1.0.0@sha256:0f4ecbefa810bd847e66c498ab3387bf21e426525a7c9a94841973c582719ba3,1231 + pantry-tree: + size: 293 + sha256: 8a6d6ef3235ab980e867f64b712b5d38f1a84c3ac4920f5b4c3b3e63bcdf6ec9 + original: + hackage: prometheus-metrics-ghc-1.0.0 +- completed: + hackage: wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314 + pantry-tree: + size: 307 + sha256: 6d64803c639ed4c7204ea6fab0536b97d3ee16cdecb9b4a883cd8e56d3c61402 + original: + hackage: wai-middleware-prometheus-1.0.0 snapshots: - completed: size: 498180 diff --git a/templates/default-layout.lucius b/templates/default-layout.lucius index 11355082b..0f09cb225 100644 --- a/templates/default-layout.lucius +++ b/templates/default-layout.lucius @@ -329,8 +329,12 @@ input[type="button"].btn-info:hover, width: 100%; } -.table:only-child { - margin: 0; +.table:first-child { + margin-top: 0; +} + +.table:last-child { + margin-bottom: 0; } .table--striped { From 827cecda8f4244861f05ec988c00929591c94bec Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Dec 2019 18:27:14 +0100 Subject: [PATCH 25/35] fix(hide-columns): fix crash if no row is present --- frontend/src/utils/hide-columns/hide-columns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index c8599e17e..766277170 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -226,7 +226,7 @@ export class HideColumns { isEmptyColumn(columnIndex) { for (let row of this._element.getElementsByTagName('TR')) { if (row.children.length <= columnIndex) { - throw new Error('Invalid column index for table'); + return; } const cell = row.children[columnIndex]; From ff366a785b9991f21806f2d54f4c99170ce1cf38 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Dec 2019 19:35:03 +0100 Subject: [PATCH 26/35] style(hide-columns): hide hiders on setup --- frontend/src/utils/hide-columns/hide-columns.js | 6 +++--- frontend/src/utils/hide-columns/hide-columns.scss | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 766277170..1901130c6 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -123,15 +123,15 @@ export class HideColumns { // reposition hider on each window resize event window.addEventListener('resize', () => this.repositionHider(hider)); + this.updateColumnDisplay(th.cellIndex, preHidden); + this.updateHider(hider, preHidden); + if (preHidden) { this._tableUtilContainer.appendChild(hider); } else { this.hideHiderBehindHeader(hider); } - this.updateColumnDisplay(th.cellIndex, preHidden); - this.updateHider(hider, preHidden); - this._tableHiderContainer.children.forEach(hider => this.repositionHider(hider)); } diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index d3df5addc..77a73bee3 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -10,8 +10,7 @@ box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); position: absolute; overflow: hidden; - - /* transition: transform .2s ease; */ + transition: transform .2s ease; transform: scaleY(0); transform-origin: top; @@ -57,7 +56,7 @@ float: left; color: #fff; padding: 10px; - border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%; + border-radius: 20px / 50%; margin-right: 20px; margin-bottom: 10px; cursor: pointer; From 615555eb597bf4a87b83020275cce7f30495d0ac Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Dec 2019 19:44:15 +0100 Subject: [PATCH 27/35] fix(hide-columns): check for content div in isEmptyColumn --- frontend/src/utils/hide-columns/hide-columns.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 1901130c6..e10627a71 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -5,8 +5,6 @@ import './hide-columns.scss'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const TABLE_HEADER_IDENT = 'uw-hide-column-header'; -const ASYNC_TABLE_IDENT = 'uw-async-table'; - const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; @@ -232,7 +230,7 @@ export class HideColumns { const cell = row.children[columnIndex]; if (cell.tagName === 'TH') continue; - if (this._element.closest(`[${ASYNC_TABLE_IDENT}]`)) { + if (cell.querySelector('.table__td-content')) { for (let child of cell.children) { if (!isEmptyElement(child)) return false; From 0798d6870e7b4dfd0baf23940fe5d87b9c2d444c Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 15:58:09 +0100 Subject: [PATCH 28/35] feat(hide-columns): support colspan & don't persist autohide --- .../lib/storage-manager/storage-manager.js | 21 ++++- .../src/utils/hide-columns/hide-columns.js | 88 +++++++++++++++---- 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index cb82a0006..8297627fa 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -50,6 +50,26 @@ export class StorageManager { } } + remove(key, options) { + if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { + throw new Error('StorageManager.load called with unsupported lifetime option'); + } + + const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; + + switch (lifetime) { + case LIFETIME.INFINITE: { + var val = this.getFromLocalStorage(); + + delete val[key]; + + return this.saveToLocalStorage(val); + } + default: + console.error('StorageManager.load cannot load item with unsupported lifetime'); + } + } + getFromLocalStorage() { const state = JSON.parse(window.localStorage.getItem(this.namespace)); if (state === null) { @@ -67,5 +87,4 @@ export class StorageManager { clearLocalStorage() { window.localStorage.removeItem(this.namespace); } - } diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index e10627a71..c4a1f6f0f 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -15,6 +15,7 @@ const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible'; const TABLE_PILL_CLASS = 'table-pill'; const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell'; +const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; @Utility({ selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, @@ -121,7 +122,7 @@ export class HideColumns { // reposition hider on each window resize event window.addEventListener('resize', () => this.repositionHider(hider)); - this.updateColumnDisplay(th.cellIndex, preHidden); + this.updateColumnDisplay(this.colIndex(th), preHidden); this.updateHider(hider, preHidden); if (preHidden) { @@ -135,22 +136,46 @@ export class HideColumns { switchColumnDisplay(th, hider) { const hidden = !this.isHiddenColumn(th); + const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1; + const colspan = Math.max(1, th.colSpan) || 1; + const columnIndex = this.colIndex(th); - this.updateColumnDisplay(th.cellIndex, hidden); + for (var i = 0; i < Math.max(colspan, originalColspan); i++) { + this.updateColumnDisplay(columnIndex + i, hidden); + } this.updateHider(hider, hidden); // persist new hidden setting for column - this._storageManager.save(this.getStorageKey(th), hidden); + if (hidden && this.isEmptyColumn(columnIndex)) { + this._storageManager.remove(this.getStorageKey(th)); + } else { + this._storageManager.save(this.getStorageKey(th), hidden); + } } updateColumnDisplay(columnIndex, hidden) { this._element.getElementsByTagName('tr').forEach(row => { - const cell = row.cells[columnIndex]; + const cell = this.getCol(row, columnIndex); + if (cell) { + const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); + const colspan = Math.max(1, cell.colSpan) || 1; + if (hidden) { - cell.classList.add(CELL_HIDDEN_CLASS); + if (colspan > 1) { + if (!originalColspan) { + cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan); + } + cell.colSpan--; + } else { + cell.classList.add(CELL_HIDDEN_CLASS); + } } else { - cell.classList.remove(CELL_HIDDEN_CLASS); + if (cell.classList.contains(CELL_HIDDEN_CLASS)) { + cell.classList.remove(CELL_HIDDEN_CLASS); + } else if (originalColspan && colspan < originalColspan) { + cell.colSpan++; + } } } }); @@ -215,20 +240,16 @@ export class HideColumns { // check for unique table header ident from backend (if not present, use cell index as fallback) let thIdent = th.getAttribute(TABLE_HEADER_IDENT); if (!thIdent) { - thIdent = th.cellIndex; + thIdent = this.colIndex(th); } return `${handlerIdent}__${tIdent}__${thIdent}`; } isEmptyColumn(columnIndex) { - for (let row of this._element.getElementsByTagName('TR')) { - if (row.children.length <= columnIndex) { - return; - } - - const cell = row.children[columnIndex]; - if (cell.tagName === 'TH') + for (let row of this._element.getElementsByTagName('tr')) { + const cell = this.getCol(row, columnIndex); + if (cell.matches('th')) continue; if (cell.querySelector('.table__td-content')) { for (let child of cell.children) { @@ -244,10 +265,47 @@ export class HideColumns { isHiddenColumn(th) { const hidden = this._storageManager.load(this.getStorageKey(th)), - emptyColumn = this.isEmptyColumn(th.cellIndex); + emptyColumn = this.isEmptyColumn(this.colIndex(th)); return hidden === true || hidden === undefined && emptyColumn; } + colSpan(cell) { + if (!cell) + return 1; + + const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); + const colspan = Math.max(1, cell.colSpan) || 1; + + return originalColspan ? Math.max(colspan, originalColspan) : colspan; + } + + colIndex(cell) { + if (!cell) + return 0; + + const rowParent = cell.closest('tr'); + + if (!rowParent) + return 0; + + var i = 0; + for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) { + i += this.colSpan(sibling); + } + + return i; + } + + getCol(row, columnIndex) { + var c = 0; + + for (const cell of row.cells) { + c += cell ? this.colSpan(cell) : 1; + + if (columnIndex < c) + return cell; + } + } } function isEmptyElement(element) { From 80ff4ac2a734bf295a026aa68d5d881da9cca3bd Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 17:02:32 +0100 Subject: [PATCH 29/35] feat(storage-manager): location hierarchy --- .../lib/storage-manager/storage-manager.js | 174 +++++++++++++----- .../src/utils/hide-columns/hide-columns.js | 4 +- 2 files changed, 134 insertions(+), 44 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index 8297627fa..a5e3e0e8f 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -1,90 +1,180 @@ -const LIFETIME = { - INFINITE: 'infinite', +export const LOCATION = { + LOCAL: 'local', + WINDOW: 'window', }; +const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.LOCAL ]; + export class StorageManager { namespace; + _options; - constructor(namespace) { + constructor(namespace, options) { if (!namespace) { throw new Error('Cannot setup StorageManager without namespace'); } this.namespace = namespace; + + if (options !== undefined) { + this._options = options; + } } - save(key, value, options) { + save(key, value, options=this._options) { if (!key) { throw new Error('StorageManager.save called with invalid key'); } - if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { - throw new Error('StorageManager.save called with unsupported lifetime option'); + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.save called with unsupported location option'); } - const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; + const location = options && options.location !== undefined ? options.location : LOCATION_SHADOWING[0]; - switch (lifetime) { - case LIFETIME.INFINITE: { - this.saveToLocalStorage({ ...this.getFromLocalStorage(), [key]: value }); + switch (location) { + case LOCATION.LOCAL: { + this._saveToLocalStorage({ ...this._getFromLocalStorage(), [key]: value }); + break; + } + case LOCATION.WINDOW: { + this._saveToWindow({ ...this._getFromLocalStorage(), [key]: value }); break; } default: - console.error('StorageManager.save cannot save item with unsupported lifetime'); + console.error('StorageManager.save cannot save item with unsupported location'); } } - load(key, options) { - if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { - throw new Error('StorageManager.load called with unsupported lifetime option'); + load(key, options=this._options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.load called with unsupported location option'); } - const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; + let locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; - switch (lifetime) { - case LIFETIME.INFINITE: - return this.getFromLocalStorage()[key]; - default: - console.error('StorageManager.load cannot load item with unsupported lifetime'); - } - } - - remove(key, options) { - if (options && options.lifetime !== undefined && !Object.values(LIFETIME).includes(options.lifetime)) { - throw new Error('StorageManager.load called with unsupported lifetime option'); - } - - const lifetime = options && options.lifetime !== undefined ? options.lifetime : LIFETIME.INFINITE; - - switch (lifetime) { - case LIFETIME.INFINITE: { - var val = this.getFromLocalStorage(); - - delete val[key]; + while (locations.length > 0) { + const location = locations.shift(); + let val; - return this.saveToLocalStorage(val); + switch (location) { + case LOCATION.LOCAL: { + val = this._getFromLocalStorage()[key]; + break; + } + case LOCATION.WINDOW: { + val = this._getFromWindow()[key]; + break; + } + default: + console.error('StorageManager.load cannot load item with unsupported location'); + } + + if (val !== undefined || locations.length === 0) { + return val; } - default: - console.error('StorageManager.load cannot load item with unsupported lifetime'); } } - getFromLocalStorage() { + remove(key, options=this._options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.load called with unsupported location option'); + } + + const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + + for (const location of locations) { + switch (location) { + case LOCATION.LOCAL: { + let val = this._getFromLocalStorage(); + + delete val[key]; + + return this._saveToLocalStorage(val); + } + case LOCATION.WINDOW: { + let val = this._getFromWindow(); + + delete val[key]; + + return this._saveToWindow(val); + } + default: + console.error('StorageManager.load cannot load item with unsupported location'); + } + } + } + + clear(options) { + if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { + throw new Error('StorageManager.clear called with unsupported location option'); + } + + const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + + for (const location of locations) { + switch (location) { + case LOCATION.LOCAL: + return this._clearLocalStorage(); + case LOCATION.WINDOW: + return this._clearWindow(); + default: + console.error('StorageManager.clear cannot clear with unsupported location'); + } + } + + } + + + _getFromLocalStorage() { const state = JSON.parse(window.localStorage.getItem(this.namespace)); if (state === null) { // remove item from localStorage if it stores an invalid value (cannot be parsed) - this.clearLocalStorage(); + this._clearLocalStorage(); return {}; } return state; } - saveToLocalStorage(value) { + _saveToLocalStorage(value) { window.localStorage.setItem(this.namespace, JSON.stringify(value)); } - clearLocalStorage() { + _clearLocalStorage() { window.localStorage.removeItem(this.namespace); } + + + _getFromWindow() { + if (!window || !window.App) + return {}; + + if (!window.App.Storage) + window.App.Storage = {}; + + if (!window.App.Storage[this.namespace]) + return {}; + + return window.App.Storage[this.namespace]; + } + + _saveToWindow(value) { + if (!window || !window.App) { + throw new Error('StorageManager._saveToWindow called when window.App is not available'); + } + + if (!window.App.Storage) + window.App.Storage = {}; + + window.App.Storage[this.namespace] = value; + } + + _clearWindow() { + if (!window || !window.App) { + throw new Error('StorageManager._saveToWindow called when window.App is not available'); + } + + delete window.App.Storage[this.namespace]; + } } diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index c4a1f6f0f..420916f41 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -1,5 +1,5 @@ import { Utility } from '../../core/utility'; -import { StorageManager } from '../../lib/storage-manager/storage-manager'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import './hide-columns.scss'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; @@ -22,7 +22,7 @@ const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; }) export class HideColumns { - _storageManager = new StorageManager('HIDE_COLUMNS'); + _storageManager = new StorageManager('HIDE_COLUMNS', { location: LOCATION.LOCAL }); _element; _elementWrapper; From 33d9bacc8aa3e412e88251a76a389d19fd148210 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 17:22:09 +0100 Subject: [PATCH 30/35] fix(allocation-list): fix sorting --- src/Handler/Allocation/List.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handler/Allocation/List.hs b/src/Handler/Allocation/List.hs index 9d52233ed..558d4c889 100644 --- a/src/Handler/Allocation/List.hs +++ b/src/Handler/Allocation/List.hs @@ -89,7 +89,7 @@ getAllocationListR = do dbtSorting = mconcat [ sortTerm $ queryAllocation . to (E.^. AllocationTerm) - , sortSchool $ queryAllocation . to (E.^. AllocationSchool) + , sortSchoolShort $ queryAllocation . to (E.^. AllocationSchool) , sortAllocationName $ queryAllocation . to (E.^. AllocationName) , singletonMap "available" . SortColumn $ view queryAvailable , if From 0b56ecc6ec50a110b6307045d3028e3bdcde7bd0 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 17:24:05 +0100 Subject: [PATCH 31/35] refactor(async-table): migrate to StorageManager --- .../lib/storage-manager/storage-manager.js | 10 +++- frontend/src/utils/async-table/async-table.js | 47 +++++++------------ 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index a5e3e0e8f..d2e407593 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -138,6 +138,9 @@ export class StorageManager { } _saveToLocalStorage(value) { + if (!value) + return this._clearLocalStorage(); + window.localStorage.setItem(this.namespace, JSON.stringify(value)); } @@ -164,6 +167,9 @@ export class StorageManager { throw new Error('StorageManager._saveToWindow called when window.App is not available'); } + if (!value) + return this._clearWindow(); + if (!window.App.Storage) window.App.Storage = {}; @@ -175,6 +181,8 @@ export class StorageManager { throw new Error('StorageManager._saveToWindow called when window.App is not available'); } - delete window.App.Storage[this.namespace]; + if (window.App.Storage) { + delete window.App.Storage[this.namespace]; + } } } diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index b8330aa4b..3067e0f1f 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { Datepicker } from '../form/datepicker'; import { HttpClient } from '../../services/http-client/http-client'; import * as debounce from 'lodash.debounce'; @@ -40,6 +41,8 @@ export class AsyncTable { }; _ignoreRequest = false; + _storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, { location: LOCATION.LOCAL }); + constructor(element, app) { if (!element) { throw new Error('Async Table utility cannot be setup without an element!'); @@ -81,10 +84,10 @@ export class AsyncTable { this._setupPageSizeSelect(); this._setupTableFilter(); - this._processLocalStorage(); + this._processStorage(); // clear currentTableUrl from previous requests - setLocalStorageParameter('currentTableUrl', null); + this._storageManager.remove('currentTableUrl'); // mark initialized this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS); @@ -100,7 +103,7 @@ export class AsyncTable { this._ths.forEach((th) => { th.clickHandler = (event) => { - setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft); + this._storageManager.save('horizPos', (this._scrollTable || {}).scrollLeft); this._linkClickHandler(event); }; th.element.addEventListener('click', th.clickHandler); @@ -122,7 +125,7 @@ export class AsyncTable { left: this._scrollTable.offsetLeft || 0, behavior: 'smooth', }; - setLocalStorageParameter('scrollTo', scrollTo); + this._storageManager.save('scrollTo', scrollTo); } this._linkClickHandler(event); }; @@ -225,7 +228,7 @@ export class AsyncTable { const prefix = findCssIdPrefix(focusedInput.id); const focusId = focusedInput.id.replace(prefix, ''); callback = function(wrapper) { - const idPrefix = getLocalStorageParameter('cssIdPrefix'); + const idPrefix = this._storageManager.load('cssIdPrefix', { location: LOCATION.WINDOW }); const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId); if (toBeFocused) { toBeFocused.focus(); @@ -238,7 +241,7 @@ export class AsyncTable { } _serializeTableFilterToURL(tableFilterForm) { - const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href); + const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); // create new FormData and format any date values const formData = Datepicker.unformatAll(this._massInputForm, new FormData(tableFilterForm)); @@ -254,18 +257,18 @@ export class AsyncTable { return url; } - _processLocalStorage() { - const scrollTo = getLocalStorageParameter('scrollTo'); + _processStorage() { + const scrollTo = this._storageManager.load('scrollTo'); if (scrollTo && this._scrollTable) { window.scrollTo(scrollTo); } - setLocalStorageParameter('scrollTo', null); + this._storageManager.remove('scrollTo'); - const horizPos = getLocalStorageParameter('horizPos'); + const horizPos = this._storageManager.load('horizPos'); if (horizPos && this._scrollTable) { this._scrollTable.scrollLeft = horizPos; } - setLocalStorageParameter('horizPos', null); + this._storageManager.remove('horizPos'); } _removeListeners() { @@ -300,7 +303,7 @@ export class AsyncTable { } _changePagesizeHandler = () => { - const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href); + const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); // create new FormData and format any date values const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm)); @@ -336,7 +339,7 @@ export class AsyncTable { return false; } - setLocalStorageParameter('currentTableUrl', url.href); + this._storageManager.save('currentTableUrl', url.href); // reset table this._removeListeners(); this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); @@ -346,9 +349,9 @@ export class AsyncTable { this._app.utilRegistry.setupAll(this._element); if (callback && typeof callback === 'function') { - setLocalStorageParameter('cssIdPrefix', response.idPrefix); + this._storageManager.save('cssIdPrefix', response.idPrefix, { location: LOCATION.WINDOW }); callback(this._element); - setLocalStorageParameter('cssIdPrefix', ''); + this._storageManager.remove('cssIdPrefix', { location: LOCATION.WINDOW }); } }).catch((err) => console.error(err) ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); @@ -365,17 +368,3 @@ function findCssIdPrefix(id) { } return ''; } - -function setLocalStorageParameter(key, value) { - const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; - if (value !== null) { - currentLSState[key] = value; - } else { - delete currentLSState[key]; - } - window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState)); -} -function getLocalStorageParameter(key) { - const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; - return currentLSState[key]; -} From 9eff3cfa10806f90d655fb32f72116e30020afab Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 17:32:41 +0100 Subject: [PATCH 32/35] fix(allocation-list): fix default sorting --- src/Handler/Allocation/List.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handler/Allocation/List.hs b/src/Handler/Allocation/List.hs index 558d4c889..081734394 100644 --- a/src/Handler/Allocation/List.hs +++ b/src/Handler/Allocation/List.hs @@ -124,7 +124,7 @@ getAllocationListR = do psValidator :: PSValidator _ _ psValidator = def - & defaultSorting [SortDescBy "term", SortAscBy "school", SortAscBy "allocation"] + & defaultSorting [SortDescBy "term", SortAscBy "school-short", SortAscBy "allocation"] table <- runDB $ dbTableWidget' psValidator DBTable{..} From 4af776fff141c0961a5f0b59e042747193f2e3df Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 6 Dec 2019 17:32:56 +0100 Subject: [PATCH 33/35] refactor(show-hide): migrate to StorageManager --- frontend/src/utils/show-hide/show-hide.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index 0780a465d..c264f3af3 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import './show-hide.scss'; const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE'; @@ -15,6 +16,8 @@ export class ShowHide { _showHideId; _element; + _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, { location: LOCATION.LOCAL }); + constructor(element) { if (!element) { throw new Error('ShowHide utility cannot be setup without an element!'); @@ -41,9 +44,9 @@ export class ShowHide { } if (this._showHideId) { - let localStorageCollapsed = this._getLocalStorage()[this._showHideId]; - if (typeof localStorageCollapsed !== 'undefined') { - collapsed = localStorageCollapsed; + let storageCollapsed = this._storageManager.load(this._showHideId); + if (typeof storageCollapsed !== 'undefined') { + collapsed = storageCollapsed; } } this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed); @@ -70,18 +73,7 @@ export class ShowHide { const newState = this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS); if (this._showHideId) { - this._setLocalStorage(this._showHideId, newState); + this._storageManager.save(this._showHideId, newState); } } - - // maybe move these to a LocalStorageHelper? - _setLocalStorage(id, state) { - const lsData = this._getLocalStorage(); - lsData[id] = state; - window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData)); - } - - _getLocalStorage() { - return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {}; - } } From e371412db48c600589800890477531706a6313bd Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 13 Dec 2019 15:04:52 +0100 Subject: [PATCH 34/35] fix(hide-columns): improve positioning BREAKING CHANGE: StorageManager version numbers --- frontend/src/app.js | 3 - .../lib/storage-manager/storage-manager.js | 53 +- frontend/src/polyfill.js | 4 + frontend/src/utils/async-table/async-table.js | 8 +- .../src/utils/hide-columns/hide-columns.js | 40 +- .../src/utils/hide-columns/hide-columns.scss | 73 ++- frontend/src/utils/show-hide/show-hide.js | 2 +- package-lock.json | 82 ++- package.json | 7 +- records.json | 544 ------------------ webpack.config.js | 26 +- 11 files changed, 199 insertions(+), 643 deletions(-) create mode 100644 frontend/src/polyfill.js delete mode 100644 records.json diff --git a/frontend/src/app.js b/frontend/src/app.js index acbc4702a..6aa438405 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -4,9 +4,6 @@ import { I18n } from './services/i18n/i18n'; import { UtilRegistry } from './services/util-registry/util-registry'; import { isValidUtility } from './core/utility'; -// load window.fetch polyfill -import 'whatwg-fetch'; - import './app.scss'; export class App { diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index d2e407593..ec564c17a 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -1,3 +1,5 @@ +import * as semver from 'semver'; + export const LOCATION = { LOCAL: 'local', WINDOW: 'window', @@ -8,14 +10,19 @@ const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.LOCAL ]; export class StorageManager { namespace; + version; _options; - constructor(namespace, options) { + constructor(namespace, version, options) { + this.namespace = namespace; + this.version = semver.valid(version); + if (!namespace) { throw new Error('Cannot setup StorageManager without namespace'); } - - this.namespace = namespace; + if (!this.version) { + throw new Error('Cannot setup StorageManager without valid semver version'); + } if (options !== undefined) { this._options = options; @@ -128,20 +135,41 @@ export class StorageManager { _getFromLocalStorage() { - const state = JSON.parse(window.localStorage.getItem(this.namespace)); - if (state === null) { - // remove item from localStorage if it stores an invalid value (cannot be parsed) + let state; + + try { + state = JSON.parse(window.localStorage.getItem(this.namespace)); + } catch { + state = null; + } + + if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) { + // remove item from localStorage if it stores an invalid state this._clearLocalStorage(); return {}; } - return state; + + if ('state' in state) + return state.state; + else { + delete state.version; + return state; + } } - _saveToLocalStorage(value) { - if (!value) + _saveToLocalStorage(state) { + if (!state) return this._clearLocalStorage(); + + let versionedState; - window.localStorage.setItem(this.namespace, JSON.stringify(value)); + if ('version' in state || 'state' in state) { + versionedState = { version: this.version, state: state }; + } else { + versionedState = { version: this.version, ...state }; + } + + window.localStorage.setItem(this.namespace, JSON.stringify(versionedState)); } _clearLocalStorage() { @@ -156,10 +184,7 @@ export class StorageManager { if (!window.App.Storage) window.App.Storage = {}; - if (!window.App.Storage[this.namespace]) - return {}; - - return window.App.Storage[this.namespace]; + return window.App.Storage[this.namespace] || {}; } _saveToWindow(value) { diff --git a/frontend/src/polyfill.js b/frontend/src/polyfill.js new file mode 100644 index 000000000..27333c1a0 --- /dev/null +++ b/frontend/src/polyfill.js @@ -0,0 +1,4 @@ +import 'whatwg-fetch'; + +import { ResizeObserver as Polyfill } from '@juggle/resize-observer'; +window.ResizeObserver = window.ResizeObserver || Polyfill; diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 3067e0f1f..80dadca3a 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -41,7 +41,7 @@ export class AsyncTable { }; _ignoreRequest = false; - _storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, { location: LOCATION.LOCAL }); + _storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.WINDOW }); constructor(element, app) { if (!element) { @@ -228,7 +228,7 @@ export class AsyncTable { const prefix = findCssIdPrefix(focusedInput.id); const focusId = focusedInput.id.replace(prefix, ''); callback = function(wrapper) { - const idPrefix = this._storageManager.load('cssIdPrefix', { location: LOCATION.WINDOW }); + const idPrefix = this._storageManager.load('cssIdPrefix'); const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId); if (toBeFocused) { toBeFocused.focus(); @@ -349,9 +349,9 @@ export class AsyncTable { this._app.utilRegistry.setupAll(this._element); if (callback && typeof callback === 'function') { - this._storageManager.save('cssIdPrefix', response.idPrefix, { location: LOCATION.WINDOW }); + this._storageManager.save('cssIdPrefix', response.idPrefix); callback(this._element); - this._storageManager.remove('cssIdPrefix', { location: LOCATION.WINDOW }); + this._storageManager.remove('cssIdPrefix'); } }).catch((err) => console.error(err) ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 420916f41..acedc2375 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -8,7 +8,6 @@ const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; -const TABLE_HIDER_CONTAINER_CLASS = 'table-hiders'; const TABLE_HIDER_CLASS = 'table-hider'; const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible'; @@ -22,12 +21,13 @@ const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; }) export class HideColumns { - _storageManager = new StorageManager('HIDE_COLUMNS', { location: LOCATION.LOCAL }); + _storageManager = new StorageManager('HIDE_COLUMNS', '1.0.0', { location: LOCATION.LOCAL }); _element; _elementWrapper; _tableUtilContainer; - _tableHiderContainer; + + _autoHide; headerToHider = new Map(); hiderToHeader = new Map(); @@ -38,6 +38,8 @@ export class HideColumns { } constructor(element) { + this._autoHide = this._storageManager.load('autoHide', {}) || false; + if (!element) { throw new Error('Hide Columns utility cannot be setup without an element!'); } @@ -64,14 +66,6 @@ export class HideColumns { hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); } - // get or create table hider container before the table - this._tableHiderContainer = this._element.previousSibling; - if (!this._tableHiderContainer || !this._tableHiderContainer.classList.contains(TABLE_HIDER_CONTAINER_CLASS)) { - this._tableHiderContainer = document.createElement('div'); - this._tableHiderContainer.classList.add(TABLE_HIDER_CONTAINER_CLASS); - this._element.parentElement.insertBefore(this._tableHiderContainer, this._element); - } - this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th)); } @@ -102,8 +96,9 @@ export class HideColumns { hider.addEventListener('click', (event) => { event.preventDefault(); + event.stopPropagation(); this.switchColumnDisplay(th, hider); - this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); + // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); }); hider.addEventListener('mouseover', () => { @@ -119,8 +114,10 @@ export class HideColumns { this.updateHiderIcon(hider, currentlyHidden); }); + new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); + // reposition hider on each window resize event - window.addEventListener('resize', () => this.repositionHider(hider)); + // window.addEventListener('resize', () => this.repositionHider(hider)); this.updateColumnDisplay(this.colIndex(th), preHidden); this.updateHider(hider, preHidden); @@ -130,8 +127,6 @@ export class HideColumns { } else { this.hideHiderBehindHeader(hider); } - - this._tableHiderContainer.children.forEach(hider => this.repositionHider(hider)); } switchColumnDisplay(th, hider) { @@ -146,7 +141,7 @@ export class HideColumns { this.updateHider(hider, hidden); // persist new hidden setting for column - if (hidden && this.isEmptyColumn(columnIndex)) { + if ((hidden && this.isEmptyColumn(columnIndex) && this._autoHide) || (!hidden && (!this.isEmptyColumn(columnIndex) || !this._autoHide))) { this._storageManager.remove(this.getStorageKey(th)); } else { this._storageManager.save(this.getStorageKey(th), hidden); @@ -202,8 +197,8 @@ export class HideColumns { } hideHiderBehindHeader(hider) { - if (!this._tableHiderContainer.contains(hider)) { - this._tableHiderContainer.appendChild(hider); + if (!this.hiderToHeader.get(hider).contains(hider)) { + this.hiderToHeader.get(hider).appendChild(hider); } this.repositionHider(hider); @@ -214,11 +209,10 @@ export class HideColumns { repositionHider(hider) { const thR = this.hiderToHeader.get(hider).getBoundingClientRect(), - hR = hider.getBoundingClientRect(), - pR = this._tableHiderContainer.getBoundingClientRect(); + hR = hider.getBoundingClientRect(); - hider.style.left = (thR.left + thR.width/2 - hR.width/2 - pR.left) + 'px'; - hider.style.top = (thR.top - pR.top + thR.height) + 'px'; + hider.style.left = (thR.width/2 - hR.width/2) + 'px'; + hider.style.top = thR.height + 'px'; } getStorageKey(th) { @@ -266,7 +260,7 @@ export class HideColumns { isHiddenColumn(th) { const hidden = this._storageManager.load(this.getStorageKey(th)), emptyColumn = this.isEmptyColumn(this.colIndex(th)); - return hidden === true || hidden === undefined && emptyColumn; + return hidden === true || hidden === undefined && emptyColumn && this._autoHide; } colSpan(cell) { diff --git a/frontend/src/utils/hide-columns/hide-columns.scss b/frontend/src/utils/hide-columns/hide-columns.scss index 77a73bee3..59aab069d 100644 --- a/frontend/src/utils/hide-columns/hide-columns.scss +++ b/frontend/src/utils/hide-columns/hide-columns.scss @@ -1,47 +1,38 @@ -.table-hiders { - position: relative; - max-height: 0px; +.table-hider { + background-color: #fff; + color: var(--color-link); + padding: 10px; + cursor: pointer; + box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); + position: absolute; + overflow: hidden; + transition: transform .2s ease; + transform: scaleY(0); + transform-origin: top; - .table-hider { - background-color: #fff; - color: var(--color-link); - padding: 10px; - cursor: pointer; - box-shadow: 0 0 2px 0 rgba(0,0,0,0.6); - position: absolute; - overflow: hidden; - transition: transform .2s ease; - transform: scaleY(0); - transform-origin: top; - - &:hover { - background-color: var(--color-grey-light); - } - - .table-hider__label { - display: none; - } - - &.table-hider--visible { - transform: scaleY(.4); - transition: none; /* TODO find better way to prevent transition on icons */ - - .fas { - transform: scaleY(2.5); - } - - &:hover { - transform: scaleY(1); - - .fas { - transform: scaleY(1); - } - } - } + &:hover { + background-color: var(--color-grey-light); } - &+.table { - margin-top: 0px; + .table-hider__label { + display: none; + } + + &.table-hider--visible { + transform: scaleY(.4); + transition: none; /* TODO find better way to prevent transition on icons */ + + .fas { + transform: scaleY(2.5); + } + + &:hover { + transform: scaleY(1); + + .fas { + transform: scaleY(1); + } + } } } diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index c264f3af3..aa1f4f792 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -16,7 +16,7 @@ export class ShowHide { _showHideId; _element; - _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, { location: LOCATION.LOCAL }); + _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL }); constructor(element) { if (!element) { diff --git a/package-lock.json b/package-lock.json index a03239ca5..a1bd61eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -1907,6 +1913,14 @@ "@babel/helper-plugin-utils": "^7.0.0", "resolve": "^1.8.1", "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -2035,6 +2049,12 @@ "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -2541,6 +2561,11 @@ "integrity": "sha512-EKKR4p0higjsIPKjSSkGqtweUwo/GgR/zKL4rCwzF5Z/BZ/ebJZaS8ZjGE7YUNEN63SYk2WhpJVI+l9dwfU7RQ==", "dev": true }, + "@juggle/resize-observer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-2.5.0.tgz", + "integrity": "sha512-Nmkeaj5LalJeciRVEqi9Uxi61r0LvGc2yhUCykhXuft9fMyb/6VkZbwJ+UmUl8bk2k6qhwd1qJw6S2YJ0joXlA==" + }, "@marionebl/sander": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz", @@ -6277,6 +6302,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "crypto-browserify": { @@ -6989,6 +7022,12 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -8608,6 +8647,12 @@ "strip-indent": "^2.0.0" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10348,6 +10393,14 @@ "requires": { "pify": "^4.0.1", "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "mamacro": { @@ -10941,6 +10994,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -14352,6 +14413,12 @@ "os-tmpdir": "^1.0.0" } }, + "outdent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.7.0.tgz", + "integrity": "sha512-Ue462G+UIFoyQmOzapGIKWS3d/9NHeD/018WGEDZIhN2/VaQpVXbofMcZX0socv1fw4/tmEn7Vd3McOdPZfKzQ==", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -15468,9 +15535,9 @@ } }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, "semver-compare": { @@ -17555,6 +17622,15 @@ "tapable": "^1.0.0" } }, + "webpack-plugin-hash-output": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/webpack-plugin-hash-output/-/webpack-plugin-hash-output-3.2.1.tgz", + "integrity": "sha512-Iu4Sox3/bdiqd6TdYwZAExuH+XNbnJStPrwh6yhzOflwc/hZUP9MdiZDbFwTXrmm9ZwoXNUmvn7C0Qj4qRez2A==", + "dev": true, + "requires": { + "outdent": "^0.7.0" + } + }, "webpack-sources": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", diff --git a/package.json b/package.json index 1c4af85d9..3361f6443 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "", "license": "ISC", "scripts": { - "start": "run-p frontend:build:watch yesod:start", + "start": "npm-run-all frontend:build --parallel \"frontend:build:watch\" \"yesod:start\"", "test": "run-s frontend:test yesod:test", "lint": "run-s frontend:lint yesod:lint", "build": "run-s frontend:build yesod:build", @@ -90,6 +90,7 @@ "null-loader": "^2.0.0", "postcss-loader": "^3.0.0", "sass-loader": "^7.3.1", + "semver": "^6.3.0", "standard-version": "^6.0.1", "style-loader": "^0.23.1", "terser-webpack-plugin": "^2.2.3", @@ -97,10 +98,12 @@ "typeface-source-sans-pro": "0.0.75", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", - "webpack-manifest-plugin": "^2.2.0" + "webpack-manifest-plugin": "^2.2.0", + "webpack-plugin-hash-output": "^3.2.1" }, "dependencies": { "@babel/runtime": "^7.7.6", + "@juggle/resize-observer": "^2.5.0", "core-js": "^3.4.8", "moment": "^2.24.0", "npm": "^6.13.3", diff --git a/records.json b/records.json deleted file mode 100644 index c3e4f6812..000000000 --- a/records.json +++ /dev/null @@ -1,544 +0,0 @@ -{ - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/typeface-roboto/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/typeface-source-sans-pro/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/@fortawesome/fontawesome-pro/css/all.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-form/async-form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/asidenav/asidenav.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/show-hide/show-hide.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/inputs.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/radio.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/form/form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/alerts/alerts.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/tooltips/tooltips.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/course-teaser/course-teaser.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/modal/modal.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/navbar/navbar.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-table/async-table.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/async-table/async-table-filter.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/mass-input/mass-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/file-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!node_modules/sass-loader/dist/cjs.js!frontend/src/utils/inputs/checkbox.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/postcss-loader/src/index.js!frontend/src/utils/form/datepicker.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - }, - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/typeface-source-sans-pro/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/typeface-roboto/index.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/@fortawesome/fontawesome-pro/css/all.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/asidenav/asidenav.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-form/async-form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/show-hide/show-hide.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/form/form.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/inputs.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/radio.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/tooltips/tooltips.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/course-teaser/course-teaser.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/modal/modal.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table-filter.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/mass-input/mass-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/alerts/alerts.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/navbar/navbar.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!frontend/src/utils/form/datepicker.css": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/file-input.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/checkbox.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ], - "mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/app.scss": [ - { - "modules": { - "byIdentifier": {}, - "usedIds": {} - }, - "chunks": { - "byName": {}, - "bySource": {}, - "usedIds": [] - } - } - ] -} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 3a93a5bbe..c16c23a9d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,9 +6,10 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const yaml = require('js-yaml'); +const HashOutput = require('webpack-plugin-hash-output'); -var webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); - +const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); +const packageVersion = require('./package.json').version; module.exports = { module: { @@ -71,15 +72,21 @@ module.exports = { }, entry: { - main: path.resolve(__dirname, 'frontend/src', 'main.js') + main: [ path.resolve(__dirname, 'frontend/src', 'polyfill.js'), + path.resolve(__dirname, 'frontend/src', 'main.js') + ] }, plugins: [ + new HashOutput({ + validateOutput: true, + validateOutputRegex: /static\/wp-[^\/]\// + }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // all options are optional - filename: '[contenthash].css', - chunkFilename: '[contenthash].css', + filename: '[chunkhash].css', + chunkFilename: '[chunkhash].css', ignoreOrder: false, // Enable to remove warnings about conflicting order }), new webpack.NamedChunksPlugin((chunk) => { @@ -101,12 +108,15 @@ module.exports = { new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new CopyPlugin([ { from: 'assets/lmu/sigillum.svg', to: path.resolve(__dirname, 'static', 'img/lmu/sigillum.svg') }, - ]) + ]), + new webpack.DefinePlugin({ + VERSION: JSON.stringify(packageVersion) + }) ], output: { - chunkFilename: '[contenthash].js', - filename: '[contenthash].js', + chunkFilename: '[chunkhash].js', + filename: '[chunkhash].js', path: path.resolve(__dirname, 'static', `wp-${webpackVersion}`), publicPath: `/static/res/wp-${webpackVersion}/`, hashFunction: 'shake256', From cb75f4e4f1de5695aa8c8c8d2ac24c9d0f5134ad Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 16 Dec 2019 14:41:56 +0100 Subject: [PATCH 35/35] chore(frontend-tests): fix environment specifications --- frontend/src/app.spec.js | 32 +++++++++---------- .../lib/storage-manager/storage-manager.js | 32 ++++++++++++------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/frontend/src/app.spec.js b/frontend/src/app.spec.js index 247be9f00..e682f0e28 100644 --- a/frontend/src/app.spec.js +++ b/frontend/src/app.spec.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + import { App } from './app'; import { Utility } from './core/utility'; @@ -13,52 +15,50 @@ const TEST_UTILS = [ ]; describe('App', () => { - let app; - beforeEach(() => { - app = new App(); + global.App = new App(); }); it('should create', () => { - expect(app).toBeTruthy(); + expect(global.App).toBeTruthy(); }); it('should setup all utlites when page is done loading', () => { - spyOn(app.utilRegistry, 'setupAll'); + spyOn(global.App.utilRegistry, 'setupAll'); document.dispatchEvent(new Event('DOMContentLoaded')); - expect(app.utilRegistry.setupAll).toHaveBeenCalled(); + expect(global.App.utilRegistry.setupAll).toHaveBeenCalled(); }); describe('provides services', () => { it('HttpClient as httpClient', () => { - expect(app.httpClient).toBeTruthy(); + expect(global.App.httpClient).toBeTruthy(); }); it('HtmlHelpers as htmlHelpers', () => { - expect(app.htmlHelpers).toBeTruthy(); + expect(global.App.htmlHelpers).toBeTruthy(); }); it('I18n as i18n', () => { - expect(app.i18n).toBeTruthy(); + expect(global.App.i18n).toBeTruthy(); }); it('UtilRegistry as utilRegistry', () => { - expect(app.utilRegistry).toBeTruthy(); + expect(global.App.utilRegistry).toBeTruthy(); }); }); describe('registerUtilities()', () => { it('should register the given utilities', () => { - spyOn(app.utilRegistry, 'register'); - app.registerUtilities(TEST_UTILS); - expect(app.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); - expect(app.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); - expect(app.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); + spyOn(global.App.utilRegistry, 'register'); + global.App.registerUtilities(TEST_UTILS); + expect(global.App.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); + expect(global.App.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); + expect(global.App.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); }); it('should throw an error if not passed an array of utilities', () => { expect(() => { - app.registerUtilities({}); + global.App.registerUtilities({}); }).toThrow(); }); }); diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index ec564c17a..9b6202329 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -1,3 +1,5 @@ +/* global global:writable */ + import * as semver from 'semver'; export const LOCATION = { @@ -12,6 +14,7 @@ export class StorageManager { namespace; version; _options; + _global; constructor(namespace, version, options) { this.namespace = namespace; @@ -27,6 +30,13 @@ export class StorageManager { if (options !== undefined) { this._options = options; } + + if (global !== undefined) + this._global = global; + else if (window !== undefined) + this._global = window; + else + throw new Error('Cannot setup StorageManager without window or global'); } save(key, value, options=this._options) { @@ -178,36 +188,36 @@ export class StorageManager { _getFromWindow() { - if (!window || !window.App) + if (!this._global || !this._global.App) return {}; - if (!window.App.Storage) - window.App.Storage = {}; + if (!this._global.App.Storage) + this._global.App.Storage = {}; - return window.App.Storage[this.namespace] || {}; + return this._global.App.Storage[this.namespace] || {}; } _saveToWindow(value) { - if (!window || !window.App) { + if (!this._global || !this._global.App) { throw new Error('StorageManager._saveToWindow called when window.App is not available'); } if (!value) return this._clearWindow(); - if (!window.App.Storage) - window.App.Storage = {}; + if (!this._global.App.Storage) + this._global.App.Storage = {}; - window.App.Storage[this.namespace] = value; + this._global.App.Storage[this.namespace] = value; } _clearWindow() { - if (!window || !window.App) { + if (!this._global || !this._global.App) { throw new Error('StorageManager._saveToWindow called when window.App is not available'); } - if (window.App.Storage) { - delete window.App.Storage[this.namespace]; + if (this._global.App.Storage) { + delete this._global.App.Storage[this.namespace]; } } }