From 97aa0847ed5fd294f1ba8011f785c520b0439aa8 Mon Sep 17 00:00:00 2001 From: Abdhilahi Wabwire <124460109+AbdhilahiRWabwire@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:27:40 -0700 Subject: [PATCH] Widgets to Refactor Later --- lib/markup/disclosure.html | 45 ++ lib/markup/menu-button.html | 21 + lib/markup/meter.html | 17 + lib/markup/navigation-menu.html | 163 ++++ lib/markup/radio-group.html | 19 + lib/markup/switch.html | 30 + lib/markup/tabs.html | 51 ++ lib/markup/toolbar.html | 82 ++ lib/styles/disclosure.css | 49 ++ lib/styles/menu-button.css | 83 ++ lib/styles/meter.css | 17 + lib/styles/navigation-menu.css | 199 +++++ lib/styles/radio-group.css | 45 ++ lib/styles/switch.css | 84 ++ lib/styles/tabs.css | 76 ++ lib/styles/toolbar.css | 237 ++++++ lib/widgets/disclosure.js | 87 +++ lib/widgets/menu_button.js | 349 +++++++++ lib/widgets/meter.js | 115 +++ lib/widgets/navigation_menu.js | 739 ++++++++++++++++++ lib/widgets/radio_group,js | 177 +++++ lib/widgets/switch.js | 34 + lib/widgets/tabs.js | 136 ++++ lib/widgets/toolbar.js | 1277 +++++++++++++++++++++++++++++++ 24 files changed, 4132 insertions(+) create mode 100644 lib/markup/disclosure.html create mode 100644 lib/markup/menu-button.html create mode 100644 lib/markup/meter.html create mode 100644 lib/markup/navigation-menu.html create mode 100644 lib/markup/radio-group.html create mode 100644 lib/markup/switch.html create mode 100644 lib/markup/tabs.html create mode 100644 lib/markup/toolbar.html create mode 100644 lib/styles/disclosure.css create mode 100644 lib/styles/menu-button.css create mode 100644 lib/styles/meter.css create mode 100644 lib/styles/navigation-menu.css create mode 100644 lib/styles/radio-group.css create mode 100644 lib/styles/switch.css create mode 100644 lib/styles/tabs.css create mode 100644 lib/styles/toolbar.css create mode 100644 lib/widgets/disclosure.js create mode 100644 lib/widgets/menu_button.js create mode 100644 lib/widgets/meter.js create mode 100644 lib/widgets/navigation_menu.js create mode 100644 lib/widgets/radio_group,js create mode 100644 lib/widgets/switch.js create mode 100644 lib/widgets/tabs.js create mode 100644 lib/widgets/toolbar.js diff --git a/lib/markup/disclosure.html b/lib/markup/disclosure.html new file mode 100644 index 0000000..4125899 --- /dev/null +++ b/lib/markup/disclosure.html @@ -0,0 +1,45 @@ + + + +

Parking FAQs

+
+
+ +
+
+
+ Park at the nearest available parking meter without paying the meter and call 999-999-9999 to report the problem. + We will note and approve your alternate location and will investigate the cause of the shortage in your assigned facility. +
+
+
+ +
+
+
+ You should come to the Parking office and report the loss. + There is a fee to replace your lost permit. + However, if your permit was stolen, a copy of a police report needs to be submitted along with a stolen parking permit form for a fee replacement exemption. +
+
+
+ +
+
+
+ All facilities are restricted from 2:00 am - 6:00 am on all days. + No exceptions are made for any holiday or recess except those officially listed as a Holidays in the calendar. + Please note: 24-hour rental spaces, 24-hour rental lots, and disabled parking is enforced at all times. +
+
+
+ +
+
+
+ Some parking facility restrictions differ from others. + Be sure to take note of the signs at each lot entrance. +
+
+
+ diff --git a/lib/markup/menu-button.html b/lib/markup/menu-button.html new file mode 100644 index 0000000..cdf7b14 --- /dev/null +++ b/lib/markup/menu-button.html @@ -0,0 +1,21 @@ + + + + +

+ +

+ diff --git a/lib/markup/meter.html b/lib/markup/meter.html new file mode 100644 index 0000000..b6337cd --- /dev/null +++ b/lib/markup/meter.html @@ -0,0 +1,17 @@ + + + +

+ The value of this meter changes every 5 seconds. + Use the pause button to stop changes. +

+

Central Processing Unit (CPU) Usage

+

+ +

+
+ +
+ diff --git a/lib/markup/navigation-menu.html b/lib/markup/navigation-menu.html new file mode 100644 index 0000000..5a011d5 --- /dev/null +++ b/lib/markup/navigation-menu.html @@ -0,0 +1,163 @@ + + + +
+ +
+
Mythical University
+
Using a Menubar for navigation links
+
+ + +
+
+

Mythical University

+
+

+
+
+
+ + +
+ diff --git a/lib/markup/radio-group.html b/lib/markup/radio-group.html new file mode 100644 index 0000000..f2a1763 --- /dev/null +++ b/lib/markup/radio-group.html @@ -0,0 +1,19 @@ + + + +

Pizza Crust

+ + + +

Pizza Delivery

+ + + diff --git a/lib/markup/switch.html b/lib/markup/switch.html new file mode 100644 index 0000000..a32ff8c --- /dev/null +++ b/lib/markup/switch.html @@ -0,0 +1,30 @@ + + + +
+ Accessibility Preferences + + + +
+ diff --git a/lib/markup/tabs.html b/lib/markup/tabs.html new file mode 100644 index 0000000..1c179ed --- /dev/null +++ b/lib/markup/tabs.html @@ -0,0 +1,51 @@ + + + +
+

Danish Composers

+
+ + + + +
+ +
+

+ Maria Theresia Ahlefeldt (16 January 1755 – 20 December 1810) was a Danish, (originally German), composer. + She is known as the first female composer in Denmark. + Maria Theresia composed music for several ballets, operas, and plays of the royal theatre. + She was given good critic as a composer and described as a “virkelig Tonekunstnerinde” ('a True Artist of Music'). +

+
+ + + +
+ diff --git a/lib/markup/toolbar.html b/lib/markup/toolbar.html new file mode 100644 index 0000000..4d4a37e --- /dev/null +++ b/lib/markup/toolbar.html @@ -0,0 +1,82 @@ + + + + +
+ + diff --git a/lib/styles/disclosure.css b/lib/styles/disclosure.css new file mode 100644 index 0000000..e002a7f --- /dev/null +++ b/lib/styles/disclosure.css @@ -0,0 +1,49 @@ +dl.faq button { + margin: 0; + margin-top: 16px; + padding: 4px 8px; + font-weight: bold; + font-size: 110%; + border: none; + background-color: transparent; + border-radius: 5px; + } + + dl dd { + margin: 0; + padding: 0; + margin-left: 1.5em; + padding-bottom: 20px; + border-bottom: 2px solid #777; + } + + dl.faq .desc { + margin: 0; + margin-top: 0.25em; + padding: 0.25em; + font-size: 110%; + display: none; + background-color: #fed; + } + + dl.faq button:hover, + dl.faq button:focus { + padding: 2px 6px; + background-color: #def; + border: 2px solid #005a9c; + cursor: pointer; + } + + dl.faq button[aria-expanded="false"]::before { + content: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' style='forced-color-adjust: auto;'%3E%3Cpolygon points='1 1, 1 11, 8 6' fill='currentcolor' stroke= 'currentcolor' /%3E%3C/svg%3E%0A"); + position: relative; + left: -2px; + } + + dl.faq button[aria-expanded="true"]::before { + content: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' style='forced-color-adjust: auto;'%3E%3Cpolygon points='1 1, 11 1, 6 8' fill='currentcolor' stroke= 'currentcolor' /%3E%3C/svg%3E "); + position: relative; + left: -4px; + top: 2px; + } + \ No newline at end of file diff --git a/lib/styles/menu-button.css b/lib/styles/menu-button.css new file mode 100644 index 0000000..b61bea4 --- /dev/null +++ b/lib/styles/menu-button.css @@ -0,0 +1,83 @@ +.menu-button-actions { + margin: 0; + padding: 0; + } + + .menu-button-actions button { + margin: 0; + padding: 6px; + display: inline-block; + position: relative; + background-color: #034575; + border: 1px solid #034575; + font-size: 0.9em; + color: white; + border-radius: 5px; + } + + .menu-button-actions [role="menu"] { + display: none; + position: absolute; + margin: 0; + padding: 7px 4px; + border: 2px solid #034575; + border-radius: 5px; + background-color: #eee; + } + + .menu-button-actions [role="menuitem"], + .menu-button-actions [role="separator"] { + margin: 0; + padding: 6px; + display: block; + width: 4em; + background-color: #eee; + color: black; + border-radius: 5px; + } + + .menu-button-actions [role="separator"] { + padding-top: 3px; + background-image: url("../images/separator.svg"); + background-position: center; + background-repeat: repeat-x; + } + + .menu-button-actions button svg.down { + padding-left: 0.125em; + fill: currentcolor; + stroke: currentcolor; + } + + .menu-button-actions button[aria-expanded="true"] svg.down { + transform: rotate(180deg); + } + + /* focus styling */ + + .menu-button-actions button:hover, + .menu-button-actions button:focus, + .menu-button-actions button[aria-expanded="true"] { + padding: 4px; + border: 3px solid #034575; + background: #eee; + color: #222; + outline: none; + margin: 0; + } + + .menu-button-actions [role="menuitem"].focus, + .menu-button-actions [role="menuitem"]:focus { + padding: 4px; + border: 2px solid #034575; + background: #034575; + color: #fff; + outline: none; + margin: 0; + } + + input.action:focus { + outline: 2px solid #034575; + background: #def; + } + \ No newline at end of file diff --git a/lib/styles/meter.css b/lib/styles/meter.css new file mode 100644 index 0000000..de56e6e --- /dev/null +++ b/lib/styles/meter.css @@ -0,0 +1,17 @@ +[role="meter"] { + padding: 2px; + width: 200px; + height: 40px; + border: 2px solid black; + border-radius: 5px; + } + + .fill { + width: 100%; + height: 100%; + box-sizing: border-box; + border: 2px solid black; + border-radius: 3px; + background-color: black; + } + \ No newline at end of file diff --git a/lib/styles/navigation-menu.css b/lib/styles/navigation-menu.css new file mode 100644 index 0000000..92080cd --- /dev/null +++ b/lib/styles/navigation-menu.css @@ -0,0 +1,199 @@ +@charset "utf-8"; + +.page header { + border: #005a9c solid 2px; + background: #005a9c; + color: white; + text-align: center; +} + +.page header .title { + font-size: 2.5em; + font-weight: bold; + font-family: serif; +} + +.page header .tagline { + font-style: italic; +} + +.page .main { + padding: 1em 5% 5em; + border-left: 2px solid #eee; + border-right: 2px solid #eee; +} + +.page footer { + border: #005a9c solid 2px; + background: #005a9c; + font-family: serif; + color: white; + font-style: italic; + padding-left: 1em; +} + +.page nav { + margin: 0; + padding: 0; + border: 2px solid #eee; +} + +.menubar-navigation { + margin: 0; + padding: 2px; + font-size: 110%; + list-style: none; + background-color: #eee; + border: #eee solid 1px; +} + +.menubar-navigation li { + margin: 0; + padding: 0; + border: 0 solid black; + list-style: none; +} + +.menubar-navigation > li { + display: inline-block; + position: relative; +} + +.menubar-navigation > li li { + display: block; +} + +.menubar-navigation > li > [role="menuitem"] { + display: inline-block; + margin: 2px; + padding: 4px; + padding-bottom: 8px; + background-color: #eee; + border: 0 solid #eee; + color: black; +} + +.menubar-navigation [role="separator"] { + padding-top: 3px; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cline x1='0' y1='6' x2='12' y2='6' style='stroke:black;stroke-width:1' /%3E%3C/svg%3E%0A"); + background-size: 10px 10px; + background-position: center; + background-repeat: repeat-x; +} + +.menubar-navigation [role="menu"] [role="menuitem"], +.menubar-navigation [role="menu"] [role="separator"] { + display: block; + width: 12em; + margin: 2px; + padding: 4px; + padding-left: 8px; + background-color: #eee; + border: 0 solid #eee; + color: black; +} + +.menubar-navigation [role="menuitem"] svg { + fill: currentcolor; + stroke: currentcolor; +} + +.menubar-navigation [role="menuitem"] svg.down { + padding-left: 0.125em; +} + +.menubar-navigation [role="menuitem"] svg.right { + position: absolute; + padding-top: 0.35em; + right: 0.75em; +} + +.menubar-navigation [role="menuitem"][aria-expanded="true"] svg.down { + transform: rotate(180deg); +} + +.menubar-navigation [role="menuitem"][aria-expanded="true"] svg.right { + transform: rotate(90deg) translate(5px, -5px); +} + +.menubar-navigation [role="menu"] { + display: none; + position: absolute; + margin: 0; + padding: 0; + padding: 7px 4px; + border: 2px solid #034575; + background-color: #eee; +} + +.menubar-navigation [role="group"] { + margin: 0; + padding: 0; +} + +/* aria-current styling */ + +.menubar-navigation > li > [role="menuitem"][aria-current], +.menubar-navigation > li > [role="menuitem"].aria-current-path { + padding-bottom: 2px; + border-bottom: 4px solid #034575; +} + +.menubar-navigation [role="menu"] [role="menuitem"].aria-current-path, +.menubar-navigation [role="menu"] [role="menuitem"][aria-current] { + padding-left: 4px; + border-left: 4px solid #034575; +} + +/* focus styling */ + +.menubar-navigation.focus { + padding: 0; + border: #034575 solid 3px; +} + +.menubar-navigation > li > [aria-expanded="true"], +.menubar-navigation > li > [role="menuitem"]:focus, +.menubar-navigation > li > [role="menuitem"]:hover { + outline: none; + background-color: #ccc; +} + +.menubar-navigation > li > [role="menuitem"]:focus, +.menubar-navigation > li > [role="menuitem"]:hover { + padding: 2px; + padding-bottom: 4px; + border: 2px solid #034575; +} + +.menubar-navigation [role="menu"] [aria-expanded="true"], +.menubar-navigation [role="menu"] [role="menuitem"]:focus, +.menubar-navigation [role="menu"] [role="menuitem"]:hover { + outline: none; + background-color: #ccc; +} + +.menubar-navigation [role="menu"] [role="menuitem"]:focus, +.menubar-navigation [role="menu"] [role="menuitem"]:hover { + padding: 2px; + padding-left: 6px; + border: 2px solid #034575; +} + +.menubar-navigation > li > [aria-expanded="true"].aria-current-path, +.menubar-navigation > li > [role="menuitem"].aria-current-path:focus, +.menubar-navigation > li > [role="menuitem"].aria-current-path:hover, +.menubar-navigation > li > [role="menuitem"][aria-current]:focus, +.menubar-navigation > li > [role="menuitem"][aria-current]:hover { + padding-bottom: 2px; + border-bottom: 4px solid #034575; +} + +.menubar-navigation [role="menu"] [aria-expanded="true"].aria-current-path, +.menubar-navigation [role="menu"] [role="menuitem"].aria-current-path:focus, +.menubar-navigation [role="menu"] [role="menuitem"].aria-current-path:hover, +.menubar-navigation [role="menu"] [role="menuitem"][aria-current]:focus, +.menubar-navigation [role="menu"] [role="menuitem"][aria-current]:hover { + padding-left: 4px; + border-left: 4px solid #034575; +} diff --git a/lib/styles/radio-group.css b/lib/styles/radio-group.css new file mode 100644 index 0000000..174bcc8 --- /dev/null +++ b/lib/styles/radio-group.css @@ -0,0 +1,45 @@ +[role="radiogroup"] { + padding: 0; + margin: 0; + list-style: none; + } + + [role="radiogroup"]:focus { + outline: none; + } + + [role="radio"] { + padding: 4px 8px; + border: 0 solid transparent; + border-radius: 5px; + display: inline-block; + position: relative; + cursor: default; + outline: none; + color: black; + } + + [role="radio"] + [role="radio"] { + margin-left: 1em; + } + + [role="radio"]::before { + position: relative; + top: 1px; + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='14' style='forced-color-adjust: auto;'%3E%3Ccircle cx='7' cy='7' r='6' stroke='rgb(0, 90, 156)' stroke-width='2' fill-opacity='0' /%3E%3C/svg%3E"); + } + + [role="radio"][aria-checked="true"]::before { + position: relative; + top: 1px; + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='14' style='forced-color-adjust: auto;'%3E%3Ccircle cx='7' cy='7' r='6' stroke='rgb(0, 90, 156)' stroke-width='2' fill-opacity='0' /%3E%3Ccircle cx='7' cy='7' r='3' fill='rgb(0, 90, 156)' stroke-opacity='0' /%3E%3C/svg%3E"); + } + + [role="radio"].focus, + [role="radio"]:hover { + padding: 2px 6px; + border: 2px solid #005a9c; + background-color: #def; + cursor: pointer; + } + \ No newline at end of file diff --git a/lib/styles/switch.css b/lib/styles/switch.css new file mode 100644 index 0000000..a709b7b --- /dev/null +++ b/lib/styles/switch.css @@ -0,0 +1,84 @@ +fieldset { + width: 22em; + } + + legend { + font-size: 110%; + } + + label { + display: block; + margin: 0.5em; + padding: 4px 4px 6px 6px; + border: 0 solid #005a9c; + border-radius: 5px; + width: 16em; + } + + label .label { + display: inline-block; + width: 9em; + user-select: none; + } + + label input[role="switch"] { + opacity: 0; + } + + label input[role="switch"] ~ .state { + display: inline-block; + user-select: none; + } + + label input[role="switch"] ~ .state > .container { + position: relative; + top: 2px; + display: inline-block; + border: 2px solid black; + width: 40px; + height: 20px; + border-radius: 11px; + } + + label input[role="switch"] ~ .state > .container > .position { + position: relative; + top: 1px; + left: 2px; + display: inline-block; + border: 2px solid black; + border-radius: 9px; + width: 14px; + height: 14px; + background: black; + opacity: 0.6; + } + + label input[role="switch"]:not(:checked) ~ .state span.on { + display: none; + } + + label input[role="switch"]:checked ~ .state > span.off { + display: none; + } + + label input[role="switch"]:checked ~ .state > .container > .position { + left: 20px; + border-color: green; + background: green; + opacity: 1; + } + + label.focus, + label:hover { + padding: 2px 2px 4px 4px; + border-width: 2px; + outline: none; + background-color: #def; + cursor: pointer; + } + + label.focus span.container, + label:hover span.container { + background-color: white; + } + \ No newline at end of file diff --git a/lib/styles/tabs.css b/lib/styles/tabs.css new file mode 100644 index 0000000..91ec356 --- /dev/null +++ b/lib/styles/tabs.css @@ -0,0 +1,76 @@ +.tabs { + font-family: "lucida grande", sans-serif; + } + + [role="tablist"] { + min-width: 100%; + } + + [role="tab"], + [role="tab"]:focus, + [role="tab"]:hover { + display: inline-block; + position: relative; + z-index: 2; + top: 2px; + margin: 0; + margin-top: 4px; + padding: 3px 3px 4px; + border: 1px solid hsl(219deg 1% 72%); + border-bottom: 2px solid hsl(219deg 1% 72%); + border-radius: 5px 5px 0 0; + background: hsl(220deg 20% 94%); + outline: none; + font-weight: bold; + max-width: 22%; + overflow: hidden; + text-align: left; + cursor: pointer; + } + + [role="tab"][aria-selected="true"] { + padding: 2px 2px 4px; + margin-top: 0; + border-width: 2px; + border-top-width: 6px; + border-top-color: rgb(36 116 214); + border-bottom-color: hsl(220deg 43% 99%); + background: hsl(220deg 43% 99%); + } + + [role="tab"][aria-selected="false"] { + border-bottom: 1px solid hsl(219deg 1% 72%); + } + + [role="tab"] span.focus { + display: inline-block; + margin: 2px; + padding: 4px 6px; + } + + [role="tab"]:hover span.focus, + [role="tab"]:focus span.focus, + [role="tab"]:active span.focus { + padding: 2px 4px; + border: 2px solid rgb(36 116 214); + border-radius: 3px; + } + + [role="tabpanel"] { + padding: 5px; + border: 2px solid hsl(219deg 1% 72%); + border-radius: 0 5px 5px; + background: hsl(220deg 43% 99%); + min-height: 10em; + width: 100%; + overflow: auto; + } + + [role="tabpanel"].is-hidden { + display: none; + } + + [role="tabpanel"] p { + margin: 0; + } + \ No newline at end of file diff --git a/lib/styles/toolbar.css b/lib/styles/toolbar.css new file mode 100644 index 0000000..2844f2c --- /dev/null +++ b/lib/styles/toolbar.css @@ -0,0 +1,237 @@ +[role="toolbar"] { + border: 2px solid transparent; + border-radius: 5px; + padding: 6px; + height: 44px; + width: 1050px; + background-color: #ececea; + } + + [role="toolbar"].focus { + border-color: #005a9c; + border-width: 3px; + padding: 5px; + } + + [role="toolbar"] .group { + padding: 0.25em; + display: block; + float: left; + } + + [role="toolbar"] .group:not(:first-child) { + margin-left: 0.75em; + } + + [role="toolbar"] button, + [role="toolbar"] [role="radio"], + [role="toolbar"] label, + [role="toolbar"] .spinbutton, + [role="toolbar"] a, + [role="toolbar"] .input { + border: 1px solid rgb(255 255 255); + outline: none; + display: inline-block; + padding: 6px 12px; + border-radius: 5px; + text-align: center; + background: rgb(255 255 255); + color: #222428; + font-size: 14px; + line-height: 1.5em; + margin-right: 0.25em; + } + + [role="toolbar"] button.popup { + position: relative; + } + + [role="toolbar"] button .popup-label { + display: block; + width: initial; + border: 1px solid white; + padding: 2px 4px; + border-radius: 5px; + position: absolute; + top: -30000em; + background-color: black; + color: white; + font-weight: normal; + } + + [role="toolbar"] button .popup-label.show { + text-align: center; + top: -2.5em; + } + + [role="toolbar"] button .popup-label::after, + [role="toolbar"] button .popup-label::before { + top: 100%; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + [role="toolbar"] button .popup-label::after { + border-color: rgb(0 0 0 / 0%); + border-top-color: #000; + border-width: 10px; + margin-left: -10px; + } + + [role="toolbar"] button .popup-label::before { + border-color: rgb(255 255 255 / 0%); + border-top-color: #fff; + border-width: 12px; + margin-left: -12px; + } + + [role="toolbar"] button[aria-pressed="true"], + [role="toolbar"] [role="radio"][aria-checked="true"] { + border-color: #555; + font-weight: bold; + background-color: #f4f4f4; + } + + [role="toolbar"] button[aria-disabled="true"] { + color: #889; + cursor: not-allowed; + } + + [role="toolbar"] button[aria-disabled="true"]:focus { + border-color: #005a9c; + } + + [role="toolbar"] button::-moz-focus-inner { + border: 0; + } + + [role="toolbar"] button:focus, + [role="toolbar"] [role="radio"]:focus, + [role="toolbar"] .spinbutton:focus, + [role="toolbar"] .focus, + [role="toolbar"] a:focus { + border-width: 2px; + border-color: #005a9c; + background: rgb(226 239 255); + padding: 5px 11px; + } + + [role="toolbar"] button:hover, + [role="toolbar"] [role="radio"]:hover, + [role="toolbar"] .spinbutton:hover, + [role="toolbar"] label.input:hover, + [role="toolbar"] a:hover { + border-color: #005a9c; + background: rgb(226 239 255); + } + + [role="toolbar"] [role="spinbutton"] .value, + [role="toolbar"] [role="spinbutton"] .increase, + [role="toolbar"] [role="spinbutton"] .decrease { + width: 60px; + display: inline-block; + padding: 0; + margin: 0; + } + + [role="toolbar"] button[aria-haspopup] span { + float: right; + } + + [role="toolbar"] button[aria-haspopup] span::after { + content: url("../images/pulldown-icon.svg"); + } + + [role="toolbar"] button[aria-haspopup]:focus span::after { + content: url("../images/pulldown-icon-focus.svg"); + } + + [role="toolbar"] [role="spinbutton"] .increase, + [role="toolbar"] [role="spinbutton"] .decrease { + width: 20px; + border: 1px solid #ececea; + border-radius: 3px; + background-color: #ececea; + } + + [role="toolbar"] [role="spinbutton"] .increase:hover, + [role="toolbar"] [role="spinbutton"] .decrease:hover, + [role="toolbar"] [role="spinbutton"]:focus .increase, + [role="toolbar"] [role="spinbutton"]:focus .decrease { + fill: #005a9c; + border-color: #005a9c; + } + + textarea { + width: 990px; + padding: 0.25em; + border: 2px solid black; + height: 400px; + font-size: 14pt; + font-family: sans-serif; + border-radius: 5px; + } + [role="toolbar"] .menu-popup { + position: relative; + } + + [role="toolbar"] .menu-popup [role="menu"] { + padding: 0; + width: 9.5em; + border: 2px solid #ddd; + border-radius: 5px; + background-color: white; + display: none; + position: absolute; + margin: 0; + z-index: 1; + } + + [role="toolbar"] .menu-popup [role="menu"].focus { + border-color: #005a9c; + } + + [role="toolbar"] .menu-popup [role="menu"] li { + padding: 0; + margin: 0; + display: block; + text-align: left; + list-style: none; + } + + [role="toolbar"] .menu-popup [role="menu"] [role="menuitemradio"] { + padding: 1px 2px; + outline: 0; + border: none; + border-radius: 0; + } + + [role="toolbar"] .menu-popup [role="menu"] [role="menuitemradio"]::before { + content: url("../images/menuitemradio-unchecked.svg"); + } + + [role="toolbar"] + .menu-popup + [role="menu"] + [role="menuitemradio"][aria-checked="true"]::before { + content: url("../images/menuitemradio-checked.svg"); + } + + [role="toolbar"] .menu-popup [role="menu"] [role="menuitemradio"]:focus, + [role="toolbar"] .menu-popup [role="menu"] [role="menuitemradio"]:hover { + background: rgb(226 239 255); + border-top: 1px solid #005a9c; + border-bottom: 1px solid #005a9c; + padding-top: 0; + padding-bottom: 0; + } + + [role="toolbar"] .menu-popup [role="menu"] [role="menuitemradio"]:focus { + border-color: #005a9c; + } + \ No newline at end of file diff --git a/lib/widgets/disclosure.js b/lib/widgets/disclosure.js new file mode 100644 index 0000000..5621777 --- /dev/null +++ b/lib/widgets/disclosure.js @@ -0,0 +1,87 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: disclosure-button.js + * + * Desc: Disclosure button widget that implements ARIA Authoring Best Practices + */ + +'use strict'; + +/* + * @constructorDisclosureButton + * + * + */ +class DisclosureButton { + constructor(buttonNode) { + this.buttonNode = buttonNode; + this.controlledNode = false; + + var id = this.buttonNode.getAttribute('aria-controls'); + + if (id) { + this.controlledNode = document.getElementById(id); + } + + this.buttonNode.setAttribute('aria-expanded', 'false'); + this.hideContent(); + + this.buttonNode.addEventListener('click', this.onClick.bind(this)); + this.buttonNode.addEventListener('focus', this.onFocus.bind(this)); + this.buttonNode.addEventListener('blur', this.onBlur.bind(this)); + } + + showContent() { + if (this.controlledNode) { + this.controlledNode.style.display = 'block'; + } + } + + hideContent() { + if (this.controlledNode) { + this.controlledNode.style.display = 'none'; + } + } + + toggleExpand() { + if (this.buttonNode.getAttribute('aria-expanded') === 'true') { + this.buttonNode.setAttribute('aria-expanded', 'false'); + this.hideContent(); + } else { + this.buttonNode.setAttribute('aria-expanded', 'true'); + this.showContent(); + } + } + + /* EVENT HANDLERS */ + + onClick() { + this.toggleExpand(); + } + + onFocus() { + this.buttonNode.classList.add('focus'); + } + + onBlur() { + this.buttonNode.classList.remove('focus'); + } +} + +/* Initialize Hide/Show Buttons */ + +window.addEventListener( + 'load', + function () { + var buttons = document.querySelectorAll( + 'button[aria-expanded][aria-controls]' + ); + + for (var i = 0; i < buttons.length; i++) { + new DisclosureButton(buttons[i]); + } + }, + false +); diff --git a/lib/widgets/menu_button.js b/lib/widgets/menu_button.js new file mode 100644 index 0000000..a116515 --- /dev/null +++ b/lib/widgets/menu_button.js @@ -0,0 +1,349 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: menu-button-actives-active-descendant.js + * + * Desc: Creates a menu button that opens a menu of actions using aria-activedescendants + */ + +'use strict'; + +class MenuButtonActionsActiveDescendant { + constructor(domNode, performMenuAction) { + this.domNode = domNode; + this.performMenuAction = performMenuAction; + this.buttonNode = domNode.querySelector('button'); + this.menuNode = domNode.querySelector('[role="menu"]'); + this.currentMenuitem = {}; + this.menuitemNodes = []; + this.firstMenuitem = false; + this.lastMenuitem = false; + this.firstChars = []; + + this.buttonNode.addEventListener( + 'keydown', + this.onButtonKeydown.bind(this) + ); + + this.buttonNode.addEventListener('click', this.onButtonClick.bind(this)); + + this.menuNode.addEventListener('keydown', this.onMenuKeydown.bind(this)); + + var nodes = domNode.querySelectorAll('[role="menuitem"]'); + + for (var i = 0; i < nodes.length; i++) { + var menuitem = nodes[i]; + this.menuitemNodes.push(menuitem); + menuitem.tabIndex = -1; + this.firstChars.push(menuitem.textContent.trim()[0].toLowerCase()); + + menuitem.addEventListener('click', this.onMenuitemClick.bind(this)); + + menuitem.addEventListener( + 'mouseover', + this.onMenuitemMouseover.bind(this) + ); + + if (!this.firstMenuitem) { + this.firstMenuitem = menuitem; + } + this.lastMenuitem = menuitem; + } + + domNode.addEventListener('focusin', this.onFocusin.bind(this)); + domNode.addEventListener('focusout', this.onFocusout.bind(this)); + + window.addEventListener( + 'mousedown', + this.onBackgroundMousedown.bind(this), + true + ); + } + + setFocusToMenuitem(newMenuitem) { + for (var i = 0; i < this.menuitemNodes.length; i++) { + var menuitem = this.menuitemNodes[i]; + if (menuitem === newMenuitem) { + this.currentMenuitem = newMenuitem; + menuitem.classList.add('focus'); + this.menuNode.setAttribute('aria-activedescendant', newMenuitem.id); + } else { + menuitem.classList.remove('focus'); + } + } + } + + setFocusToFirstMenuitem() { + this.setFocusToMenuitem(this.firstMenuitem); + } + + setFocusToLastMenuitem() { + this.setFocusToMenuitem(this.lastMenuitem); + } + + setFocusToPreviousMenuitem() { + var newMenuitem, index; + + if (this.currentMenuitem === this.firstMenuitem) { + newMenuitem = this.lastMenuitem; + } else { + index = this.menuitemNodes.indexOf(this.currentMenuitem); + newMenuitem = this.menuitemNodes[index - 1]; + } + + this.setFocusToMenuitem(newMenuitem); + + return newMenuitem; + } + + setFocusToNextMenuitem() { + var newMenuitem, index; + + if (this.currentMenuitem === this.lastMenuitem) { + newMenuitem = this.firstMenuitem; + } else { + index = this.menuitemNodes.indexOf(this.currentMenuitem); + newMenuitem = this.menuitemNodes[index + 1]; + } + this.setFocusToMenuitem(newMenuitem); + + return newMenuitem; + } + + setFocusByFirstCharacter(char) { + var start, index; + + if (char.length > 1) { + return; + } + + char = char.toLowerCase(); + + // Get start index for search based on position of currentItem + start = this.menuitemNodes.indexOf(this.currentMenuitem) + 1; + if (start >= this.menuitemNodes.length) { + start = 0; + } + + // Check remaining slots in the menu + index = this.firstChars.indexOf(char, start); + + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.firstChars.indexOf(char, 0); + } + + // If match was found... + if (index > -1) { + this.setFocusToMenuitem(this.menuitemNodes[index]); + } + } + + // Utilities + + getIndexFirstChars(startIndex, char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (char === this.firstChars[i]) { + return i; + } + } + return -1; + } + + // Popup menu methods + + openPopup() { + this.menuNode.style.display = 'block'; + this.buttonNode.setAttribute('aria-expanded', 'true'); + this.menuNode.focus(); + this.setFocusToFirstMenuitem(); + } + + closePopup() { + if (this.isOpen()) { + this.buttonNode.setAttribute('aria-expanded', 'false'); + this.menuNode.setAttribute('aria-activedescendant', ''); + for (let i = 0; i < this.menuitemNodes.length; i++) { + this.menuitemNodes[i].classList.remove('focus'); + } + this.menuNode.style.display = 'none'; + this.buttonNode.focus(); + } + } + + isOpen() { + return this.buttonNode.getAttribute('aria-expanded') === 'true'; + } + + // Menu event handlers + + onFocusin() { + this.domNode.classList.add('focus'); + } + + onFocusout() { + this.domNode.classList.remove('focus'); + } + + onButtonKeydown(event) { + var key = event.key, + flag = false; + + switch (key) { + case ' ': + case 'Enter': + case 'ArrowDown': + case 'Down': + this.openPopup(); + this.setFocusToFirstMenuitem(); + flag = true; + break; + + case 'Esc': + case 'Escape': + this.closePopup(); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.openPopup(); + this.setFocusToLastMenuitem(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onButtonClick(event) { + if (this.isOpen()) { + this.closePopup(); + } else { + this.openPopup(); + } + event.stopPropagation(); + event.preventDefault(); + } + + onMenuKeydown(event) { + var key = event.key, + flag = false; + + function isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } + + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + + if (event.shiftKey) { + if (isPrintableCharacter(key)) { + this.setFocusByFirstCharacter(key); + flag = true; + } + + if (event.key === 'Tab') { + this.closePopup(); + flag = true; + } + } else { + switch (key) { + case ' ': + case 'Enter': + this.closePopup(); + this.performMenuAction(this.currentMenuitem); + flag = true; + break; + + case 'Esc': + case 'Escape': + this.closePopup(); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + this.setFocusToPreviousMenuitem(); + flag = true; + break; + + case 'ArrowDown': + case 'Down': + this.setFocusToNextMenuitem(); + flag = true; + break; + + case 'Home': + case 'PageUp': + this.setFocusToFirstMenuitem(); + flag = true; + break; + + case 'End': + case 'PageDown': + this.setFocusToLastMenuitem(); + flag = true; + break; + + case 'Tab': + this.closePopup(); + break; + + default: + if (isPrintableCharacter(key)) { + this.setFocusByFirstCharacter(key); + flag = true; + } + break; + } + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onMenuitemMouseover(event) { + var tgt = event.currentTarget; + this.setFocusToMenuitem(tgt); + } + + onMenuitemClick(event) { + var tgt = event.currentTarget; + this.closePopup(); + this.performMenuAction(tgt); + } + + onBackgroundMousedown(event) { + if (!this.domNode.contains(event.target)) { + if (this.isOpen()) { + this.closePopup(); + } + } + } +} + +// Initialize menu buttons + +window.addEventListener('load', function () { + document.getElementById('action_output').value = 'none'; + + function performMenuAction(node) { + document.getElementById('action_output').value = node.textContent.trim(); + } + + var menuButtons = document.querySelectorAll('.menu-button-actions'); + for (var i = 0; i < menuButtons.length; i++) { + new MenuButtonActionsActiveDescendant(menuButtons[i], performMenuAction); + } +}); diff --git a/lib/widgets/meter.js b/lib/widgets/meter.js new file mode 100644 index 0000000..0370157 --- /dev/null +++ b/lib/widgets/meter.js @@ -0,0 +1,115 @@ +'use strict'; +var Meter = function (element) { + this.rootEl = element; + this.fillEl = element.querySelector('.fill'); + + // set up min, max, and current value + var min = element.getAttribute('aria-valuemin'); + var max = element.getAttribute('aria-valuemax'); + var value = element.getAttribute('aria-valuenow'); + this._update(parseFloat(min), parseFloat(max), parseFloat(value)); +}; + +/* Private methods */ + +// returns a number representing a percentage between 0 - 100 +Meter.prototype._calculatePercentFill = function (min, max, value) { + if ( + typeof min !== 'number' || + typeof max !== 'number' || + typeof value !== 'number' + ) { + return 0; + } + + return (100 * (value - min)) / (max - min); +}; + +// returns an hsl color string between red and green +Meter.prototype._getColorValue = function (percent) { + // red is 0deg, green is 120deg in hsl + // if percent is 100, hue should be red, and if percent is 0, hue should be green + var hue = (percent / 100) * (0 - 120) + 120; + + return 'hsl(' + hue + ', 100%, 40%)'; +}; + +// no return value; updates the meter element +Meter.prototype._update = function (min, max, value) { + // update fill width + if (min !== this.min || max !== this.max || value !== this.value) { + var percentFill = this._calculatePercentFill(min, max, value); + this.fillEl.style.width = percentFill + '%'; + this.fillEl.style.color = this._getColorValue(percentFill); + } + + // update aria attributes + if (min !== this.min) { + this.min = min; + this.rootEl.setAttribute('aria-valuemin', min + ''); + } + + if (max !== this.max) { + this.max = max; + this.rootEl.setAttribute('aria-valuemax', max + ''); + } + + if (value !== this.value) { + this.value = value; + this.rootEl.setAttribute('aria-valuenow', value + ''); + } +}; + +/* Public methods */ + +// no return value; modifies the meter element based on a new value +Meter.prototype.setValue = function (value) { + if (typeof value !== 'number') { + value = parseFloat(value); + } + + if (!isNaN(value)) { + this._update(this.min, this.max, value); + } +}; + +/* Code for example page */ + +window.addEventListener('load', function () { + // init meters + var meterEls = document.querySelectorAll('[role=meter]'); + var meters = []; + Array.prototype.slice.call(meterEls).forEach(function (meterEl) { + meters.push(new Meter(meterEl)); + }); + + // randomly update meter values + + // returns an id for setInterval + function playMeters() { + return window.setInterval(function () { + meters.forEach(function (meter) { + meter.setValue(Math.random() * 100); + }); + }, 5000); + } + + // start meters + var updateInterval = playMeters(); + + // play/pause meter updates + var playButton = document.querySelector('.play-meters'); + playButton.addEventListener('click', function () { + var isPaused = playButton.classList.contains('paused'); + + if (isPaused) { + updateInterval = playMeters(); + playButton.classList.remove('paused'); + playButton.innerHTML = 'Pause Updates'; + } else { + clearInterval(updateInterval); + playButton.classList.add('paused'); + playButton.innerHTML = 'Start Updates'; + } + }); +}); diff --git a/lib/widgets/navigation_menu.js b/lib/widgets/navigation_menu.js new file mode 100644 index 0000000..6f42fea --- /dev/null +++ b/lib/widgets/navigation_menu.js @@ -0,0 +1,739 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: menubar-navigation.js + * + * Desc: Creates a menubar of hierarchical set of links + */ + +'use strict'; + +class NavigationContentGenerator { + constructor(siteURL, siteName) { + this.siteName = siteName; + this.siteURL = siteURL; + this.fillerTextSentences = []; + + this.fillerTextSentences.push( + 'The content on this page is associated with the $linkName link for $siteName.' + ); + } + + renderParagraph(linkURL, linkName) { + var content = ''; + this.fillerTextSentences.forEach( + (s) => + (content += s + .replace('$siteName', this.siteName) + .replace('$siteURL', this.siteURL) + .replace('$linkName', linkName) + .replace('$linkURL', linkURL)) + ); + return content; + } +} + +class MenubarNavigation { + constructor(domNode) { + var linkURL, linkTitle; + + this.domNode = domNode; + + this.menuitems = []; + this.popups = []; + this.menuitemGroups = {}; + this.menuOrientation = {}; + this.isPopup = {}; + this.isPopout = {}; + this.openPopups = false; + + this.firstChars = {}; // see Menubar init method + this.firstMenuitem = {}; // see Menubar init method + this.lastMenuitem = {}; // see Menubar init method + + this.initMenu(domNode, 0); + + domNode.addEventListener('focusin', this.onMenubarFocusin.bind(this)); + domNode.addEventListener('focusout', this.onMenubarFocusout.bind(this)); + + window.addEventListener( + 'pointerdown', + this.onBackgroundPointerdown.bind(this), + true + ); + + domNode.querySelector('[role=menuitem]').tabIndex = 0; + + // Initial content for page + if (location.href.split('#').length > 1) { + linkURL = location.href; + linkTitle = getLinkNameFromURL(location.href); + } else { + linkURL = location.href + '#home'; + linkTitle = 'Home'; + } + + this.contentGenerator = new NavigationContentGenerator( + '#home', + 'Mythical University' + ); + this.updateContent(linkURL, linkTitle, false); + + function getLinkNameFromURL(url) { + function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + var name = url.split('#')[1]; + if (typeof name === 'string') { + name = name.split('-').map(capitalize).join(' '); + } else { + name = 'Home'; + } + return name; + } + } + + getParentMenuitem(menuitem) { + var node = menuitem.parentNode; + if (node) { + node = node.parentNode; + if (node) { + node = node.previousElementSibling; + if (node) { + if (node.getAttribute('role') === 'menuitem') { + return node; + } + } + } + } + return false; + } + + updateContent(linkURL, linkName, moveFocus) { + var h1Node, paraNodes, pathNode; + + if (typeof moveFocus !== 'boolean') { + moveFocus = true; + } + + // Update content area + h1Node = document.querySelector('.page .main h1'); + if (h1Node) { + h1Node.textContent = linkName; + h1Node.tabIndex = -1; + if (moveFocus) { + h1Node.focus(); + } + } + paraNodes = document.querySelectorAll('.page .main p'); + paraNodes.forEach( + (p) => + (p.innerHTML = this.contentGenerator.renderParagraph(linkURL, linkName)) + ); + + // Update aria-current + this.menuitems.forEach((item) => { + item.removeAttribute('aria-current'); + item.classList.remove('aria-current-path'); + item.title = ''; + }); + + this.menuitems.forEach((item) => { + if (item.href === linkURL) { + item.setAttribute('aria-current', 'page'); + pathNode = this.getParentMenuitem(item); + while (pathNode) { + pathNode.classList.add('aria-current-path'); + pathNode.title = 'Contains current page link'; + pathNode = this.getParentMenuitem(pathNode); + } + } + }); + } + + getMenuitems(domNode, depth) { + var nodes = []; + + var initMenu = this.initMenu.bind(this); + var popups = this.popups; + + function findMenuitems(node) { + var role, flag; + + while (node) { + flag = true; + role = node.getAttribute('role'); + + if (role) { + role = role.trim().toLowerCase(); + } + + switch (role) { + case 'menu': + node.tabIndex = -1; + initMenu(node, depth + 1); + flag = false; + break; + + case 'menuitem': + if (node.getAttribute('aria-haspopup') === 'true') { + popups.push(node); + } + nodes.push(node); + break; + + default: + break; + } + + if ( + flag && + node.firstElementChild && + node.firstElementChild.tagName !== 'svg' + ) { + findMenuitems(node.firstElementChild); + } + node = node.nextElementSibling; + } + } + findMenuitems(domNode.firstElementChild); + return nodes; + } + + initMenu(menu, depth) { + var menuitems, menuitem, role; + + var menuId = this.getMenuId(menu); + + menuitems = this.getMenuitems(menu, depth); + this.menuOrientation[menuId] = this.getMenuOrientation(menu); + + this.isPopup[menuId] = menu.getAttribute('role') === 'menu' && depth === 1; + this.isPopout[menuId] = menu.getAttribute('role') === 'menu' && depth > 1; + + this.menuitemGroups[menuId] = []; + this.firstChars[menuId] = []; + this.firstMenuitem[menuId] = null; + this.lastMenuitem[menuId] = null; + + for (var i = 0; i < menuitems.length; i++) { + menuitem = menuitems[i]; + role = menuitem.getAttribute('role'); + + if (role.indexOf('menuitem') < 0) { + continue; + } + + menuitem.tabIndex = -1; + this.menuitems.push(menuitem); + this.menuitemGroups[menuId].push(menuitem); + this.firstChars[menuId].push( + menuitem.textContent.trim().toLowerCase()[0] + ); + + menuitem.addEventListener('keydown', this.onKeydown.bind(this)); + menuitem.addEventListener('click', this.onMenuitemClick.bind(this), { + capture: true, + }); + + menuitem.addEventListener( + 'pointerover', + this.onMenuitemPointerover.bind(this) + ); + + if (!this.firstMenuitem[menuId]) { + if (this.hasPopup(menuitem)) { + menuitem.tabIndex = 0; + } + this.firstMenuitem[menuId] = menuitem; + } + this.lastMenuitem[menuId] = menuitem; + } + } + + setFocusToMenuitem(menuId, newMenuitem) { + this.closePopupAll(newMenuitem); + + if (this.menuitemGroups[menuId]) { + this.menuitemGroups[menuId].forEach(function (item) { + if (item === newMenuitem) { + item.tabIndex = 0; + newMenuitem.focus(); + } else { + item.tabIndex = -1; + } + }); + } + } + + setFocusToFirstMenuitem(menuId) { + this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]); + } + + setFocusToLastMenuitem(menuId) { + this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]); + } + + setFocusToPreviousMenuitem(menuId, currentMenuitem) { + var newMenuitem, index; + + if (currentMenuitem === this.firstMenuitem[menuId]) { + newMenuitem = this.lastMenuitem[menuId]; + } else { + index = this.menuitemGroups[menuId].indexOf(currentMenuitem); + newMenuitem = this.menuitemGroups[menuId][index - 1]; + } + + this.setFocusToMenuitem(menuId, newMenuitem); + + return newMenuitem; + } + + setFocusToNextMenuitem(menuId, currentMenuitem) { + var newMenuitem, index; + + if (currentMenuitem === this.lastMenuitem[menuId]) { + newMenuitem = this.firstMenuitem[menuId]; + } else { + index = this.menuitemGroups[menuId].indexOf(currentMenuitem); + newMenuitem = this.menuitemGroups[menuId][index + 1]; + } + this.setFocusToMenuitem(menuId, newMenuitem); + + return newMenuitem; + } + + setFocusByFirstCharacter(menuId, currentMenuitem, char) { + var start, index; + + char = char.toLowerCase(); + + // Get start index for search based on position of currentItem + start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1; + if (start >= this.menuitemGroups[menuId].length) { + start = 0; + } + + // Check remaining slots in the menu + index = this.getIndexFirstChars(menuId, start, char); + + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.getIndexFirstChars(menuId, 0, char); + } + + // If match was found... + if (index > -1) { + this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]); + } + } + + // Utilities + + getIndexFirstChars(menuId, startIndex, char) { + for (var i = startIndex; i < this.firstChars[menuId].length; i++) { + if (char === this.firstChars[menuId][i]) { + return i; + } + } + return -1; + } + + isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } + + getIdFromAriaLabel(node) { + var id = node.getAttribute('aria-label'); + if (id) { + id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-'); + } + return id; + } + + getMenuOrientation(node) { + var orientation = node.getAttribute('aria-orientation'); + + if (!orientation) { + var role = node.getAttribute('role'); + + switch (role) { + case 'menubar': + orientation = 'horizontal'; + break; + + case 'menu': + orientation = 'vertical'; + break; + + default: + break; + } + } + + return orientation; + } + + getMenuId(node) { + var id = false; + var role = node.getAttribute('role'); + + while (node && role !== 'menu' && role !== 'menubar') { + node = node.parentNode; + if (node) { + role = node.getAttribute('role'); + } + } + + if (node) { + id = role + '-' + this.getIdFromAriaLabel(node); + } + + return id; + } + + getMenu(menuitem) { + var menu = menuitem; + var role = menuitem.getAttribute('role'); + + while (menu && role !== 'menu' && role !== 'menubar') { + menu = menu.parentNode; + if (menu) { + role = menu.getAttribute('role'); + } + } + + return menu; + } + + // Popup menu methods + + isAnyPopupOpen() { + for (var i = 0; i < this.popups.length; i++) { + if (this.popups[i].getAttribute('aria-expanded') === 'true') { + return true; + } + } + return false; + } + + setMenubarDataExpanded(value) { + this.domNode.setAttribute('data-menubar-item-expanded', value); + } + + isMenubarDataExpandedTrue() { + return this.domNode.getAttribute('data-menubar-item-expanded') === 'true'; + } + + openPopup(menuId, menuitem) { + // set aria-expanded attribute + var popupMenu = menuitem.nextElementSibling; + + if (popupMenu) { + var rect = menuitem.getBoundingClientRect(); + + // Set CSS properties + if (this.isPopup[menuId]) { + popupMenu.parentNode.style.position = 'relative'; + popupMenu.style.display = 'block'; + popupMenu.style.position = 'absolute'; + popupMenu.style.left = rect.width + 10 + 'px'; + popupMenu.style.top = '0px'; + popupMenu.style.zIndex = 100; + } else { + popupMenu.style.display = 'block'; + popupMenu.style.position = 'absolute'; + popupMenu.style.left = '0px'; + popupMenu.style.top = rect.height + 8 + 'px'; + popupMenu.style.zIndex = 100; + } + + menuitem.setAttribute('aria-expanded', 'true'); + this.setMenubarDataExpanded('true'); + return this.getMenuId(popupMenu); + } + + return false; + } + + closePopout(menuitem) { + var menu, + menuId = this.getMenuId(menuitem), + cmi = menuitem; + + while (this.isPopup[menuId] || this.isPopout[menuId]) { + menu = this.getMenu(cmi); + cmi = menu.previousElementSibling; + menuId = this.getMenuId(cmi); + menu.style.display = 'none'; + } + cmi.focus(); + return cmi; + } + + closePopup(menuitem) { + var menu, + menuId = this.getMenuId(menuitem), + cmi = menuitem; + + if (this.isMenubar(menuId)) { + if (this.isOpen(menuitem)) { + menuitem.setAttribute('aria-expanded', 'false'); + menuitem.nextElementSibling.style.display = 'none'; + } + } else { + menu = this.getMenu(menuitem); + cmi = menu.previousElementSibling; + cmi.setAttribute('aria-expanded', 'false'); + cmi.focus(); + menu.style.display = 'none'; + } + + return cmi; + } + + doesNotContain(popup, menuitem) { + if (menuitem) { + return !popup.nextElementSibling.contains(menuitem); + } + return true; + } + + closePopupAll(menuitem) { + if (typeof menuitem !== 'object') { + menuitem = false; + } + for (var i = 0; i < this.popups.length; i++) { + var popup = this.popups[i]; + if (this.doesNotContain(popup, menuitem) && this.isOpen(popup)) { + var cmi = popup.nextElementSibling; + if (cmi) { + popup.setAttribute('aria-expanded', 'false'); + cmi.style.display = 'none'; + } + } + } + } + + hasPopup(menuitem) { + return menuitem.getAttribute('aria-haspopup') === 'true'; + } + + isOpen(menuitem) { + return menuitem.getAttribute('aria-expanded') === 'true'; + } + + isMenubar(menuId) { + return !this.isPopup[menuId] && !this.isPopout[menuId]; + } + + isMenuHorizontal(menuitem) { + return this.menuOrientation[menuitem] === 'horizontal'; + } + + hasFocus() { + return this.domNode.classList.contains('focus'); + } + + // Menu event handlers + + onMenubarFocusin() { + // if the menubar or any of its menus has focus, add styling hook for hover + this.domNode.classList.add('focus'); + } + + onMenubarFocusout() { + // remove styling hook for hover on menubar item + this.domNode.classList.remove('focus'); + } + + onKeydown(event) { + var tgt = event.currentTarget, + key = event.key, + flag = false, + menuId = this.getMenuId(tgt), + id, + popupMenuId, + mi; + + switch (key) { + case ' ': + case 'Enter': + if (this.hasPopup(tgt)) { + this.openPopups = true; + popupMenuId = this.openPopup(menuId, tgt); + this.setFocusToFirstMenuitem(popupMenuId); + } else { + if (tgt.href !== '#') { + this.closePopupAll(); + this.updateContent(tgt.href, tgt.textContent.trim()); + this.setMenubarDataExpanded('false'); + } + } + flag = true; + break; + + case 'Esc': + case 'Escape': + this.openPopups = false; + mi = this.closePopup(tgt); + id = this.getMenuId(mi); + this.setMenubarDataExpanded('false'); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + if (this.isMenuHorizontal(menuId)) { + if (this.hasPopup(tgt)) { + this.openPopups = true; + popupMenuId = this.openPopup(menuId, tgt); + this.setFocusToLastMenuitem(popupMenuId); + } + } else { + this.setFocusToPreviousMenuitem(menuId, tgt); + } + flag = true; + break; + + case 'ArrowDown': + case 'Down': + if (this.isMenuHorizontal(menuId)) { + if (this.hasPopup(tgt)) { + this.openPopups = true; + popupMenuId = this.openPopup(menuId, tgt); + this.setFocusToFirstMenuitem(popupMenuId); + } + } else { + this.setFocusToNextMenuitem(menuId, tgt); + } + flag = true; + break; + + case 'Left': + case 'ArrowLeft': + if (this.isMenuHorizontal(menuId)) { + mi = this.setFocusToPreviousMenuitem(menuId, tgt); + if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) { + this.openPopup(menuId, mi); + } + } else { + if (this.isPopout[menuId]) { + mi = this.closePopup(tgt); + id = this.getMenuId(mi); + mi = this.setFocusToMenuitem(id, mi); + } else { + mi = this.closePopup(tgt); + id = this.getMenuId(mi); + mi = this.setFocusToPreviousMenuitem(id, mi); + this.openPopup(id, mi); + } + } + flag = true; + break; + + case 'Right': + case 'ArrowRight': + if (this.isMenuHorizontal(menuId)) { + mi = this.setFocusToNextMenuitem(menuId, tgt); + if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) { + this.openPopup(menuId, mi); + } + } else { + if (this.hasPopup(tgt)) { + popupMenuId = this.openPopup(menuId, tgt); + this.setFocusToFirstMenuitem(popupMenuId); + } else { + mi = this.closePopout(tgt); + id = this.getMenuId(mi); + mi = this.setFocusToNextMenuitem(id, mi); + this.openPopup(id, mi); + } + } + flag = true; + break; + + case 'Home': + case 'PageUp': + this.setFocusToFirstMenuitem(menuId, tgt); + flag = true; + break; + + case 'End': + case 'PageDown': + this.setFocusToLastMenuitem(menuId, tgt); + flag = true; + break; + + case 'Tab': + this.openPopups = false; + this.setMenubarDataExpanded('false'); + this.closePopup(tgt); + break; + + default: + if (this.isPrintableCharacter(key)) { + this.setFocusByFirstCharacter(menuId, tgt, key); + flag = true; + } + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onMenuitemClick(event) { + var tgt = event.currentTarget; + var menuId = this.getMenuId(tgt); + + if (this.hasPopup(tgt)) { + if (this.isOpen(tgt)) { + this.closePopup(tgt); + } else { + this.closePopupAll(tgt); + this.openPopup(menuId, tgt); + } + } else { + this.updateContent(tgt.href, tgt.textContent.trim()); + this.closePopupAll(); + } + event.stopPropagation(); + event.preventDefault(); + } + + onMenuitemPointerover(event) { + var tgt = event.currentTarget; + var menuId = this.getMenuId(tgt); + + if (this.hasFocus()) { + this.setFocusToMenuitem(menuId, tgt); + } + + if (this.isAnyPopupOpen() || this.hasFocus()) { + this.closePopupAll(tgt); + if (this.hasPopup(tgt)) { + this.openPopup(menuId, tgt); + } + } + } + + onBackgroundPointerdown(event) { + if (!this.domNode.contains(event.target)) { + this.closePopupAll(); + } + } +} + +// Initialize menubar editor + +window.addEventListener('load', function () { + var menubarNavs = document.querySelectorAll('.menubar-navigation'); + for (var i = 0; i < menubarNavs.length; i++) { + new MenubarNavigation(menubarNavs[i]); + } +}); diff --git a/lib/widgets/radio_group,js b/lib/widgets/radio_group,js new file mode 100644 index 0000000..6a29f0e --- /dev/null +++ b/lib/widgets/radio_group,js @@ -0,0 +1,177 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: radio-activedescendant.js + * + * Desc: Radio group widget using aria-activedescendant that implements ARIA Authoring Practices + */ + +'use strict'; + +class RadioGroupActiveDescendant { + constructor(groupNode) { + this.groupNode = groupNode; + + this.radioButtons = []; + + this.firstRadioButton = null; + this.lastRadioButton = null; + + this.groupNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.groupNode.addEventListener('focus', this.handleFocus.bind(this)); + this.groupNode.addEventListener('blur', this.handleBlur.bind(this)); + + // initialize + if (!this.groupNode.getAttribute('role')) { + this.groupNode.setAttribute('role', 'radiogroup'); + } + + var rbs = this.groupNode.querySelectorAll('[role=radio]'); + + for (var i = 0; i < rbs.length; i++) { + var rb = rbs[i]; + rb.addEventListener('click', this.handleClick.bind(this)); + this.radioButtons.push(rb); + if (!this.firstRadioButton) { + this.firstRadioButton = rb; + } + this.lastRadioButton = rb; + } + this.groupNode.tabIndex = 0; + } + + isRadioInView(radio) { + var bounding = radio.getBoundingClientRect(); + return ( + bounding.top >= 0 && + bounding.left >= 0 && + bounding.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + bounding.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); + } + + setChecked(currentItem) { + for (var i = 0; i < this.radioButtons.length; i++) { + var rb = this.radioButtons[i]; + rb.setAttribute('aria-checked', 'false'); + rb.classList.remove('focus'); + } + currentItem.setAttribute('aria-checked', 'true'); + currentItem.classList.add('focus'); + this.groupNode.setAttribute('aria-activedescendant', currentItem.id); + if (!this.isRadioInView(currentItem)) { + currentItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + this.groupNode.focus(); + } + + setCheckedToPreviousItem(currentItem) { + var index; + + if (currentItem === this.firstRadioButton) { + this.setChecked(this.lastRadioButton); + } else { + index = this.radioButtons.indexOf(currentItem); + this.setChecked(this.radioButtons[index - 1]); + } + } + + setCheckedToNextItem(currentItem) { + var index; + + if (currentItem === this.lastRadioButton) { + this.setChecked(this.firstRadioButton); + } else { + index = this.radioButtons.indexOf(currentItem); + this.setChecked(this.radioButtons[index + 1]); + } + } + + getCurrentRadioButton() { + var id = this.groupNode.getAttribute('aria-activedescendant'); + if (!id) { + this.groupNode.setAttribute( + 'aria-activedescendant', + this.firstRadioButton.id + ); + return this.firstRadioButton; + } + for (var i = 0; i < this.radioButtons.length; i++) { + var rb = this.radioButtons[i]; + if (rb.id === id) { + return rb; + } + } + this.groupNode.setAttribute( + 'aria-activedescendant', + this.firstRadioButton.id + ); + return this.firstRadioButton; + } + + // Event Handlers + + handleKeydown(event) { + var flag = false; + + var currentItem = this.getCurrentRadioButton(); + switch (event.key) { + case ' ': + this.setChecked(currentItem); + flag = true; + break; + + case 'Up': + case 'ArrowUp': + case 'Left': + case 'ArrowLeft': + this.setCheckedToPreviousItem(currentItem); + flag = true; + break; + + case 'Down': + case 'ArrowDown': + case 'Right': + case 'ArrowRight': + this.setCheckedToNextItem(currentItem); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + handleClick(event) { + this.setChecked(event.currentTarget); + } + + handleFocus() { + var currentItem = this.getCurrentRadioButton(); + if (!this.isRadioInView(currentItem)) { + currentItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + currentItem.classList.add('focus'); + } + + handleBlur() { + var currentItem = this.getCurrentRadioButton(); + currentItem.classList.remove('focus'); + } +} + +// Initialize radio button group using aria-activedescendant +window.addEventListener('load', function () { + var radios = document.querySelectorAll('.radiogroup-activedescendant'); + for (var i = 0; i < radios.length; i++) { + new RadioGroupActiveDescendant(radios[i]); + } +}); diff --git a/lib/widgets/switch.js b/lib/widgets/switch.js new file mode 100644 index 0000000..7c6a88b --- /dev/null +++ b/lib/widgets/switch.js @@ -0,0 +1,34 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: switch-checkbox.js + * + * Desc: Switch widget using input[type=checkbox] that implements ARIA Authoring Practices + */ + +'use strict'; + +class CheckboxSwitch { + constructor(domNode) { + this.switchNode = domNode; + this.switchNode.addEventListener('focus', () => this.onFocus(event)); + this.switchNode.addEventListener('blur', () => this.onBlur(event)); + } + + onFocus(event) { + event.currentTarget.parentNode.classList.add('focus'); + } + + onBlur(event) { + event.currentTarget.parentNode.classList.remove('focus'); + } +} + +// Initialize switches +window.addEventListener('load', function () { + // Initialize the Switch component on all matching DOM nodes + Array.from( + document.querySelectorAll('input[type=checkbox][role^=switch]') + ).forEach((element) => new CheckboxSwitch(element)); +}); diff --git a/lib/widgets/tabs.js b/lib/widgets/tabs.js new file mode 100644 index 0000000..0c82866 --- /dev/null +++ b/lib/widgets/tabs.js @@ -0,0 +1,136 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: tabs-automatic.js + * + * Desc: Tablist widget that implements ARIA Authoring Practices + */ + +'use strict'; + +class TabsAutomatic { + constructor(groupNode) { + this.tablistNode = groupNode; + + this.tabs = []; + + this.firstTab = null; + this.lastTab = null; + + this.tabs = Array.from(this.tablistNode.querySelectorAll('[role=tab]')); + this.tabpanels = []; + + for (var i = 0; i < this.tabs.length; i += 1) { + var tab = this.tabs[i]; + var tabpanel = document.getElementById(tab.getAttribute('aria-controls')); + + tab.tabIndex = -1; + tab.setAttribute('aria-selected', 'false'); + this.tabpanels.push(tabpanel); + + tab.addEventListener('keydown', this.onKeydown.bind(this)); + tab.addEventListener('click', this.onClick.bind(this)); + + if (!this.firstTab) { + this.firstTab = tab; + } + this.lastTab = tab; + } + + this.setSelectedTab(this.firstTab, false); + } + + setSelectedTab(currentTab, setFocus) { + if (typeof setFocus !== 'boolean') { + setFocus = true; + } + for (var i = 0; i < this.tabs.length; i += 1) { + var tab = this.tabs[i]; + if (currentTab === tab) { + tab.setAttribute('aria-selected', 'true'); + tab.removeAttribute('tabindex'); + this.tabpanels[i].classList.remove('is-hidden'); + if (setFocus) { + tab.focus(); + } + } else { + tab.setAttribute('aria-selected', 'false'); + tab.tabIndex = -1; + this.tabpanels[i].classList.add('is-hidden'); + } + } + } + + setSelectedToPreviousTab(currentTab) { + var index; + + if (currentTab === this.firstTab) { + this.setSelectedTab(this.lastTab); + } else { + index = this.tabs.indexOf(currentTab); + this.setSelectedTab(this.tabs[index - 1]); + } + } + + setSelectedToNextTab(currentTab) { + var index; + + if (currentTab === this.lastTab) { + this.setSelectedTab(this.firstTab); + } else { + index = this.tabs.indexOf(currentTab); + this.setSelectedTab(this.tabs[index + 1]); + } + } + + /* EVENT HANDLERS */ + + onKeydown(event) { + var tgt = event.currentTarget, + flag = false; + + switch (event.key) { + case 'ArrowLeft': + this.setSelectedToPreviousTab(tgt); + flag = true; + break; + + case 'ArrowRight': + this.setSelectedToNextTab(tgt); + flag = true; + break; + + case 'Home': + this.setSelectedTab(this.firstTab); + flag = true; + break; + + case 'End': + this.setSelectedTab(this.lastTab); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + } + + onClick(event) { + this.setSelectedTab(event.currentTarget); + } +} + +// Initialize tablist + +window.addEventListener('load', function () { + var tablists = document.querySelectorAll('[role=tablist].automatic'); + for (var i = 0; i < tablists.length; i++) { + new TabsAutomatic(tablists[i]); + } +}); diff --git a/lib/widgets/toolbar.js b/lib/widgets/toolbar.js new file mode 100644 index 0000000..757bed4 --- /dev/null +++ b/lib/widgets/toolbar.js @@ -0,0 +1,1277 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: FormatToolbar.js + */ + +/* global FormatToolbarItem, FontMenuButton, SpinButton */ + +'use strict'; + +/** + * @class + * @description + * Format Toolbar object representing the state and interactions for a toolbar widget + * to format the text in a textarea element + * @param domNode + * The DOM node pointing to the element with the toolbar tole + */ +function FormatToolbar(domNode) { + this.domNode = domNode; + this.firstItem = null; + this.lastItem = null; + + this.toolbarItems = []; + this.alignItems = []; + this.textarea = null; + + this.copyButton = null; + this.cutButton = null; + + this.start = null; + this.end = null; + this.ourClipboard = ''; + this.selected = null; + + this.nightModeCheck = null; +} + +FormatToolbar.prototype.init = function () { + var i, items, toolbarItem, menuButton; + + this.textarea = document.getElementById( + this.domNode.getAttribute('aria-controls') + ); + this.textarea.style.width = + this.domNode.getBoundingClientRect().width - 12 + 'px'; + this.textarea.addEventListener('mouseup', this.selectTextContent.bind(this)); + this.textarea.addEventListener('keyup', this.selectTextContent.bind(this)); + this.domNode.addEventListener('click', this.handleContainerClick.bind(this)); + + this.selected = this.textarea.selectText; + + this.copyButton = this.domNode.querySelector('.copy'); + this.cutButton = this.domNode.querySelector('.cut'); + this.pasteButton = this.domNode.querySelector('.paste'); + + this.nightModeCheck = this.domNode.querySelector('.nightmode'); + items = this.domNode.querySelectorAll('.item'); + + for (i = 0; i < items.length; i++) { + toolbarItem = new FormatToolbarItem(items[i], this); + toolbarItem.init(); + + if (items[i].hasAttribute('aria-haspopup')) { + menuButton = new FontMenuButton(items[i], this, toolbarItem); + menuButton.init(); + } + + if (i === 0) { + this.firstItem = toolbarItem; + } + this.lastItem = toolbarItem; + this.toolbarItems.push(toolbarItem); + } + + var spinButtons = this.domNode.querySelectorAll('[role=spinbutton]'); + + for (i = 0; i < spinButtons.length; i++) { + var s = new SpinButton(spinButtons[i], this); + s.init(); + } +}; + +FormatToolbar.prototype.handleContainerClick = function () { + if (event.target !== this.domNode) return; + this.setFocusCurrentItem(); +}; + +FormatToolbar.prototype.setFocusCurrentItem = function () { + var item = this.domNode.querySelector('[tabindex="0"]'); + item.focus(); +}; + +FormatToolbar.prototype.selectTextContent = function () { + this.start = this.textarea.selectionStart; + this.end = this.textarea.selectionEnd; + this.selected = this.textarea.value.substring(this.start, this.end); + this.updateDisable( + this.copyButton, + this.cutButton, + this.pasteButton, + this.selected + ); +}; +FormatToolbar.prototype.updateDisable = function ( + copyButton, + cutButton, + pasteButton +) { + var start = this.textarea.selectionStart; + var end = this.textarea.selectionEnd; + if (start !== end) { + copyButton.setAttribute('aria-disabled', false); + cutButton.setAttribute('aria-disabled', false); + } else { + copyButton.setAttribute('aria-disabled', true); + cutButton.setAttribute('aria-disabled', true); + } + if (this.ourClipboard.length > 0) { + pasteButton.setAttribute('aria-disabled', false); + } +}; + +FormatToolbar.prototype.selectText = function (start, end, textarea) { + if (typeof textarea.selectionStart !== 'undefined') { + textarea.focus(); + textarea.selectionStart = start; + textarea.selectionEnd = end; + return true; + } +}; +FormatToolbar.prototype.copyTextContent = function () { + if (this.copyButton.getAttribute('aria-disabled') === 'true') { + return; + } + this.selectText(this.start, this.end, this.textarea); + this.ourClipboard = this.selected; + this.updateDisable( + this.copyButton, + this.cutButton, + this.pasteButton, + this.selected + ); +}; + +FormatToolbar.prototype.cutTextContent = function (toolbarItem) { + if (this.cutButton.getAttribute('aria-disabled') === 'true') { + return; + } + this.copyTextContent(toolbarItem); + var str = this.textarea.value; + this.textarea.value = str.replace(str.substring(this.start, this.end), ''); + this.selected = ''; + this.updateDisable( + this.copyButton, + this.cutButton, + this.pasteButton, + this.selected + ); +}; + +FormatToolbar.prototype.pasteTextContent = function () { + if (this.pasteButton.getAttribute('aria-disabled') === 'true') { + return; + } + var str = this.textarea.value; + this.textarea.value = + str.slice(0, this.textarea.selectionStart) + + this.ourClipboard + + str.slice(this.textarea.selectionEnd); + this.textarea.focus(); + this.updateDisable( + this.copyButton, + this.cutButton, + this.pasteButton, + this.selected + ); +}; + +FormatToolbar.prototype.toggleBold = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.fontWeight = 'normal'; + toolbarItem.resetPressed(); + } else { + this.textarea.style.fontWeight = 'bold'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.toggleUnderline = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.textDecoration = 'none'; + toolbarItem.resetPressed(); + } else { + this.textarea.style.textDecoration = 'underline'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.toggleItalic = function (toolbarItem) { + if (toolbarItem.isPressed()) { + this.textarea.style.fontStyle = 'normal'; + toolbarItem.resetPressed(); + } else { + this.textarea.style.fontStyle = 'italic'; + toolbarItem.setPressed(); + } +}; + +FormatToolbar.prototype.changeFontSize = function (value) { + this.textarea.style.fontSize = value + 'pt'; +}; + +FormatToolbar.prototype.toggleNightMode = function () { + if (this.nightModeCheck.checked) { + this.textarea.style.color = '#eee'; + this.textarea.style.background = 'black'; + } else { + this.textarea.style.color = 'black'; + this.textarea.style.background = 'white'; + } +}; + +FormatToolbar.prototype.redirectLink = function (toolbarItem) { + window.open(toolbarItem.domNode.href, '_blank'); +}; + +FormatToolbar.prototype.setAlignment = function (toolbarItem) { + for (var i = 0; i < this.alignItems.length; i++) { + this.alignItems[i].resetChecked(); + } + switch (toolbarItem.value) { + case 'left': + this.textarea.style.textAlign = 'left'; + toolbarItem.setChecked(); + break; + case 'center': + this.textarea.style.textAlign = 'center'; + toolbarItem.setChecked(); + break; + case 'right': + this.textarea.style.textAlign = 'right'; + toolbarItem.setChecked(); + break; + + default: + break; + } +}; + +FormatToolbar.prototype.setFocusToFirstAlignItem = function () { + this.setFocusItem(this.alignItems[0]); +}; + +FormatToolbar.prototype.setFocusToLastAlignItem = function () { + this.setFocusItem(this.alignItems[2]); +}; + +FormatToolbar.prototype.setFontFamily = function (font) { + this.textarea.style.fontFamily = font; +}; + +FormatToolbar.prototype.activateItem = function (toolbarItem) { + switch (toolbarItem.buttonAction) { + case 'bold': + this.toggleBold(toolbarItem); + break; + case 'underline': + this.toggleUnderline(toolbarItem); + break; + case 'italic': + this.toggleItalic(toolbarItem); + break; + case 'align': + this.setAlignment(toolbarItem); + break; + case 'copy': + this.copyTextContent(toolbarItem); + break; + case 'cut': + this.cutTextContent(toolbarItem); + break; + case 'paste': + this.pasteTextContent(toolbarItem); + break; + case 'font-family': + this.setFontFamily(toolbarItem.value); + break; + case 'nightmode': + this.toggleNightMode(toolbarItem); + break; + case 'link': + this.redirectLink(toolbarItem); + break; + default: + break; + } +}; + +/** + * @description + * Focus on the specified item + * @param item + * The item to focus on + */ +FormatToolbar.prototype.setFocusItem = function (item) { + for (var i = 0; i < this.toolbarItems.length; i++) { + this.toolbarItems[i].domNode.setAttribute('tabindex', '-1'); + } + + item.domNode.setAttribute('tabindex', '0'); + item.domNode.focus(); +}; + +FormatToolbar.prototype.setFocusToNext = function (currentItem) { + var index, newItem; + + if (currentItem === this.lastItem) { + newItem = this.firstItem; + } else { + index = this.toolbarItems.indexOf(currentItem); + newItem = this.toolbarItems[index + 1]; + } + this.setFocusItem(newItem); +}; + +FormatToolbar.prototype.setFocusToPrevious = function (currentItem) { + var index, newItem; + + if (currentItem === this.firstItem) { + newItem = this.lastItem; + } else { + index = this.toolbarItems.indexOf(currentItem); + newItem = this.toolbarItems[index - 1]; + } + this.setFocusItem(newItem); +}; + +FormatToolbar.prototype.setFocusToFirst = function () { + this.setFocusItem(this.firstItem); +}; + +FormatToolbar.prototype.setFocusToLast = function () { + this.setFocusItem(this.lastItem); +}; + +FormatToolbar.prototype.hidePopupLabels = function () { + var tps = this.domNode.querySelectorAll('button .popup-label'); + tps.forEach(function (tp) { + tp.classList.remove('show'); + }); +}; + +// Initialize toolbars + +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * ARIA Toolbar Examples + * @function onload + * @desc Initialize the toolbar example once the page has loaded + */ + +window.addEventListener('load', function () { + var toolbars = document.querySelectorAll('[role="toolbar"].format'); + + for (var i = 0; i < toolbars.length; i++) { + var toolbar = new FormatToolbar(toolbars[i]); + + toolbar.init(); + } +}); +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: FontToolbarItem.js + */ + +'use strict'; + +function FormatToolbarItem(domNode, toolbar) { + this.domNode = domNode; + this.toolbar = toolbar; + this.buttonAction = ''; + this.value = ''; + this.popupLabelNode = null; + this.hasHover = false; + this.popupLabelDelay = 800; + + this.keyCode = Object.freeze({ + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + PAGEUP: 33, + PAGEDOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + }); +} + +FormatToolbarItem.prototype.init = function () { + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this)); + this.domNode.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); + + document.body.addEventListener( + 'keydown', + this.handleHideAllPopupLabels.bind(this) + ); + + if (this.domNode.classList.contains('bold')) { + this.buttonAction = 'bold'; + } + + if (this.domNode.classList.contains('italic')) { + this.buttonAction = 'italic'; + } + + if (this.domNode.classList.contains('underline')) { + this.buttonAction = 'underline'; + } + + if (this.domNode.classList.contains('align-left')) { + this.buttonAction = 'align'; + this.value = 'left'; + this.toolbar.alignItems.push(this); + } + + if (this.domNode.classList.contains('align-center')) { + this.buttonAction = 'align'; + this.value = 'center'; + this.toolbar.alignItems.push(this); + } + + if (this.domNode.classList.contains('align-right')) { + this.buttonAction = 'align'; + this.value = 'right'; + this.toolbar.alignItems.push(this); + } + if (this.domNode.classList.contains('nightmode')) { + this.buttonAction = 'nightmode'; + } + if (this.domNode.classList.contains('link')) { + this.buttonAction = 'link'; + } + if (this.domNode.classList.contains('copy')) { + this.buttonAction = 'copy'; + } + if (this.domNode.classList.contains('paste')) { + this.buttonAction = 'paste'; + } + if (this.domNode.classList.contains('cut')) { + this.buttonAction = 'cut'; + } + if (this.domNode.classList.contains('spinbutton')) { + this.buttonAction = 'changeFontSize'; + } + // Initialize any popup label + + this.popupLabelNode = this.domNode.querySelector('.popup-label'); + if (this.popupLabelNode) { + var width = 8 * this.popupLabelNode.textContent.length; + this.popupLabelNode.style.width = width + 'px'; + this.popupLabelNode.style.left = + -1 * ((width - this.domNode.offsetWidth) / 2) - 5 + 'px'; + } +}; + +FormatToolbarItem.prototype.isPressed = function () { + return this.domNode.getAttribute('aria-pressed') === 'true'; +}; + +FormatToolbarItem.prototype.setPressed = function () { + this.domNode.setAttribute('aria-pressed', 'true'); +}; + +FormatToolbarItem.prototype.resetPressed = function () { + this.domNode.setAttribute('aria-pressed', 'false'); +}; + +FormatToolbarItem.prototype.setChecked = function () { + this.domNode.setAttribute('aria-checked', 'true'); + this.domNode.checked = true; +}; + +FormatToolbarItem.prototype.resetChecked = function () { + this.domNode.setAttribute('aria-checked', 'false'); + this.domNode.checked = false; +}; + +FormatToolbarItem.prototype.disable = function () { + this.domNode.setAttribute('aria-disabled', 'true'); +}; + +FormatToolbarItem.prototype.enable = function () { + this.domNode.removeAttribute('aria-disabled'); +}; + +FormatToolbarItem.prototype.showPopupLabel = function () { + if (this.popupLabelNode) { + this.toolbar.hidePopupLabels(); + this.popupLabelNode.classList.add('show'); + } +}; + +FormatToolbarItem.prototype.hidePopupLabel = function () { + if (this.popupLabelNode && !this.hasHover) { + this.popupLabelNode.classList.remove('show'); + } +}; + +// Events + +FormatToolbarItem.prototype.handleHideAllPopupLabels = function (event) { + switch (event.keyCode) { + case this.keyCode.ESC: + this.toolbar.hidePopupLabels(); + break; + + default: + break; + } +}; + +FormatToolbarItem.prototype.handleBlur = function () { + this.toolbar.domNode.classList.remove('focus'); + + if (this.domNode.classList.contains('nightmode')) { + this.domNode.parentNode.classList.remove('focus'); + } + this.hidePopupLabel(); +}; + +FormatToolbarItem.prototype.handleFocus = function () { + this.toolbar.domNode.classList.add('focus'); + + if (this.domNode.classList.contains('nightmode')) { + this.domNode.parentNode.classList.add('focus'); + } + this.showPopupLabel(); +}; + +FormatToolbarItem.prototype.handleMouseLeave = function () { + this.hasHover = false; + setTimeout(this.hidePopupLabel.bind(this), this.popupLabelDelay); +}; + +FormatToolbarItem.prototype.handleMouseOver = function () { + this.showPopupLabel(); + this.hasHover = true; +}; + +FormatToolbarItem.prototype.handleKeyDown = function (event) { + var flag = false; + + switch (event.keyCode) { + case this.keyCode.ENTER: + case this.keyCode.SPACE: + if ( + this.buttonAction !== '' && + this.buttonAction !== 'bold' && + this.buttonAction !== 'italic' && + this.buttonAction !== 'underline' + ) { + this.toolbar.activateItem(this); + if (this.buttonAction !== 'nightmode') { + flag = true; + } + } + break; + + case this.keyCode.RIGHT: + this.toolbar.setFocusToNext(this); + flag = true; + break; + + case this.keyCode.LEFT: + this.toolbar.setFocusToPrevious(this); + flag = true; + break; + + case this.keyCode.HOME: + this.toolbar.setFocusToFirst(this); + flag = true; + break; + + case this.keyCode.END: + this.toolbar.setFocusToLast(this); + flag = true; + break; + + case this.keyCode.UP: + if (this.buttonAction === 'align') { + if (this.domNode.classList.contains('align-left')) { + this.toolbar.setFocusToLastAlignItem(); + } else { + this.toolbar.setFocusToPrevious(this); + } + flag = true; + } + break; + case this.keyCode.DOWN: + if (this.buttonAction === 'align') { + if (this.domNode.classList.contains('align-right')) { + this.toolbar.setFocusToFirstAlignItem(); + } else { + this.toolbar.setFocusToNext(this); + } + flag = true; + } + break; + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +FormatToolbarItem.prototype.handleClick = function () { + if (this.buttonAction == 'link') { + return; + } + + this.toolbar.setFocusItem(this); + this.toolbar.activateItem(this); +}; +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: FontMenuButton.js + */ + +/* global FontMenu */ + +'use strict'; + +function FontMenuButton(node, toolbar, toolbarItem) { + this.domNode = node; + this.fontMenu = false; + this.toolbar = toolbar; + this.toolbarItem = toolbarItem; + + this.buttonAction = 'font-family'; + this.value = ''; + + this.keyCode = Object.freeze({ + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + UP: 38, + DOWN: 40, + }); +} + +FontMenuButton.prototype.init = function () { + var id = this.domNode.getAttribute('aria-controls'); + + if (id) { + var node = document.getElementById(id); + + if (node) { + this.fontMenu = new FontMenu(node, this); + this.fontMenu.init(); + } + } + + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); +}; + +FontMenuButton.prototype.handleKeyDown = function (event) { + var flag = false; + + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.ENTER: + case this.keyCode.DOWN: + case this.keyCode.UP: + this.fontMenu.open(); + this.fontMenu.setFocusToCheckedItem(); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +FontMenuButton.prototype.handleClick = function () { + if (this.fontMenu.isOpen()) { + this.fontMenu.close(); + } else { + this.fontMenu.open(); + } +}; + +FontMenuButton.prototype.setFontFamily = function (font) { + this.value = font; + this.domNode.innerHTML = font.toUpperCase() + ''; + this.domNode.style.fontFamily = font; + this.domNode.setAttribute('aria-label', 'Font: ' + font); + this.toolbar.activateItem(this); +}; +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: FontMenu.js + */ + +/* global FontMenuItem */ + +'use strict'; + +var FontMenu = function (domNode, controllerObj) { + var msgPrefix = 'FontMenu constructor argument domNode '; + + // Check whether domNode is a DOM element + if (!(domNode instanceof Element)) { + throw new TypeError(msgPrefix + 'is not a DOM Element.'); + } + + // Check whether domNode has child elements + if (domNode.childElementCount === 0) { + throw new Error(msgPrefix + 'has no element children.'); + } + + // Check whether domNode child elements are A elements + var childElement = domNode.firstElementChild; + + while (childElement) { + var menuitem = childElement.firstElementChild; + + if (menuitem && menuitem === 'A') { + throw new Error( + msgPrefix + 'Cannot have descendant elements are A elements.' + ); + } + childElement = childElement.nextElementSibling; + } + + this.domNode = domNode; + this.controller = controllerObj; + + this.menuitems = []; // See PopupMenu init method + this.firstChars = []; // See PopupMenu init method + + this.firstItem = null; // See PopupMenu init method + this.lastItem = null; // See PopupMenu init method + + this.hasFocus = false; // See MenuItem handleFocus, handleBlur + this.hasHover = false; // See PopupMenu handleMouseover, handleMouseout +}; + +/* + * @method FontMenu.prototype.init + * + * @desc + * Add domNode event listeners for mouseover and mouseout. Traverse + * domNode children to configure each menuitem and populate menuitems + * array. Initialize firstItem and lastItem properties. + */ +FontMenu.prototype.init = function () { + var menuitemElements, menuitemElement, menuItem, textContent, numItems; + + // Configure the domNode itself + this.domNode.setAttribute('tabindex', '-1'); + + this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); + + // Traverse the element children of domNode: configure each with + // menuitem role behavior and store reference in menuitems array. + menuitemElements = this.domNode.querySelectorAll('[role="menuitemradio"]'); + + for (var i = 0; i < menuitemElements.length; i++) { + menuitemElement = menuitemElements[i]; + menuItem = new FontMenuItem(menuitemElement, this); + menuItem.init(); + this.menuitems.push(menuItem); + textContent = menuitemElement.textContent.trim(); + this.firstChars.push(textContent.substring(0, 1).toLowerCase()); + } + + // Use populated menuitems array to initialize firstItem and lastItem. + numItems = this.menuitems.length; + if (numItems > 0) { + this.firstItem = this.menuitems[0]; + this.lastItem = this.menuitems[numItems - 1]; + } +}; + +/* EVENT HANDLERS */ + +FontMenu.prototype.handleMouseover = function () { + this.hasHover = true; +}; + +FontMenu.prototype.handleMouseout = function () { + this.hasHover = false; + setTimeout(this.close.bind(this, false), 300); +}; + +/* FOCUS MANAGEMENT METHODS */ + +FontMenu.prototype.setFocusToController = function (command) { + if (typeof command !== 'string') { + command = ''; + } + + if (command === 'previous') { + this.controller.toolbar.setFocusToPrevious(this.controller.toolbarItem); + } else { + if (command === 'next') { + this.controller.toolbar.setFocusToNext(this.controller.toolbarItem); + } else { + this.controller.domNode.focus(); + } + } +}; + +FontMenu.prototype.setFontFamily = function (menuitem, font) { + for (var i = 0; i < this.menuitems.length; i++) { + var mi = this.menuitems[i]; + mi.domNode.setAttribute('aria-checked', mi === menuitem); + } + this.controller.setFontFamily(font); +}; + +FontMenu.prototype.setFocusToFirstItem = function () { + this.firstItem.domNode.focus(); +}; + +FontMenu.prototype.setFocusToLastItem = function () { + this.lastItem.domNode.focus(); +}; + +FontMenu.prototype.setFocusToPreviousItem = function (currentItem) { + var index; + + if (currentItem === this.firstItem) { + this.lastItem.domNode.focus(); + } else { + index = this.menuitems.indexOf(currentItem); + this.menuitems[index - 1].domNode.focus(); + } +}; + +FontMenu.prototype.setFocusToNextItem = function (currentItem) { + var index; + + if (currentItem === this.lastItem) { + this.firstItem.domNode.focus(); + } else { + index = this.menuitems.indexOf(currentItem); + this.menuitems[index + 1].domNode.focus(); + } +}; + +FontMenu.prototype.setFocusToCheckedItem = function () { + for (var index = 0; index < this.menuitems.length; index++) { + if (this.menuitems[index].domNode.getAttribute('aria-checked') === 'true') { + this.menuitems[index].domNode.focus(); + } + } +}; + +FontMenu.prototype.setFocusByFirstCharacter = function (currentItem, char) { + var start, index; + + char = char.toLowerCase(); + + // Get start index for search based on position of currentItem + start = this.menuitems.indexOf(currentItem) + 1; + if (start === this.menuitems.length) { + start = 0; + } + + // Check remaining slots in the menu + index = this.getIndexFirstChars(start, char); + + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.getIndexFirstChars(0, char); + } + + // If match was found... + if (index > -1) { + this.menuitems[index].domNode.focus(); + } +}; + +FontMenu.prototype.getIndexFirstChars = function (startIndex, char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (char === this.firstChars[i]) { + return i; + } + } + return -1; +}; + +/* Focus methods */ + +FontMenu.prototype.setFocus = function () { + this.hasFocus = true; + this.domNode.classList.add('focus'); + this.controller.toolbar.domNode.classList.add('focus'); +}; + +FontMenu.prototype.removeFocus = function () { + this.hasFocus = false; + this.domNode.classList.remove('focus'); + this.controller.toolbar.domNode.classList.remove('focus'); + setTimeout(this.close.bind(this, false), 300); +}; + +/* MENU DISPLAY METHODS */ + +FontMenu.prototype.isOpen = function () { + return this.controller.domNode.getAttribute('aria-expanded') === 'true'; +}; + +FontMenu.prototype.open = function () { + // Get bounding rectangle of controller object's DOM node + var rect = this.controller.domNode.getBoundingClientRect(); + + // Set CSS properties + this.domNode.style.display = 'block'; + this.domNode.style.position = 'absolute'; + this.domNode.style.top = rect.height - 1 + 'px'; + this.domNode.style.left = '0px'; + this.domNode.style.zIndex = 100; + + // Set aria-expanded attribute + this.controller.domNode.setAttribute('aria-expanded', 'true'); +}; + +FontMenu.prototype.close = function (force) { + if (typeof force !== 'boolean') { + force = false; + } + + if ( + force || + (!this.hasFocus && !this.hasHover && !this.controller.hasHover) + ) { + this.domNode.style.display = 'none'; + this.controller.domNode.setAttribute('aria-expanded', 'false'); + } +}; +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: FontMenuItem.js + */ + +'use strict'; + +/* + * @constructor MenuItem + * + * @desc + * Wrapper object for a simple menu item in a popup menu + * + * @param domNode + * The DOM element node that serves as the menu item container. + * The menuObj PopupMenu is responsible for checking that it has + * requisite metadata, e.g. role="menuitem". + * + * @param menuObj + * The object that is a wrapper for the PopupMenu DOM element that + * contains the menu item DOM element. See PopupMenuAction.js + */ +var FontMenuItem = function (domNode, fontMenu) { + this.domNode = domNode; + this.fontMenu = fontMenu; + this.font = ''; + + this.keyCode = Object.freeze({ + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + PAGEUP: 33, + PAGEDOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + }); +}; + +FontMenuItem.prototype.init = function () { + this.domNode.setAttribute('tabindex', '-1'); + + if (!this.domNode.getAttribute('role')) { + this.domNode.setAttribute('role', 'menuitemradio'); + } + + this.font = this.domNode.textContent.trim().toLowerCase(); + + this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); +}; + +/* EVENT HANDLERS */ + +FontMenuItem.prototype.handleKeydown = function (event) { + var flag = false, + char = event.key; + + function isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } + + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + + if (event.shiftKey) { + if (isPrintableCharacter(char)) { + this.fontMenu.setFocusByFirstCharacter(this, char); + } + } else { + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.ENTER: + this.handleClick(event); + flag = true; + break; + + case this.keyCode.ESC: + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); + flag = true; + break; + + case this.keyCode.UP: + this.fontMenu.setFocusToPreviousItem(this); + flag = true; + break; + + case this.keyCode.DOWN: + this.fontMenu.setFocusToNextItem(this); + flag = true; + break; + + case this.keyCode.RIGHT: + flag = true; + break; + + case this.keyCode.LEFT: + flag = true; + break; + + case this.keyCode.HOME: + case this.keyCode.PAGEUP: + this.fontMenu.setFocusToFirstItem(); + flag = true; + break; + + case this.keyCode.END: + case this.keyCode.PAGEDOWN: + this.fontMenu.setFocusToLastItem(); + flag = true; + break; + + case this.keyCode.TAB: + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); + break; + + default: + if (isPrintableCharacter(char)) { + this.fontMenu.setFocusByFirstCharacter(this, char); + } + break; + } + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } +}; + +FontMenuItem.prototype.handleClick = function () { + this.fontMenu.setFontFamily(this, this.font); + this.fontMenu.setFocusToController(); + this.fontMenu.close(true); +}; + +FontMenuItem.prototype.handleFocus = function () { + this.fontMenu.setFocus(); +}; + +FontMenuItem.prototype.handleBlur = function () { + this.fontMenu.removeFocus(); +}; + +FontMenuItem.prototype.handleMouseover = function () { + this.fontMenu.hasHover = true; + this.fontMenu.open(); +}; + +FontMenuItem.prototype.handleMouseout = function () { + this.fontMenu.hasHover = false; + setTimeout(this.fontMenu.close.bind(this.fontMenu, false), 300); +}; +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: SpinButton.js + */ + +'use strict'; + +// Create SpinButton that contains value, valuemin, valuemax, and valuenow +var SpinButton = function (domNode, toolbar) { + this.domNode = domNode; + this.toolbar = toolbar; + + this.valueDomNode = domNode.querySelector('.value'); + this.increaseDomNode = domNode.querySelector('.increase'); + this.decreaseDomNode = domNode.querySelector('.decrease'); + + this.valueMin = 8; + this.valueMax = 40; + this.valueNow = 12; + this.valueText = this.valueNow + ' Point'; + + this.keyCode = Object.freeze({ + UP: 38, + DOWN: 40, + PAGEUP: 33, + PAGEDOWN: 34, + END: 35, + HOME: 36, + }); +}; + +// Initialize slider +SpinButton.prototype.init = function () { + if (this.domNode.getAttribute('aria-valuemin')) { + this.valueMin = parseInt(this.domNode.getAttribute('aria-valuemin')); + } + + if (this.domNode.getAttribute('aria-valuemax')) { + this.valueMax = parseInt(this.domNode.getAttribute('aria-valuemax')); + } + + if (this.domNode.getAttribute('aria-valuenow')) { + this.valueNow = parseInt(this.domNode.getAttribute('aria-valuenow')); + } + + this.setValue(this.valueNow); + + this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); + + this.increaseDomNode.addEventListener( + 'click', + this.handleIncreaseClick.bind(this) + ); + this.decreaseDomNode.addEventListener( + 'click', + this.handleDecreaseClick.bind(this) + ); +}; + +SpinButton.prototype.setValue = function (value) { + if (value > this.valueMax) { + value = this.valueMax; + } + + if (value < this.valueMin) { + value = this.valueMin; + } + + this.valueNow = value; + this.valueText = value + ' Point'; + + this.domNode.setAttribute('aria-valuenow', this.valueNow); + this.domNode.setAttribute('aria-valuetext', this.valueText); + + if (this.valueDomNode) { + this.valueDomNode.innerHTML = this.valueText; + } + + this.toolbar.changeFontSize(value); +}; + +SpinButton.prototype.handleKeyDown = function (event) { + var flag = false; + + switch (event.keyCode) { + case this.keyCode.DOWN: + this.setValue(this.valueNow - 1); + flag = true; + break; + + case this.keyCode.UP: + this.setValue(this.valueNow + 1); + flag = true; + break; + + case this.keyCode.PAGEDOWN: + this.setValue(this.valueNow - 5); + flag = true; + break; + + case this.keyCode.PAGEUP: + this.setValue(this.valueNow + 5); + flag = true; + break; + + case this.keyCode.HOME: + this.setValue(this.valueMin); + flag = true; + break; + + case this.keyCode.END: + this.setValue(this.valueMax); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.preventDefault(); + event.stopPropagation(); + } +}; + +SpinButton.prototype.handleIncreaseClick = function (event) { + this.setValue(this.valueNow + 1); + + event.preventDefault(); + event.stopPropagation(); +}; + +SpinButton.prototype.handleDecreaseClick = function (event) { + this.setValue(this.valueNow - 1); + + event.preventDefault(); + event.stopPropagation(); +};