feat(communication): send test emails

This commit is contained in:
Gregor Kleen 2020-05-12 16:44:53 +02:00
parent e060080261
commit d90da85df3
10 changed files with 185 additions and 130 deletions

View File

@ -23,6 +23,7 @@
"no-extra-semi": "off", "no-extra-semi": "off",
"semi": ["error", "always"], "semi": ["error", "always"],
"comma-dangle": ["error", "always-multiline"], "comma-dangle": ["error", "always-multiline"],
"quotes": ["error", "single"] "quotes": ["error", "single"],
"no-var": "error"
} }
} }

View File

@ -0,0 +1,88 @@
import { Utility } from '../../core/utility';
const MASS_INPUT_SELECTOR = '.massinput';
const RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories';
const RECIPIENT_CATEGORY_SELECTOR = '.recipient-category';
const RECIPIENT_CATEGORY_CHECKBOX_SELECTOR = '.recipient-category__checkbox';
const RECIPIENT_CATEGORY_OPTIONS_SELECTOR = '.recipient-category__options';
const RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR = '.recipient-category__toggle-all [type="checkbox"]';
const RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checked-counter';
@Utility({
selector: RECIPIENT_CATEGORIES_SELECTOR,
})
export class CommunicationRecipients {
massInputElement;
constructor(element) {
if (!element) {
throw new Error('Communication Recipient utility cannot be setup without an element!');
}
this.massInputElement = element.closest(MASS_INPUT_SELECTOR);
this.setupRecipientCategories();
const recipientObserver = new MutationObserver(this.setupRecipientCategories.bind(this));
recipientObserver.observe(this.massInputElement, { childList: true });
}
setupRecipientCategories() {
Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory);
}
}
function setupRecipientCategory(recipientCategoryElement) {
const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR);
const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR);
if (categoryOptions) {
const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]'));
const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR);
// setup category checkbox to toggle all child checkboxes if changed
categoryCheckbox.addEventListener('change', () => {
categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => {
checkbox.checked = categoryCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
// update counter and toggle checkbox initially
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
// register change listener for individual checkboxes
categoryCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
});
// register change listener for toggle all checkbox
if (toggleAllCheckbox) {
toggleAllCheckbox.addEventListener('change', () => {
categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => {
checkbox.checked = toggleAllCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
});
}
}
}
// update checked state of toggle all checkbox based on all other checkboxes
function updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes) {
const allChecked = categoryCheckboxes.reduce((acc, checkbox) => acc && checkbox.checked, true);
toggleAllCheckbox.checked = allChecked;
}
// update value of checked counter
function updateCheckedCounter(recipientCategoryElement, categoryCheckboxes) {
const checkedCounter = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR);
const checkedCheckboxes = categoryCheckboxes.reduce((acc, checkbox) => checkbox.checked ? acc + 1 : acc, 0);
checkedCounter.innerHTML = checkedCheckboxes + '/' + categoryCheckboxes.length;
}

View File

@ -5,6 +5,7 @@ import { Datepicker } from './datepicker';
import { FormErrorRemover } from './form-error-remover'; import { FormErrorRemover } from './form-error-remover';
import { InteractiveFieldset } from './interactive-fieldset'; import { InteractiveFieldset } from './interactive-fieldset';
import { NavigateAwayPrompt } from './navigate-away-prompt'; import { NavigateAwayPrompt } from './navigate-away-prompt';
import { CommunicationRecipients } from './communication-recipients';
export const FormUtils = [ export const FormUtils = [
AutoSubmitButton, AutoSubmitButton,
@ -13,5 +14,6 @@ export const FormUtils = [
FormErrorRemover, FormErrorRemover,
InteractiveFieldset, InteractiveFieldset,
NavigateAwayPrompt, NavigateAwayPrompt,
CommunicationRecipients,
// ReactiveSubmitButton // not used currently // ReactiveSubmitButton // not used currently
]; ];

View File

@ -1,3 +1,5 @@
@use "../../app" as *
// GENERAL STYLES FOR FORMS // GENERAL STYLES FOR FORMS
// FORM GROUPS // FORM GROUPS
@ -24,7 +26,7 @@
margin-top: 40px margin-top: 40px
.form-section-legend .form-section-legend
color: var(--color-fontsec) @extend .explanation
margin: 7px 0 margin: 7px 0
.form-group__hint, .form-section-title__hint .form-group__hint, .form-section-title__hint
@ -51,6 +53,7 @@
content: ' *' content: ' *'
color: var(--color-error) color: var(--color-error)
font-weight: 600 font-weight: 600
font-style: normal
.form-group--submit .form-group__input .form-group--submit .form-group__input
grid-column: 2 grid-column: 2

View File

@ -37,6 +37,9 @@ BtnAllocationAccept: Vergabe akzeptieren
BtnSystemMessageHide: Verstecken BtnSystemMessageHide: Verstecken
BtnSystemMessageUnhide: Nicht mehr verstecken BtnSystemMessageUnhide: Nicht mehr verstecken
BtnCommunicationSend: Senden
BtnCommunicationTest: Test-Nachricht verschicken
Aborted: Abgebrochen Aborted: Abgebrochen
Remarks: Hinweise Remarks: Hinweise
@ -952,6 +955,7 @@ MailEditNotifications: Benachrichtigungen ein-/ausschalten
MailSubjectSupport: Supportanfrage MailSubjectSupport: Supportanfrage
MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject} MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject}
CommCourseTestSubject customSubject@Text: [TEST] #{customSubject}
CommCourseSubject: Kursmitteilung CommCourseSubject: Kursmitteilung
MailSubjectLecturerInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zum Kursverwalter MailSubjectLecturerInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zum Kursverwalter
InvitationAcceptDecline: Einladung annehmen/ablehnen InvitationAcceptDecline: Einladung annehmen/ablehnen
@ -1415,6 +1419,7 @@ CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen. CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
CommUndisclosedRecipients: Verborgene Empfänger CommUndisclosedRecipients: Verborgene Empfänger
CommAllRecipients: alle-empfaenger CommAllRecipients: alle-empfaenger

View File

@ -7,9 +7,6 @@ import Import
import Handler.Utils import Handler.Utils
import Handler.Utils.Communication import Handler.Utils.Communication
import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -18,23 +15,13 @@ import qualified Database.Esqueleto as E
getCCommR, postCCommR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCCommR, postCCommR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCCommR = postCCommR getCCommR = postCCommR
postCCommR tid ssh csh = do postCCommR tid ssh csh = do
jSender <- requireAuthId
cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh
commR CommunicationRoute commR CommunicationRoute
{ crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommCourseHeading { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommCourseHeading
, crUltDest = SomeRoute $ CourseR tid ssh csh CCommR , crUltDest = SomeRoute $ CourseR tid ssh csh CCommR
, crJobs = \Communication{..} -> do , crJobs = crJobsCourseCommunication cid
let jSubject = cSubject , crTestJobs = crTestJobsCourseCommunication cid
jMailContent = cBody
jCourse = cid
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
jMailObjectUUID <- liftIO getRandom
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
Left email -> return . Address Nothing $ CI.original email
Right rid -> userAddress <$> getJust rid
forM_ allRecipients $ \jRecipientEmail ->
yield JobSendCourseCommunication{..}
, crRecipients = Map.fromList , crRecipients = Map.fromList
[ ( RGCourseParticipants [ ( RGCourseParticipants
, E.from $ \(user `E.InnerJoin` participant) -> do , E.from $ \(user `E.InnerJoin` participant) -> do

View File

@ -11,31 +11,18 @@ import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.CaseInsensitive as CI
getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
getTCommR = postTCommR getTCommR = postTCommR
postTCommR tid ssh csh tutn = do postTCommR tid ssh csh tutn = do
jSender <- requireAuthId
(cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn (cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn
commR CommunicationRoute commR CommunicationRoute
{ crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading
, crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR , crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR
, crJobs = \Communication{..} -> do , crJobs = crJobsCourseCommunication cid
let jSubject = cSubject , crTestJobs = crTestJobsCourseCommunication cid
jMailContent = cBody
jCourse = cid
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
jMailObjectUUID <- liftIO getRandom
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
Left email -> return . Address Nothing $ CI.original email
Right rid -> userAddress <$> getJust rid
forM_ allRecipients $ \jRecipientEmail ->
yield JobSendCourseCommunication{..}
, crRecipients = Map.fromList , crRecipients = Map.fromList
[ ( RGTutorialParticipants [ ( RGTutorialParticipants
, E.from $ \(user `E.InnerJoin` participant) -> do , E.from $ \(user `E.InnerJoin` participant) -> do

View File

@ -3,6 +3,7 @@ module Handler.Utils.Communication
, CommunicationRoute(..) , CommunicationRoute(..)
, Communication(..) , Communication(..)
, commR , commR
, crJobsCourseCommunication, crTestJobsCourseCommunication
-- * Re-Exports -- * Re-Exports
, Job(..) , Job(..)
) where ) where
@ -18,6 +19,8 @@ import Data.Map ((!), (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Conduit.Combinators as C
data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors
| RGTutorialParticipants | RGTutorialParticipants
@ -67,11 +70,25 @@ instance RenderMessage UniWorX RecipientCategory where
renderMessage' :: forall msg. RenderMessage UniWorX msg => msg -> Text renderMessage' :: forall msg. RenderMessage UniWorX msg => msg -> Text
renderMessage' = renderMessage foundation ls renderMessage' = renderMessage foundation ls
data CommunicationButton
= BtnCommunicationSend
| BtnCommunicationTest
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''CommunicationButton $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''CommunicationButton id
makePrisms ''CommunicationButton
instance Button UniWorX CommunicationButton where
btnClasses BtnCommunicationSend = [BCIsButton, BCPrimary]
btnClasses BtnCommunicationTest = [BCIsButton]
data CommunicationRoute = CommunicationRoute data CommunicationRoute = CommunicationRoute
{ crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User))) { crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User)))
, crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion , crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion
, crJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) () , crJobs, crTestJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) ()
, crHeading :: SomeMessage UniWorX , crHeading :: SomeMessage UniWorX
, crUltDest :: SomeRoute UniWorX , crUltDest :: SomeRoute UniWorX
} }
@ -83,6 +100,26 @@ data Communication = Communication
} }
crJobsCourseCommunication, crTestJobsCourseCommunication :: CourseId -> Communication -> ConduitT () Job (YesodDB UniWorX) ()
crJobsCourseCommunication jCourse Communication{..} = do
jSender <- requireAuthId
let jSubject = cSubject
jMailContent = cBody
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
jMailObjectUUID <- liftIO getRandom
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
Left email -> return . Address Nothing $ CI.original email
Right rid -> userAddress <$> getJust rid
forM_ allRecipients $ \jRecipientEmail ->
yield JobSendCourseCommunication{..}
crTestJobsCourseCommunication jCourse comm = do
jSender <- requireAuthId
MsgRenderer mr <- getMsgRenderer
let comm' = comm { cSubject = Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgCommCourseSubject) $ cSubject comm }
crJobsCourseCommunication jCourse comm' .| C.filter ((== Right jSender) . jRecipientEmail)
commR :: CommunicationRoute -> Handler Html commR :: CommunicationRoute -> Handler Html
commR CommunicationRoute{..} = do commR CommunicationRoute{..} = do
cUser <- maybeAuth cUser <- maybeAuth
@ -129,14 +166,18 @@ commR CommunicationRoute{..} = do
miAdd (EnumPosition RecipientCustom, 0) 1 nudge submitView = Just $ \csrf -> do miAdd (EnumPosition RecipientCustom, 0) 1 nudge submitView = Just $ \csrf -> do
(addRes, addView) <- mpreq (multiUserField True Nothing) (fslpI MsgEMail (mr MsgEMail) & setTooltip MsgMultiEmailFieldTip & addName (nudge "email")) Nothing (addRes, addView) <- mpreq (multiUserField True Nothing) (fslpI MsgEMail (mr MsgEMail) & setTooltip MsgMultiEmailFieldTip & addName (nudge "email")) Nothing
let let
addRes' = addRes <&> \(Set.toList -> nEmails) (maybe 0 (succ . snd . fst) . Map.lookupMax . Map.filterWithKey (\(EnumPosition c, _) _ -> c == RecipientCustom) -> kStart) -> FormSuccess . Map.fromList $ zip (map (EnumPosition RecipientCustom, ) [kStart..]) nEmails addRes' = addRes <&> \nEmails ((Map.elems &&& maybe 0 (succ . snd . fst) . Map.lookupMax) . Map.filterWithKey (\(EnumPosition c, _) _ -> c == RecipientCustom) -> (oEmails, kStart)) -> FormSuccess . Map.fromList . zip (map (EnumPosition RecipientCustom, ) [kStart..]) . Set.toList $ nEmails `Set.difference` Set.fromList oEmails
return (addRes', $(widgetFile "widgets/communication/recipientAdd")) return (addRes', $(widgetFile "widgets/communication/recipientAdd"))
miAdd _ _ _ _ = Nothing miAdd _ _ _ _ = Nothing
miCell _ (Left (CI.original -> email)) initRes nudge csrf = do miCell _ (Left (CI.original -> email)) initRes nudge csrf = do
(tickRes, tickView) <- mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True (tickRes, tickView) <- mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True
return (tickRes, $(widgetFile "widgets/communication/recipientEmail")) return (tickRes, $(widgetFile "widgets/communication/recipientEmail"))
miCell _ (Right (lookupUser -> User{..})) initRes nudge csrf = do miCell _ (Right uid@(lookupUser -> User{..})) initRes nudge csrf = do
(tickRes, tickView) <- mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True (tickRes, tickView) <- if
| fmap entityKey cUser == Just uid
-> mforced checkBoxField ("" & addName (nudge "tick")) True
| otherwise
-> mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True
return (tickRes, $(widgetFile "widgets/communication/recipientName")) return (tickRes, $(widgetFile "widgets/communication/recipientName"))
miAllowAdd (EnumPosition RecipientCustom, 0) 1 _ = True miAllowAdd (EnumPosition RecipientCustom, 0) 1 _ = True
miAllowAdd _ _ _ = False miAllowAdd _ _ _ = False
@ -167,22 +208,33 @@ commR CommunicationRoute{..} = do
recipientsListMsg <- messageI Info MsgCommRecipientsList recipientsListMsg <- messageI Info MsgCommRecipientsList
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication
<$> recipientAForm <$> recipientAForm
<* aformMessage recipientsListMsg <* aformMessage recipientsListMsg
<*> aopt textField (fslI MsgCommSubject) Nothing <*> aopt textField (fslI MsgCommSubject) Nothing
<*> areq htmlField (fslI MsgCommBody) Nothing <*> areq htmlField (fslI MsgCommBody) Nothing
formResult commRes $ \comm -> do formResult commRes $ \case
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs (comm, BtnCommunicationSend) -> do
addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
redirect crUltDest addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm
redirect crUltDest
(comm, BtnCommunicationTest) -> do
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crTestJobs comm) .| sinkDBJobs
addMessageI Info MsgCommTestSuccess
let formWdgt = wrapForm commWdgt def let formWdgt = wrapForm commWdgt def
{ formMethod = POST { formMethod = POST
, formAction = SomeRoute <$> mbCurrentRoute , formAction = SomeRoute <$> mbCurrentRoute
, formEncoding = commEncoding , formEncoding = commEncoding
, formSubmit = FormNoSubmit
} }
siteLayoutMsg crHeading $ do siteLayoutMsg crHeading $ do
setTitleI crHeading setTitleI crHeading
formWdgt let commTestTip = $(i18nWidgetFile "comm-test-tip")
[whamlet|
$newline never
<section>
^{formWdgt}
<section .explanation>
^{commTestTip}
|]

View File

@ -0,0 +1,17 @@
$newline never
<p>
Über den Knopf „_{MsgBtnCommunicationTest}“ haben Sie die #
Möglichkeit zunächst eine Kopie der Nachricht nur an sich selbst zu #
schicken.
<br>
Diese wird den Präfix „[TEST]“ im Betreff enthalten und ansonsten in #
allen Aspekten identisch sein zur eigentlichen Nachricht.
<p>
Um die Nachricht tatsächlich an alle Empfänger zu verschicken, #
verwenden Sie bitte „_{MsgBtnSubmit}“.

View File

@ -1,87 +0,0 @@
(function() {
var MASS_INPUT_SELECTOR = '.massinput';
var RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories';
var RECIPIENT_CATEGORY_SELECTOR = '.recipient-category';
var RECIPIENT_CATEGORY_CHECKBOX_SELECTOR = '.recipient-category__checkbox ';
var RECIPIENT_CATEGORY_OPTIONS_SELECTOR = '.recipient-category__options';
var RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR = '.recipient-category__toggle-all [type="checkbox"]';
var RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checked-counter';
var massInputElement;
document.addEventListener('DOMContentLoaded', function() {
var recipientCategoriesElement = document.querySelector(RECIPIENT_CATEGORIES_SELECTOR);
massInputElement = recipientCategoriesElement.closest(MASS_INPUT_SELECTOR);
setupRecipientCategories();
var recipientObserver = new MutationObserver(setupRecipientCategories);
recipientObserver.observe(massInputElement, { childList: true });
});
function setupRecipientCategories() {
var recipientCategoryElements = Array.from(massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR));
recipientCategoryElements.forEach(function(element) {
setupRecipientCategory(element);
});
}
function setupRecipientCategory(recipientCategoryElement) {
var categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR);
var categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR);
if (categoryOptions) {
var categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]'));
var toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR);
// setup category checkbox to toggle all child checkboxes if changed
categoryCheckbox.addEventListener('change', function() {
categoryCheckboxes.forEach(function(checkbox) {
checkbox.checked = categoryCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
// update counter and toggle checkbox initially
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
// register change listener for individual checkboxes
categoryCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
});
// register change listener for toggle all checkbox
if (toggleAllCheckbox) {
toggleAllCheckbox.addEventListener('change', function() {
categoryCheckboxes.forEach(function(checkbox) {
checkbox.checked = toggleAllCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
});
}
}
}
// update checked state of toggle all checkbox based on all other checkboxes
function updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes) {
var allChecked = categoryCheckboxes.reduce(function(acc, checkbox) {
return acc && checkbox.checked;
}, true);
toggleAllCheckbox.checked = allChecked;
}
// update value of checked counter
function updateCheckedCounter(recipientCategoryElement, categoryCheckboxes) {
var checkedCounter = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR);
var checkedCheckboxes = categoryCheckboxes.reduce(function(acc, checkbox) { return checkbox.checked ? acc + 1 : acc; }, 0);
checkedCounter.innerHTML = checkedCheckboxes + '/' + categoryCheckboxes.length;
}
})();