--- /dev/null
+serverkey
+[0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=]
--- /dev/null
+.PHONY: _subtrees
+
+_subtrees: _cgilite
+
+cgilite:
+ git subtree add --squash -P $@ https://git.plutz.net/git/$@ master
+
+_cgilite: cgilite
+ git subtree pull --squash -P $< https://git.plutz.net/git/$< master
--- /dev/null
+#!/bin/sh
+
+_EXEC="${_EXEC:-${0%/*}/}"
+_DATA="${_DATA:-.}"
+_BASE="${_BASE%/}"
+
+. "$_EXEC"/cgilite/cgilite.sh
+. "$_EXEC"/cgilite/session.sh
+. "$_EXEC"/cgilite/file.sh
+. "$_EXEC"/cgilite/storage.sh
+#. "$_EXEC"/session_lock.sh
+. "$_EXEC"/widgets.sh
+
+export MD_HTML="false"
+if [ "$(which awk)" ]; then
+ markdown() { awk -f "$_EXEC/cgilite/markdown.awk"; }
+else
+ markdown() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; }
+fi
+
+PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")"
+
+#git init "$_DATA" >/dev/null &
+
+yield_page(){
+ title="${1:-Webpoll}" page="$2"
+ printf '%s\r\n' 'Content-Type: text/html; charset=utf-8' \
+ "Content-Security-Policy: script-src 'none'" \
+ ''
+ { printf '[html
+ [head
+ [meta name="viewport" content="width=device-width"]
+ [link rel="stylesheet" type="text/css" href="%s/common.css"]
+ [link rel="stylesheet" type="text/css" href="%s/widgets.css"]
+ [link rel="stylesheet" type="text/css" href="%s/webpoll.css"]
+ [title %s]
+ ] [body class="%s"
+ ' "$_BASE" "$_BASE" "$_BASE" "$(HTML "$title")" "$page"
+ cat
+ printf '] ]'
+ } |"$_EXEC/cgilite/html-sh.sed" -u
+}
+
+pagename() {
+ local id="$1"
+ local file="$_DATA/$id"
+ if [ -f "$file" ]; then
+ DBM "$file" get title || printf 'Unnamed Page'
+ else
+ return 1;
+ fi
+}
+
+page_home() {
+ if [ "$REQUEST_METHOD" = POST ]; then
+ case $(POST start) in
+ date)
+ id="$(randomid)"
+ touch "$_DATA/$id"
+ REDIRECT "$_BASE/$id/newdate"
+ ;;
+ options)
+ id="$(randomid)"
+ touch "$_DATA/$id"
+ REDIRECT "$_BASE/$id/newoptions"
+ ;;
+ *) REDIRECT "$_BASE/";;
+ esac
+ else
+ recent="$(COOKIE pages)"
+ yield_page "Start a Poll" "home" <<-EOF
+ [form method=post
+ [submit "start" "date" Start a new poll]
+ $(if [ "$recent" ]; then
+ printf '[h2 Recent Polls][ul .recent'
+ for page in $recent; do
+ [ -f "$_DATA/$(checkid "$page")" ] \
+ && printf '[li [a href="./%s" . %s]]' "$page" "$(pagename "$page" |HTML)"
+ done
+ printf ']'
+ fi)
+ ]
+ EOF
+ fi
+}
+page_newdate() { . "$_EXEC"/newdate.sh; }
+page_poll() { . "$_EXEC/poll.sh"; }
+
+case ${PATH_INFO} in
+ /favicon.ico) printf '%s\r\n' 'Content-Length: 0' '';;
+ /common.css) FILE "$_EXEC/cgilite/common.css";;
+ /widgets.css|/webpoll.css) FILE "${_EXEC}/${PATH_INFO}";;
+ /) page_home;;
+ /*/newdate) page_newdate;;
+ /*/newoptions);;
+ /[0-9a-zA-Z:=]???????????????) page_poll;;
+esac
+
+exit 0
--- /dev/null
+#!/bin/sh
+
+fs_timeofday() {
+ local todall="$(DBM "$file" get todall)" time c=0
+ cat <<-EOF
+ [fieldset .timeofday
+ [label .todstart Start Time (optional):
+ ]
+ [label .todend End Time (optional):
+ ]
+ $(for time in ${todall:--}; do
+ c=$((c + 1))
+ printf '
+ <input name="todstart" value="%s" placeholder="HH:MM" list="dlist_timeofday"
+ pattern="^(0?\[0-9\]|1\[0-9\]|2\[0-3\]):(\[0-5\]\[0-9\])$"/>
+ <input name="todend" value="%s" placeholder="HH:MM" list="dlist_timeofday"
+ pattern="^(0?\[0-9\]|1\[0-9\]|2\[0-3\]):(\[0-5\]\[0-9\])$"/>
+ [submit "todremove" "%i" -]
+ ' "${time%-*}" "${time#*-}" "${c}"
+ done)
+ [submit "addtime" "global" + Add Time Option]
+ [checkbox "none" "none" .splittimes disabled=disabled] [submit "splittimes" "yes" Separate Time Options per Day]
+ ]
+ EOF
+}
+
+fs_splittimes() {
+ local days day times time c
+ days="$(DBM "$file" get dates)"
+ cat <<-EOF
+ [fieldset .splittimes
+ [checkbox "none" "none" .splittimes checked disabled=disabled] [submit "splittimes" "no" Separate Time Options per Day]
+ $([ ! "$days" ] && printf '[p You have not selected any days yet.]\n')
+ $(for day in $days; do
+ date -d $day +"[h2 . %A - %B %_d, %Y]"
+ times=$(DBM "$file" get "tod_$day")
+ for time in ${times:--}; do
+ c=$((c + 1))
+ printf '
+ <input name="todstart_%s" value="%s" placeholder="HH:MM" list="dlist_timeofday"
+ pattern="^(0?\[0-9\]|1\[0-9\]|2\[0-3\]):(\[0-5\]\[0-9\])$"/>
+ <input name="todend_%s" value="%s" placeholder="HH:MM" list="dlist_timeofday"
+ pattern="^(0?\[0-9\]|1\[0-9\]|2\[0-3\]):(\[0-5\]\[0-9\])$"/>
+ [submit "todremove" "%i" -]
+ ' "$day" "${time%-*}" "$day" "${time#*-}" "${c}"
+ done
+ printf '[submit "addtime" "%s" + Add Time Option]' "$day"
+ done)
+ ]
+ EOF
+}
+
+if [ "$REQUEST_METHOD" = POST ]; then
+ id="${PATH_INFO%/newdate}"; id="${id#/}"
+ file="$_DATA/$id"
+ month="$(POST month |grep -m 1 -xE '[0-9]{4}-(0[1-9]|1[012])')"
+ todremove="$(POST todremove |grep -m 1 -xE '[0-9]+')"
+ splittimes="$(POST splittimes |grep -m 1 -xE 'yes|no')"
+ addtime="$(POST addtime)"
+
+ if [ "$splittimes" = yes ]; then
+ DBM "$file" set splittimes "$splittimes"
+ splittimes="no" # receive remainder of todall form
+ elif [ "$splittimes" = no ]; then
+ DBM "$file" set splittimes "$splittimes"
+ splittimes="yes" # receive remainder of splittimes form
+ else
+ splittimes="$(DBM "$file" get splittimes || printf no)"
+ fi
+
+ DBM "$file" set title "$(POST title)"
+ DBM "$file" set description "$(POST description)"
+ # Store common time options "todall"
+ [ "$splittimes" = no ] && DBM "$file" set todall "$(
+ for todcount in $(seq 1 $(POST_COUNT todstart)); do
+ [ "$todremove" -eq "$todcount" ] 2>&- && continue;
+ todstart="$(POST todstart "$todcount")"
+ todend="$(POST todend "$todcount")"
+ [ "${todstart%:??}" -lt "${todend%:??}" -o "${todstart%:??}" -eq "${todend%:??}" -a "${todstart#*:}" -lt "${todend#*:}" ] \
+ 2>&- \
+ && { printf '%02i:%02i-%02i:%02i\n' "${todstart%:??}" "${todstart#*:}" "${todend%:??}" "${todend#*:}"; }\
+ || { [ "${todstart%:??}" -ge 0 -a "${todstart#*:}" -ge 0 ] 2>&- && printf '%02i:%02i-\n' "${todstart%:??}" "${todstart#*:}"; }
+ done |grep -xE '^([01][0-9]|2[0-3]):([0-5][0-9])-(([01][0-9]|2[0-3]):([0-5][0-9]))?$' |sort -u
+ )"
+ [ "$addtime" = global ] && DBM "$file" append todall "${BR}-"
+
+ # Store per-date time options "tod_YYYY-mm-dd"
+ [ "$splittimes" = yes ] && for date in $(DBM "$file" get dates); do
+ todremove="$(POST todremove_$date |grep -m 1 -xE '[0-9]+')"
+ DBM "$file" set "tod_$date" "$(
+ for todcount in $(seq 1 $(POST_COUNT "todstart_${date}")); do
+ [ "$todremove" -eq "$todcount" ] 2>&- && continue;
+ todstart="$(POST "todstart_${date}" "$todcount")"
+ todend="$(POST "todend_${date}" "$todcount")"
+ [ "${todstart%:??}" -lt "${todend%:??}" -o "${todstart%:??}" -eq "${todend%:??}" -a "${todstart#*:}" -lt "${todend#*:}" ] \
+ 2>&- \
+ && { printf '%02i:%02i-%02i:%02i\n' "${todstart%:??}" "${todstart#*:}" "${todend%:??}" "${todend#*:}"; }\
+ || { [ "${todstart%:??}" -ge 0 -a "${todstart#*:}" -ge 0 ] 2>&- && printf '%02i:%02i-\n' "${todstart%:??}" "${todstart#*:}"; }
+ done |grep -xE '^([01][0-9]|2[0-3]):([0-5][0-9])-(([01][0-9]|2[0-3]):([0-5][0-9]))?$' |sort -u
+ )"
+ [ "$addtime" = "$date" ] && DBM "$file" append "tod_${date}" "${BR}-"
+ done
+
+ DBM "$file" set dates "$(
+ for date in $(seq 0 $(POST_COUNT date)); do
+ [ "$date" -eq 0 ] \
+ && POST date_add \
+ || POST date "$date"
+ printf \\n
+ done \
+ | grep -vxF "$(POST date_remove)" \
+ | grep -xE '^[0-9]{4}-((01|03|05|07|08|10|12)-([012][0-9]|3[01])|(04|06|09|11)-([012][0-9]|30)|02-[012][0-9])$' \
+ | sort -u
+ )"
+
+ if [ "$(POST cancel)" = cancel ]; then
+ rm -- "$file"
+ REDIRECT "$_BASE/"
+ elif [ "$(POST post)" = post ]; then
+ REDIRECT "$_BASE${PATH_INFO%/*}"
+ else
+ REDIRECT "$_BASE$PATH_INFO${month:+?month=}${month}"
+ fi
+else
+ id="${PATH_INFO%/newdate}"; id="${id#/}"
+ file="$_DATA/$id"
+ month="$(GET month |grep -m1 -xE '[0-9]{4}-(0[1-9]|1[012])' || date +%Y-%m)"
+ Y="${month%-*}"; m="${month#*-}"; Y=${Y#0}; m=${m#0};
+ [ "$m" = 1 ] && prev=$(printf '%04i-%02i' $((Y - 1)) 12) || prev=$(printf '%04i-%02i' $Y $((m - 1)))
+ [ "$m" = 12 ] && next=$(printf '%04i-%02i' $((Y + 1)) 01) || next=$(printf '%04i-%02i' $Y $((m + 1)))
+ dates="$(DBM "$file" get dates)"
+ days="$(printf %s "$dates" |sed -E "/^${month}-/!d; s;^.*-([0-9]{2})$;\1;g")"
+ additional="$(printf %s "$dates" |sed -E "/^${month}-/d;")"
+ splittimes="$(DBM "$file" get splittimes || printf no)"
+
+ yield_page "$(pagename "$id")" "newdate" <<-EOF
+ $(dlist_timeofday)
+ [form method=post
+ [input name=title value="$(DBM "$file" get title |HTML)" placeholder="Title"]
+ [textarea name=description placeholder="Description" . $(DBM "$file" get description |HTML)]
+ [fieldset .date
+ $(printf '[hidden "date" "%s"]' $additional)
+ [submit "month" "$prev" Previous Month]
+ $([ "$splittimes" = yes ] && w_month submit date "$month" $days || w_month multiple date "$month" $days)
+ [submit "month" "$next" Next Month]
+ [hidden "month" "$month"]
+ ]
+ $([ "$splittimes" = "yes" ] && fs_splittimes || fs_timeofday )
+ [submit "cancel" "cancel" Cancel]
+ [submit "post" "post" Post Event]
+ ]
+ EOF
+fi
--- /dev/null
+#!/bin/sh
+
+id="$(checkid "${PATH_INFO#/}")"
+file="${_DATA}/${id}"
+
+#cancel if poll is invalid
+[ "$id" -a -f "$file" ] || REDIRECT "$_BASE/"
+
+tkey() {
+ # convert time stamps for use in POST keys
+ local str="$1" out
+ while [ "$str" ]; do
+ case $str in
+ :*) out="${out}.";;
+ *) out="${out}${str%"${str#?}"}";;
+ esac
+ str="${str#?}"
+ done
+ printf %s "$out"
+}
+
+timelist() {
+ local dates todall splittimes
+ local date tod todsplit
+
+ if [ "$splittimes" = no -a "$dates" -a "$todall" ]; then
+ for date in $dates; do for tod in $todall; do
+ printf %s\\n "${date}_${tod%-}"
+ done ;done
+
+ elif [ "$splittimes" = no -a "$dates" ]; then
+ for date in $dates; do
+ printf %s\\n "${date}"
+ done
+
+ elif [ "$splittimes" = no -a "$todall" ]; then
+ for tod in $todall; do
+ printf %s\\n "${tod%-}"
+ done
+
+ elif [ "$splittimes" = yes ]; then
+ for date in $dates; do
+ todsplit="$(DBM "$file" get "tod_$date")"
+ [ "$todsplit" ] \
+ && for tod in $todsplit; do printf %s\\n "${date}_${tod%-}"; done \
+ || printf %s\\n "${date}"
+ done
+
+ else
+ return 1
+
+ fi
+}
+
+table_poll() {
+ local splittimes="$(DBM "$file" get splittimes || printf no)"
+ local dates="$(DBM "$file" get dates)"
+ local todall="$(DBM "$file" get todall)"
+ local timelist="$(timelist)"
+ local time date span name
+
+ [ "$timelist" ] || return 1
+
+ printf '[table .poll [thead\n'
+ # date header
+ if [ "$dates" ]; then
+ printf '[tr .dates [th]'
+ for date in $dates; do
+ span=0; for time in $timelist; do case $time in
+ ${date}*) span=$((span + 1));;
+ esac; done
+ date -d "$date" +"[th colspan=\"${span}\" . %A <br/> %B %_d, %Y]";
+ done
+ printf '[th]]\n'
+ fi
+
+ # tod header
+ if [ "$splittimes" = yes -o "$todall" ]; then
+ printf '[tr .tod [th]'
+ for time in $timelist; do
+ [ "${time#*_}" = "${time}" ] && time="${time}_"
+ printf '[th . %s]' "${time#*_}"
+ done
+ printf '[th]]\n'
+ fi
+
+ printf '][tbody\n'
+
+ { DBM "$file" get participants; printf \\n; } |while read -r name; do
+ yes="$(DBM "$file" get "reply_yes_${name}")"
+ no="$(DBM "$file" get "reply_no_${name}")"
+ maybe="$(DBM "$file" get "reply_maybe_${name}")"
+
+ printf '[tr [th .name . %s]' "$(HTML "$name")"
+ for time in $timelist; do
+ printf %s "$yes" |grep -qwF "$time" && printf '[td .yes Yes]' && continue
+ printf %s "$no" |grep -qwF "$time" && printf '[td .no No]' && continue
+ printf %s "$maybe" |grep -qwF "$time" && printf '[td .maybe Maybe]' && continue
+ printf '[td .missing . ?]'
+ done
+ printf '[td]]'
+ done
+
+ # Submit line
+ printf '[tr .new [td [input name="name" value="" placeholder="Your Name" autocomplete=off]]'
+ for time in $timelist; do
+ time="$(tkey "$time")"
+ printf '[td [radio "%s" "yes" #yes_%s][label for="yes_%s" Yes]
+ [radio "%s" "no" #no_%s][label for="no_%s" No]
+ [radio "%s" "maybe" #maybe_%s][label for="maybe_%s" Maybe]
+ ]' "${time}" "${time}" "${time}" \
+ "${time}" "${time}" "${time}" \
+ "${time}" "${time}" "${time}"
+ done
+ printf '[td [submit "new" "new" Submit]]]\n'
+
+ printf ']]'
+}
+
+if [ "$REQUEST_METHOD" = POST ]; then
+ local name="$(POST name |grep -m 1 -xE '.*[^ ].*')"
+ local splittimes="$(DBM "$file" get splittimes || printf no)"
+ local dates="$(DBM "$file" get dates)"
+ local todall="$(DBM "$file" get todall)"
+ local timelist="$(timelist)"
+ local time yes no maybe reply
+
+ if [ "$(POST new)" = new ]; then
+ if [ ! "$name" ]; then
+ REDIRECT "${_BASE}${PATH_INFO}#ERROR_NONAME"
+ elif DBM "$file" get participants |grep -qxF "$name"; then
+ REDIRECT "${_BASE}${PATH_INFO}#ERROR_NAMEEXISTS"
+ fi
+ DBM "$file" append participants "${BR}${name}" || DBM "$file" insert participants "${name}" \
+ || REDIRECT "${_BASE}${PATH_INFO}#ERROR_DBACCESS"
+
+ for time in $timelist; do reply="$(POST "$(tkey "$time")")"; case $reply in
+ yes) yes="${yes}${yes:+ }${time}";;
+ no) no="${no}${no:+ }${time}";;
+ maybe) maybe="${maybe}${maybe:+ }${time}";;
+ esac; done
+ DBM "$file" set "reply_yes_${name}" "$yes"
+ DBM "$file" set "reply_no_${name}" "$no"
+ DBM "$file" set "reply_maybe_${name}" "$maybe"
+ REDIRECT "${_BASE}${PATH_INFO}"
+ fi
+
+else
+ pagename="$(pagename "$id")"
+
+ yield_page "$pagename" poll <<-EOF
+ [form method=POST
+ [section .description
+ [h1 .title $(HTML "$pagename")]
+ $(DBM "$file" get description |markdown)
+ ]
+ $(table_poll || printf '[p Poll parameters are invalid]')
+ ]
+ EOF
+fi
--- /dev/null
+body {
+ background-size: 4pt 4pt;
+ background-image: /* #6AF #6FF */
+ linear-gradient( 0deg, transparent 25%, rgba(102,170,255,.5) 25% 50%, transparent 50% 75%, rgba(102,255,255,.5) 75%),
+ linear-gradient(90deg, transparent 25%, rgba(102,170,255,.5) 25% 50%, transparent 50% 75%, rgba(102,255,255,.5) 75%);
+}
+
+body > form {
+ background-color: rgba(255,255,255,.75) ;
+ padding: 1em; margin: 1em auto 1em auto;
+ border-radius: .5ex;
+ box-shadow: #000 .125em .125em 1em;
+}
+
+body.home form {
+ position: fixed;
+ left: 50%; top: 50%;
+ transform: translate(-50%, -50%);
+}
+
+body.poll form {
+ text-align: center;
+ max-width: 95%;
+}
+body.poll .description {
+ text-align: left;
+ max-width: 50em;
+ padding: 1pt 1em 1em 1em;
+ margin: auto;
+ margin-bottom: 1em;
+ background-color: rgba(255,255,255,.5);
+}
+body.poll .description .title {
+ text-align: center;
+}
+body.poll table {
+ background-color: rgba(255,255,255,.5);
+ border-collapse: collapse;
+ margin: auto;
+ -border: .5pt solid;
+ box-shadow: #000 .25em .25em .5em;
+ border-radius: 2pt;
+}
+body.poll table thead tr.dates th {
+ padding: .25em;
+}
+body.poll table thead tr.tod th {
+ border-width: .5pt;
+ border-style: none solid none solid;
+ padding: .25em;
+}
+body.poll table tbody tr td {
+ text-align: center;
+ border: .5pt solid;
+ padding: 0 .25em;
+}
+body.poll table tbody tr td:first-child,
+body.poll table tbody tr td:last-child,
+body.poll table thead tr th:first-child,
+body.poll table thead tr th:last-child { border: none; }
+
+body.poll table tbody tr th.name { padding: .25em .5em; text-align: right; }
+body.poll table tbody tr td.yes { background-color: #AFA; }
+body.poll table tbody tr td.no { background-color: #FAA; }
+body.poll table tbody tr td.maybe { background-color: #FFA; }
+
+body.poll table td input[type=radio] { display: none; }
+body.poll table td input[type=radio] + label {
+ font-size: .875em;
+ text-decoration: underline;
+ color: #066;
+ padding: .25em;
+ margin: 0;
+}
+body.poll table td input[type=radio]:checked + label {
+ font-weight: bold;
+}
+body.poll table td input[type=radio][value=yes]:checked + label {
+ background-color: #AFA;
+ margin: 0 -1.5pt;
+}
+body.poll table td input[type=radio][value=no]:checked + label {
+ background-color: #FAA;
+ margin: 0 -.75pt;
+}
+body.poll table td input[type=radio][value=maybe]:checked + label {
+ background-color: #FFA;
+ margin: 0 -1.75pt;
+}
+
+body.newdate form {
+ text-align: center;
+ max-width: 100%;
+}
+body.newdate form fieldset.date,
+body.newdate form fieldset.timeofday,
+body.newdate form fieldset.splittimes {
+ display: inline-block;
+ vertical-align: top;
+ margin: .5em 0 1em 0;
+}
+
+body.newdate form { width: 26em; }
+body.newdate form input[name=title],
+body.newdate form textarea[name=description] {
+ width: 100%;
+}
+body.newdate form fieldset.date,
+body.newdate form fieldset.timeofday,
+body.newdate form fieldset.splittimes {
+ width: 100%;
+}
+
+@media(min-width: 50em) {
+ body.newdate form { width: 50em; }
+ body.newdate form input[name=title],
+ body.newdate form textarea[name=description] {
+ width: 100%;
+ }
+ body.newdate form fieldset.date,
+ body.newdate form fieldset.timeofday,
+ body.newdate form fieldset.splittimes {
+ width: 49.5%; width: calc(50% - .375ex);
+ }
+ body.newdate form fieldset.date { padding-right: .75em; }
+ body.newdate form fieldset.timeofday,
+ body.newdate form fieldset.splittimes { padding-left: .75em; }
+}
+
+body.newdate form input[name=title],
+body.newdate form textarea[name=description] {
+ display: block;
+ margin-bottom: .75em;
+}
+body.newdate form textarea[name=description] {
+ height: 8em;
+}
+
+body.newdate form .date button[name=month] {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ height: 2.375em;
+ width: 2em; width: calc(50% - 9em);
+ padding: 0;
+ color: transparent;
+ background-color: transparent;
+ border: none;
+ overflow: hidden;
+ z-index: 1;
+}
+body.newdate form .date button[name=month]:before {
+ display: block;
+ content: '<';
+ font-size: 1.75em;
+ font-weight: bold;
+ color: #666;
+ margin-top: .25em;
+}
+body.newdate form .date table + button[name=month] { right: .75em; }
+body.newdate form .date table + button[name=month]:before {
+ content: '>';
+}
+
+body.newdate form .date table.calendar {
+ background-color: #FFF;
+ font-size: 17pt;
+ -vertical-align: middle;
+}
+
+body.newdate form .timeofday label.todstart,
+body.newdate form .timeofday label.todend {
+ display: inline-block;
+ margin: 0;
+ font-weight: bold;
+ text-align: left;
+ font-size: .75em;
+ width: 49%; width: calc(50% - 2.5pt);
+}
+body.newdate form fieldset > input[name^=todstart],
+body.newdate form fieldset > input[name^=todend] {
+ display: inline-block;
+ margin: 0;
+ width: 49%; width: calc(50% - 2.5pt);
+ text-align: right;
+}
+body.newdate form fieldset > input[name^=todend] {
+ width: 39%; width: calc( 50% - 4.375ex);
+}
+
+body.newdate form fieldset button[name^=addtime] {
+ width: 100%;
+}
+
+body.newdate form fieldset input.splittimes { display: none;}
+body.newdate form fieldset input.splittimes + * { left: 12pt; }
+body.newdate form fieldset input.splittimes + *:before {
+ position: absolute;
+ width: 16pt; height: 16pt;
+ left: -24pt;
+ content: '';
+ text-align: center;
+ font-weight: bold;
+ font-size: 1.75em;
+ border: 1pt solid;
+ border-radius: .25ex;
+ background-color: #FFF;
+}
+body.newdate form fieldset input.splittimes:checked + *:before { content: '\2713'; background-color: #6AF;}
+body.newdate form .timeofday button[name=splittimes] { margin-top: 1.5em;}
+
+body.newdate form .splittimes p { margin-top: 2em; padding: .5em; background: rgba(255,255,255,.5); }
--- /dev/null
+table.calendar {
+ display: inline-block;
+ border-collapse: collapse;
+ vertical-align: top;
+}
+table.calendar td {
+ border: .5pt solid;
+}
+table.calendar th {
+ font-weight: normal;
+}
+
+table.calendar thead tr.monthname {
+ border-style: solid;
+ border-width: .5pt .5pt .5pt .5pt;
+ line-height: 1.5em;
+ font-size: 1.125em;
+}
+table.calendar thead tr.weekday {
+ line-height: 1.5em;
+}
+table.calendar thead tr.weekday th:first-of-type {
+ border-style: solid;
+ border-width: 0pt 0pt 0pt .5pt;
+}
+table.calendar thead tr.weekday th:last-child {
+ border-right: .5pt solid;
+}
+table.calendar tbody tr th {
+ border-left: .5pt solid #000;
+}
+table.calendar tbody tr:last-child th {
+ border-bottom: .5pt solid #000;
+}
+
+table.calendar tbody tr th.weekno {
+ width: 2em;
+ padding: 0 .25em;
+ text-align: right;
+ font-weight: normal;
+ color: #888;
+}
+
+table.calendar input[type=radio],
+table.calendar input[type=checkbox] {
+ display: none;
+}
+table.calendar td label,
+table.calendar td button {
+ display: inline-block;
+ width: 2em;
+ margin: 0; padding: .25em;
+ text-align: right;
+ line-height: 1em;
+ box-shadow: none;
+ border-radius: 0;
+ border: none;
+}
+table.calendar td input:checked + label,
+table.calendar td label[checked],
+table.calendar td button[name$=_remove] {
+ font-weight: bold;
+ line-height: .75em;
+ border: .125em solid;
+}
--- /dev/null
+#!/bin/sh
+
+checked(){
+ local check="$1"; shift 1;
+ for comp in "$@"; do
+ if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then
+ printf 'checked="checked"'
+ break;
+ fi 2>/dev/null
+ done
+}
+selected(){
+ local check="$1"; shift 1;
+ for comp in "$@"; do
+ if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then
+ printf 'selected="selected"'
+ break;
+ fi 2>/dev/null
+ done
+}
+
+w_month() {
+ # Arguments:
+ # 1. (optional) select, multiple, submit, none - default: select
+ # 2. (optional) Name of form field - default: "date"
+ # 3. (optional) Month to display in format: YYYY-MM - default: current month
+ # 4. (optional, multiple) Days to preselect in format: DD - default: none
+
+ local type="${1:-select}" input="${2:-date}" month="$3"
+ shift 3; local selected="$*"
+ local dow dom days n=1 Y m d V w B
+ if [ $month ]; then
+ read Y m d V w B<<-EOF
+ $(date -d "${month}-01" +"%_Y %_m %_d %_V %w %B")
+ EOF
+ else
+ read Y m d V w <<-EOF
+ $(date +"%Y %m %d %V %w")
+ EOF
+ month="$Y-$m"
+ V="$((V - d / 7))"
+ [ $V -lt 1 ] && V=$((V + 53))
+ fi
+
+ case $m in
+ [13578]|10|12)
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31";;
+ [469]|11)
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30";;
+ 2) if [ $(( Y / 400 )) = 0 ]; then
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29";
+ elif [ $(( Y / 100 )) = 0 ]; then
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28";
+ elif [ $(( Y / 4 )) = 0 ]; then
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29";
+ else
+ days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28";
+ fi;;
+ esac
+
+ printf '[table .calendar month=%s [thead
+ [tr .monthname [th colspan=8 . %s]]
+ [tr .weekday [th][th . %s][th . %s][th . %s][th . %s][th . %s][th . %s][th . %s]]
+ ][tbody
+ ' "$month" "$B $Y" Mo Tu We Th Fr Sa Su
+ for dom in $days; do
+ dow=$(( ( w - d + 35 + dom ) % 7))
+ [ $dow = 1 -o $dom = 1 ] && printf '[tr [th .weekno . %i]' $V
+ [ $dom = 1 ] && while [ $n -lt $(( ($dow + 6) % 7 + 1)) ]; do printf '[td ]'; n=$((n + 1)); done
+ date="$(printf "%04i-%02i-%02i" $Y $m $dom)"
+ case $type in
+ none)
+ printf '[td [label %s . %i]]' \
+ "$(checked $dom $selected)" "$dom"
+ ;;
+ multiple)
+ printf '[td [input type=checkbox id="%s_%s" name="%s" value="%s" %s][label for="%s_%s" . %i]]' \
+ "$input" "$date" "$input" "$date" "$(checked $dom $selected)" "$input" "$date" "$dom"
+ ;;
+ submit)
+ [ "$(checked $dom $selected)" ] \
+ && printf '[td [submit "%s_remove" "%s" . %i][hidden "%s" "%s"]]' "$input" "$date" "$dom" "$input" "$date" \
+ || printf '[td [submit "%s_add" "%s" . %i]]' "$input" "$date" "$dom"
+ ;;
+ select|*)
+ printf '[td [input type=radio id="%s_%s" name="%s" value="%s" %s][label for="%s_%s" . %i]]' \
+ "$input" "$date" "$input" "$date" "$(checked $dom $selected)" "$input" "$date" "$dom"
+ ;;
+ esac
+ if [ $dow = 0 ]; then
+ printf ']\n'
+ V=$((V + 1))
+ [ $m = 1 -a $V -ge 53 ] && V=1
+ fi
+ done
+ if [ $dow -gt 0 ]; then
+ while [ $dow -le 6 ]; do printf '[td ]'; dow=$((dow + 1)) ; done
+ printf ']\n'
+ fi
+ printf ']]'
+}
+
+dlist_timeofday() {
+ local step="${1:-15}" id="${2:-dlist_timeofday}"
+ printf '[datalist id="%s"\n' $id
+ for h in $(seq 0 23); do for m in $(seq 0 "$step" 59); do
+ printf '[option value="%i:%02i"]\n' $h $m
+ done; done
+ printf ']\n'
+}