diff --git a/README.md b/README.md index 42105f7..0ba5475 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,106 @@ # keymanweb.com -Home page for online web keyboard with support for over 2000 languages +This is the source for the website https://keymanweb.com/, which hosts the +KeymanWeb online keyboard supporting over 2500 different languages. This site +runs on Apache in a Docker container. + +## Other Keyman websites + +* **[api.keyman.com]** - database backend for Keyman websites +* **[help.keyman.com]** - documentation home for Keyman +* **[keyman.com]** - Keyman home +* **[keymanweb.com]** - KeymanWeb online keyboard +* **[s.keyman.com]** - static Javascript, font, and related resources +* **[website-local-proxy]** - run all Keyman sites on localhost on the same port + +## How to run keymanweb.com locally + +When run locally, this site can be accessed at http://localhost:8057 or +http://web.keyman.com.localhost:8057. Note that the site is internally called +web.keyman.com, but the public site is at keymanweb.com; web.keyman.com +automatically forwards to keymanweb.com. + +**Recommended:** Use [website-local-proxy] to run multiple keyman.com sites +all from the same port (default port 80). + +**Recommended:** Use [shared-sites] to control startup and shutdown of all +keyman.com sites together. + +### Prerequisites + +The host machine needs the following apps installed: +* [Git] +* Bash 5.x (on Windows, you can use Git Bash that comes with [Git]) +* [Docker Desktop] + +
+ Configuration of Docker on Windows + + On Windows machines, you can setup Docker in two different ways, either of + which should work: + * [Enable Hyper-V on Windows 11](https://techcommunity.microsoft.com/t5/educator-developer-blog/step-by-step-enabling-hyper-v-for-use-on-windows-11/ba-p/3745905) + * [WSL2](https://ubuntu.com/tutorials/install-ubuntu-on-wsl2-on-windows-10#1-overview) + +
+ +### Actions + +#### Build the Docker image + +The first time you want to start up the site, or if there have been Docker +configuration changes, you will need to rebuild the Docker images. Start a bash +shell, and from this folder, run: + +```sh +./build.sh build +``` + +#### Start the Docker container + +To start up the website, in bash, run: + +```sh +./build.sh start --debug +``` + +Once the container starts, you can access the web.keyman.com site at +http://localhost:8057 or http://web.keyman.com.localhost:8057. Note that the +site is internally called web.keyman.com, but the public site is at +keymanweb.com; web.keyman.com automatically forwards to keymanweb.com. + +#### Stop the Docker container + +In bash, run: + +```sh +./build.sh stop +``` + +#### Remove the Docker container and image + +In bash, run: + +```sh +./build.sh clean +``` + +#### Running tests + +To check for broken links and .php file conformance, when the site is running, +in bash, run: + +```sh +./build.sh test +``` + +[Git]: https://git-scm.com/downloads +[Docker Desktop]: https://docs.docker.com/get-docker/ +[api.keyman.com]: https://github.com/keymanapp/api.keyman.com +[help.keyman.com]: https://github.com/keymanapp/help.keyman.com +[keyman.com]: https://github.com/keymanapp/keyman.com +[keymanweb.com]: https://github.com/keymanapp/keymanweb.com +[s.keyman.com]: https://github.com/keymanapp/s.keyman.com +[website-local-proxy]: https://github.com/keymanapp/website-local-proxy +[shared-sites]: https://github.com/keymanapp/shared-sites +[enable Hyper-V]: https://techcommunity.microsoft.com/t5/educator-developer-blog/step-by-step-enabling-hyper-v-for-use-on-windows-11/ba-p/3745905 +[enable WSL2]: https://ubuntu.com/tutorials/install-ubuntu-on-wsl2-on-windows-10#1-overview diff --git a/build.sh b/build.sh index 04c255d..19ea186 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ ## START STANDARD SITE BUILD SCRIPT INCLUDE readonly THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" readonly BOOTSTRAP="$(dirname "$THIS_SCRIPT")/resources/bootstrap.inc.sh" -readonly BOOTSTRAP_VERSION=v1.0.6 +readonly BOOTSTRAP_VERSION=v1.08 [ -f "$BOOTSTRAP" ] && source "$BOOTSTRAP" || source <(curl -H "Cache-Control: no-cache" -fs https://raw.githubusercontent.com/keymanapp/shared-sites/$BOOTSTRAP_VERSION/bootstrap.inc.sh) ## END STANDARD SITE BUILD SCRIPT INCLUDE diff --git a/cdn/dev/css/feat-instruction.css b/cdn/dev/css/feat-instruction.css new file mode 100644 index 0000000..47758f6 --- /dev/null +++ b/cdn/dev/css/feat-instruction.css @@ -0,0 +1,239 @@ +/* ========================= + Instruction Modal Overlay +========================= */ + +.instruction-modal-overlay { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 12px; + background: rgba(0, 0, 0, 0.18); + z-index: 9999; +} + +/* ========================= + Modal Container +========================= */ + +.instruction-modal { + display: flex; + flex-direction: column; + justify-content: space-between; + background: #ffffff; + border-radius: 36px; + padding: 30px 72px; +} + +/* ========================= + Header +========================= */ + +.instruction-modal-header { + text-align: center; +} + +.instruction-modal-title { + margin: 0; + font-size: 28px; + font-weight: 500; + color: black; + padding: 20px; +} + +/* ========================= + Body +========================= */ + +.instruction-modal-body { + display: flex; + align-items: stretch; + justify-content: center; + gap: 40px; + flex-wrap: wrap; + margin: 20px 0; +} + +/* ========================= + Instruction Card +========================= */ + +.instruction-card { + width: 300px; + min-height: 264px; + background: #ffffff; + border-radius: 34px; + padding: 28px 22px; + display: flex; + flex-direction: column; + align-items: center; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +/* ========================= + Card Border Variants +========================= */ + +.instruction-card-search { + border: 2px solid #ff7a00; +} + +.instruction-card-pick { + border: 2px solid #d72b55; +} + +.instruction-card-type { + border: 2px solid #6ec6ea; +} + +/* ========================= + Card Heading +========================= */ + +.instruction-card-heading h3 { + margin: 0; + font-size: 1.5rem; + font-weight: 400; + color: black; + text-align: center; +} + +/* ========================= + Card Graphic +========================= */ + +.instruction-card-graphic { + margin-top: 28px; +} + +.instruction-card-graphic img { + width: 220px; + max-width: 100%; + height: auto; + object-fit: contain; + display: block; +} + +/* ========================= + Divider +========================= */ + +.instruction-card-divider { + width: 100%; + height: 2px; + opacity: 50%; + background: #262626; + margin: 26px 0; +} + +/* ========================= + Description +========================= */ + +.instruction-card-description { + text-align: center; +} + +.instruction-card-description p { + margin: 0; + font-size: 1rem; + line-height: 1.45; + color: #474747; +} + +/* ========================= + Footer +========================= */ + +.instruction-modal-footer { + display: flex; + align-items: center; + justify-content: center; + gap: 48px; + flex-wrap: wrap; + margin-top: 20px; +} + +/* ========================= + Buttons +========================= */ + +.instruction-modal-btn { + border: transparent; + min-width: 200px; + padding: 14px 34px !important; + border-radius: 999px; + background: var(--keyman-blue); + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + font-weight: 500; + color: white; + cursor: pointer; + transition: + background 0.2s ease, + transform 0.2s ease, + color 0.2s ease; +} + +.instruction-modal-btn:hover { + border: 1px solid var(--keyman-blue); + background: transparent; + color: rgb(0, 0, 0); + transform: translateY(-2px); +} + +/* ========================= + Responsive +========================= */ + +@media (max-width: 1024px) { + + .instruction-modal { + padding: 48px 32px; + } + + .instruction-modal-body { + gap: 32px; + } + + .instruction-card { + width: 100%; + max-width: 360px; + } +} + +@media (max-width: 640px) { + + .instruction-modal { + min-height: auto; + border-radius: 24px; + + padding: 32px 20px; + } + + .instruction-modal-title { + font-size: 2rem; + } + + .instruction-card { + min-height: auto; + } + + .instruction-card-heading h3 { + font-size: 1.6rem; + } + + .instruction-modal-footer { + gap: 20px; + } + + .instruction-modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/cdn/dev/css/footer.css b/cdn/dev/css/footer.css deleted file mode 100644 index e69de29..0000000 diff --git a/cdn/dev/css/kmw-body.css b/cdn/dev/css/kmw-body.css new file mode 100644 index 0000000..b81f69c --- /dev/null +++ b/cdn/dev/css/kmw-body.css @@ -0,0 +1,139 @@ +/* + Name: KeymanWeb's Body Styles + Copyright: Copyright (C) 2025 SIL Global + Create Date: 5 November 2024 + + Modified Date: + Authors: Meng-Heng + + History: + + Description: All of the styles for KMW's Body +*/ +/* +========= +==Icons== +========= +*/ +.fa-grip-lines:hover { + cursor: grab; +} +/* +================ +==Section Tag== +================ +*/ +.container-flex { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} +/* +============== +==Text area== +============== +*/ +.textarea-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: var(--textarea-width); + height: var(--textarea-height); +} + +.text-area { + padding: 10px; + resize: none; + width: 100%; + height: 100% !important; + box-shadow: var(--textarea-box); + font-size: 16px; + border: none; + padding: 10px; + line-height: 1.6em; + font-family:SindhiWeb,Verdana,GeezWeb,LaoWeb,TibetanWeb,MyanmarWeb,SinhalaWeb,TamilWeb,KhmerWeb,LatinWeb,OriyaWeb,EgyptianWeb !important; +} +/* +============ +==Divider== +============ +*/ +.divider-container i { + opacity: 50%; +} + +.divider-container i:hover { + color: var(--keyman-blue); + opacity: 100%; +} +/* +===================================== +==Items in the middle of the divider== +===================================== +*/ +.middle-divider { + grid-area: middle; + text-align: center; + width: 100%; + height: 100%; + z-index: 1; +} +/* +============= +==Keyboard== +============= +*/ +.keyboard-container { + width: 70vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.keyboard-area { + user-select: none; + flex: 1; + width: inherit; + height: inherit; + border: var(--keyman-blue) 1px solid !important; +} + +.phone.kmw-osk-frame, .tablet.kmw-osk-frame { + display: block !important; + position: relative !important; + height: 100% !important; + width: 100% !important; + max-width: 100vw !important; + max-height: 50vh !important; + z-index: 0 !important; +} +/* +============= +==Examples== +============= +*/ +.example-box { + background-color: var(--keyman-blue); + color: white; + width: inherit; + text-align: center; + padding: 5px; + border-bottom: 1px solid rgb(159, 159, 159); +} + +#exampleBox { + visibility: visible !important; +} + +.example-box p { + margin: 10px; +} + +#example { + visibility: visible !important; +} + +/* End of Section Tag */ diff --git a/cdn/dev/css/kmw-desktop.css b/cdn/dev/css/kmw-desktop.css index 68a0620..514abd5 100644 --- a/cdn/dev/css/kmw-desktop.css +++ b/cdn/dev/css/kmw-desktop.css @@ -6,677 +6,180 @@ ====================== */ -html { - overflow-y: auto; - overflow-x: hidden; -} - -body { - width:100%; - font-family: Cabin, Arial, Helvetica, sans-serif; - color:#2D2C2C; - margin:0px; - } - -a { - text-decoration:none; - color: #2EADD3; - } - -a:hover { - text-decoration:underline; - } - -h3 { - font-size:18px; - } - -p { - font-size:13px; - line-height:1.5em; - } - -header, #content, footer { - /* width:960px; */ - margin:0px auto; - clear: both; - } - -.box { - -webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; +#kmw_controls{ + border-top-left-radius: 8px; + border-top-right-radius: 8px; -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - } - -/* -=================== -==Header Elements== -=================== -*/ -header { - margin-top:0px; - height: 96px; - } - -header > div > img { - width: 100%; - display: block; - height: 8px; } -#headerLeft img { - display: block; /* default (inline) display inserts an unwanted bottom border */ - margin-top:0px; - margin-left:28px; - height:88px; - /*width: 660px;*/ - /*-webkit-border-radius: 0px 0px 8px 8px; -moz-border-radius: 0px 0px 8px 8px; border-radius: 0px 0px 8px 8px;*/ - } -#headerRight { - display: block; - float: right; - text-align: right; - } - -#headerRight-beta { - display: block; - font-weight: bold; - color: red; - font-size: 16pt; - padding: 20px 12px 0; +#kmw_btn_osk{ + border-top-right-radius: 10px; } -#headerRight-link { - padding: 0 12px 0; +#kmw_controls_start{ + border-top-left-radius: 10px; } + /* -==================== -==Content Elements== -==================== +====================== +==Mobile Tool Box== +====================== */ -#content { - min-height:375px; - } - - -/* KMW Main App */ -#app { - float:left; - /*height:330px;*/ - width:710px; - margin: 30px 15px 15px; - } -#KeymanWebControl { - width:710px; - height: 35px; - display:block; - -webkit-border-radius: 8px 8px 0px 0px; -moz-border-radius: 8px 8px 0px 0px; border-radius: 8px 8px 0px 0px; - } - - #kmw_controls{ - border-top-left-radius: 8px; - border-top-right-radius: 8px; - -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - } - - #kmw_btn_osk{ - border-top-right-radius: 10px; - } - - #kmw_controls_start{ - border-top-left-radius: 10px; - } - -#example,#help { - margin:8px 10px; - background-color:#95C6D6; - border:1px solid #2EADD3; - padding:2px; - text-align:center; - } - -#example { - visibility: hidden; - width:685px; - min-height: 45px; - display: flex; - justify-content: center; - align-items: center; - padding-top: 5px; -} - -#example a{ - padding-left: 10px; +.tool-menu { + display: none; } -textarea { - height:260px; - width:554px; - float:left; - margin: 0px 10px 10px; - color:black; - font-size:16px; - font-family:SindhiWeb,Verdana,GeezWeb,LaoWeb,TibetanWeb,MyanmarWeb,SinhalaWeb,TamilWeb,KhmerWeb,LatinWeb,OriyaWeb,EgyptianWeb !important; - } - form { padding-right:10px; float:left; - } -#messageContainer { - float:left; - width:80%; - } - -/* KMW App Buttons */ - -#instructions { - margin:-1px 0px 2px; - } - -#buttons { - float:right; - padding-right:10px; - } - -#buttons div { - width:120px; - -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; - background-color: #eeeeee; - border: 1px solid #888888; - margin: 0px 10px; -/* -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - color: white; - font: bold 14px Helvetica, Verdana, Arial, sans-serif; - text-shadow: 0px -1px 1px #1E2D4D; */ - text-align:left; - margin: 0 auto 6px; - padding: 3px; - padding-top: 9px; - height: 26px; - background-repeat: no-repeat; - background-position: center left; - } -#buttons div:disabled { - cursor: default !important; - color: #eeeeee !important; - /* -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; */ -} - -#buttons p{ - margin-left: 41px; -} - -#search{ - background-image:url('../img/btn_google_small.png'); - -} - -#search.linksOff{ - background-image:url('../img/btn_google_small_white.png'); -} - -#copy{ - background-image:url('../img/btn_copy_small.png'); - -} - -#copy.linksOff{ - background-image:url('../img/btn_copy_small_white.png'); } -#font{ - display: none; -} - -#mobile-font{ +/* +================== +==Mobile Divider== +================== +*/ +.left-divider { display: none; } -#font-size{ - float: right; - width: 120px; - margin-right: 10px; - margin-bottom: 8px; - padding: 0px 4px; - display: table; -} - -#font-small{ - font-size: 10pt; - padding-top: 7px; - text-align: left; -} - -#font-large{ - font-size: 16pt; - text-align: right; -} - -.ui-slider-handle{ - width: 10px !important; - height: 10px !important; - cursor: pointer !important; - outline: none !important; -} - -#slider{ - height: 4px; - width: 68px; - float: left; - position: relative; - top: 14px; -} - -.font-letter{ - float: left; - width: 25px; - text-align: center; - display: table-cell; -} - -#buttons div * { - display:inline; - } - -div.links { - cursor:pointer; - } - -div.linksOff { - cursor:default !important; - border: 1px solid #cccccc !important; - } - -div.linksOff * { - color: gray; - -moz-user-select:none; - } - -hr { - margin: 6px 4px 6px 4px; - } - - -/* Aside Elements */ -aside { - float:right; - width:200px; - margin-right:15px; - } -aside h3 { - background-color: rgba(181, 177, 176, 0.2); - padding:6px 10px 7px; - margin:0px; - -webkit-border-radius: 8px 8px 0px 0px; -moz-border-radius: 8px 8px 0px 0px; border-radius: 8px 8px 0px 0px; - -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - } -aside > div { - margin:30px 0px 0px; - } -aside p { - padding:10px; - margin:0px; - } -aside #learn { - font-size:13px; - text-align:center; - } - -#keymandesktop, #bookmarklet{ +.right-divider { display: none; } /* -=================== -==Footer Elements== -=================== +================== +==Tool Container== +================== */ - - -.button { - float: left; - width: 200px; - height: 40px; - line-height: 40px; - border-radius: 6px; - text-align: center; - margin: 20px; - cursor: pointer; - box-shadow: 1px 1px 2px #3e3e3e; -} - -.footer{ - float: left; - text-align: center; - width: 100%; - background: #A4A8AB; - line-height: 1; -} - -.footer .wrapper { - width: auto; - display: inline-block; - margin: 0; - text-align: left; -} - -.footer-third{ - float: left; - width: 320px; - margin-left: 4px; - margin-right: 4px; -} - -.footer-third-title{ - margin-top: 30px; - text-align: center; - font-size: 18pt; - font-weight: normal; - color: #fff; - margin-bottom: 25px; +.tool-container{ + background-color: var(--bs-body-bg); + border-radius: 15px; + border: var(--bs-border-width) solid var(--bs-border-color); + width: 400px; + height: 50px; + padding: 10px; } -.footer-third form{ - width: 320px; +.small-icon-tools, .large-icon-tools { + height: 40px; + padding: 0 10px; } -.footer-third form input{ - -webkit-appearance: none; - padding: 0px; - float: left; - height: 38px; - width: 200px; - padding-left: 10px; - font-size: 14pt; - border-radius: 6px; - border-bottom-right-radius: 0px; - border-top-right-radius: 0px; - box-shadow: none; - border: 1px solid #FFFFFF; - margin-left: 10px; +.large-icon-tools { + width: 50%; } -.footer-third form input:focus{ - outline: 0; +.small-icon-tools { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 50%; + height: 40px; + border-left: 0.5px solid gray; } -#footer-mailchimp br { - clear: both; +.small-icon-tools > * { + width: max-content; + height: max-content; + padding: 10px; + margin: 0 5px; } - /* - social-logos font from https://github.com/Automattic/social-logos, GPLv2 - */ -@font-face { - font-family: "social-logos"; - src: url("../../fonts/social-logos.ttf"); +====================== +==Keyboard Examples== +====================== +*/ +.highlightKeys kbd { + border: solid 1px #808080; + border-radius: 4px; + background: #cccccf; + color: black; font-weight: normal; - font-style: normal; -} - -[data-icon]:before { - font-family: "social-logos" !important; - content: attr(data-icon); - font-style: normal !important; - font-weight: normal !important; - font-variant: normal !important; - text-transform: none !important; - line-height: 1; - speak: none; - padding-right: 4px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } - -#footer-social { - text-align: center; -} - -#footer-social a { - display: block; - text-decoration: none; - color: #666666; - font-size: 14pt; - padding: 4px; -} - -#footer-social a:hover { - color: #444444; -} - -#privacy-policy { - text-align: center; - margin-top: 16px; -} - -#privacy-policy a { - display: block; - text-decoration: none; - color: white; - font-size: 12pt; -} - -#privacy-policy a:hover { - color: #444444; -} - -#version { - color: #666666; - font-size: 10pt; - margin-top: 46px; - text-align: center; -} - -#mobile-footer { - display: none; -} - -#footer-social div { + min-width: 16px; display: inline-block; - text-align: left; - margin-bottom: 24px; + box-shadow: 2px 2px 1px rgba(128, 128, 128, 0.3); + margin-right: 4px; } -#footer-social a#footer-community { - padding-left: 27px; +.highlightKeys kbd.space { + min-width: 64px; } -.subscribe{ - width: 90px; - margin: 0px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - background: #666666; - box-shadow: none; +/* +====================== +==Keyboard Styling== +====================== +*/ +.kmw-osk-frame { + display: block !important; + position: static !important; + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; + font-size: 21px !important; + border-radius: 0; } -.subscribe:hover{ - background: #888888; +.kmw-help-osk-frame { + height: 40vh !important; + scroll-behavior: smooth !important; + scrollbar-width: thin; + overflow-x: hidden !important; + overflow-y: scroll !important; + text-align: center !important; } -.subscribe h2{ - font-size: 16px; - margin: 0px; +.kmw-osk-static { + height: fit-content !important; + text-align: center !important; + border: none !important; } -.sil-logo a, .sil-logo a:visited { - color: white; - text-decoration: none; +.kmw-osk-static table { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: center !important; + width: 100% !important; } -.sil-logo a:hover { - color: white; - text-decoration: underline; +.kmw-osk-inner-frame { + font-size: 21px !important; + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; } -.sil-logo { - text-align: center; - margin-top: 20px; +#kmw-osk-help-page > table > tbody > tr > td > .row { + --bs-gutter-x: 0 !important; } -.sil-logo p{ - color: #fff; - font-size: 12pt; - margin-top: 10px; +.kmw-title-bar { + background-color: #2EADD3 !important; + border-radius: 0 !important; + height: fit-content !important; } -/* -=================== -==Extras== -=================== -*/ - -#fblike{ -position: absolute; -left: 50%; -margin-left: 320px; - } - -nav ul { - list-style: none outside none; - width:90%; - margin:0px auto; - padding:15px 0px; - } - -nav li { - display:inline; - padding:20px 39px; - } - -nav a { - color:#eee; - font-family:Tahoma; - font-weight:bold; - font-size:10.7px; - } - -.messageBox { - -webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; - -moz-box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - position: fixed; - left: 50%; - top: 25%; - z-index: 10002; - margin-left: -250px; - margin-top: -50px; - width: 500px; - height: 200px; +#keyboard_europeanlatin_help p, .kmw-osk-static p { + display: flex !important; + justify-content: end !important; + position: relative !important; + right: 0 !important; + top: 0 !important; } -.messageBox div { - width: 464px; - height: 134px; - left: 10px; - top: 10px; - padding: 8px; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; - background: white; +.kmw-key-square { + z-index: 0 !important; } -.messageBox button { - width:83px; - position: absolute; - right: 10px; - bottom: 4px; - background: #414141; - border-top: 1px solid #383838; - border-right: 1px solid #1F1F1F; - border-bottom: 1px solid #151515; - border-left: 1px solid #1F1F1F; - -webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; - -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - color: white; - font: bold 12px Helvetica, Verdana, Arial, sans-serif; - text-shadow: 0px -1px 1px #1E2D4D; - text-align:center; - margin: 0 auto 6px; - padding: 3px; - } - -.messageBox button:disabled { - cursor: default !important; +.kmw-spacebar-caption { color: gray !important; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; -} - -.messageBox button:hover { --webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; - -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - cursor: pointer; } -.messageBox button:active { - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; -} - -.progressBox { --webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; - -moz-box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 3px 3px 3px 0px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - position: fixed; - left: 50%; - top: 25%; - margin-left: -200px; - width: 400px; - height: 54px; -} - -.progressBox div { - width: 364px; - height: 18px; - left: 10px; - top: 10px; - padding: 8px; - background: url('../img/wait.gif') no-repeat 354px 8px white; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; -} - -#footer-logo{ - margin-top: 10px; -} - -#content {width: 960px;} - -/* -====================== -==Keyboard Styling== -====================== -*/ - .desktop .kmw-osk-frame, .desktop .kmw-osk-inner-frame, .desktop .kmw-key-layer-group { background-color: #95C6D6 !important; } @@ -710,7 +213,7 @@ nav a { .desktop .kmw-key-label { top: 5%; } .desktop .kmw-key-label { left: 10%; } .desktop .kmw-key-default .kmw-key-text { top: 25%; } - .desktop .kmw-footer, .desktop .kmw-title-bar { background: #2EADD3 !important; } + .desktop .kmw-footer, .desktop .kmw-title-bar { background-color: #2EADD3 !important; display: none;} .desktop .kmw-title-bar div{min-height: 100%;} .desktop .kmw-footer-caption a { color: white !important; } .kmw-osk-frame, .kmw-osk-inner-frame { border-color: #2EADD3 !important; } @@ -733,58 +236,6 @@ nav a { text-align: center; } -#keyman-desktop-download { - display: block; - text-align: center; - margin-top: 8px; -} - -#free-open-source { - color: #606060; - font-size: 8pt; - font-weight: normal; - display: block; - text-align: center; - -} - -#close { - float: right; - cursor: pointer; - position: relative; - top: -10px; -} - -#bookmarklet.hidden { - display: none; -} - -#bookmarklet div { - text-align: center; - margin: 16px 0 2px; -} - -#bookmarklet p { - font-size: 9px; - text-align: justify; -} - -#learn div { - margin: 0; - padding-bottom: 12px; -} - -@media all and (min-width: 1320px) { - ins.adsbygoogle { - display: inline-block !important; - height: 600px; - position: absolute; - right: 16px; - top: 120px; - width: 160px; - } -} - .desktop .kmw-keyboard-dari_clra .kmw-key-default .kmw-key-text { top: -33%; } diff --git a/cdn/dev/css/kmw.css b/cdn/dev/css/kmw-global.css similarity index 58% rename from cdn/dev/css/kmw.css rename to cdn/dev/css/kmw-global.css index 02c9c07..50c9ca1 100644 --- a/cdn/dev/css/kmw.css +++ b/cdn/dev/css/kmw-global.css @@ -1,8 +1,33 @@ /* ============= -==Reset CSS== +==Reset & Global CSS== ============= */ +:root { + --textarea-box: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgb(209, 213, 219) 0px 0px 0px 1px inset; + --textarea-width: 100vw; + --textarea-height: calc(100vh - 55vh); + --divider-height: 5vh; + --keyboard-height: calc(97vh - (var(--textarea-height) + var(--divider-height))); + --box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; + --keyman-orange: #FC7200; + --keyman-red: #B92034; + --keyman-blue: #2EADD3; + --tablet--kb-bg-color: #666; + --mobile-font-size: 12px; + --mobile-search-max-width: 100%; +} + +* { + box-sizing: border-box; + letter-spacing: .05rem; + font-weight: 400; +} + +body { + overflow: hidden; +} + html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, @@ -24,11 +49,12 @@ time, mark, audio, video { } a { - margin:0; - padding:0; + margin:0 !important; + padding:0 !important; vertical-align:baseline; background:transparent; - } + text-decoration: none !important; +} article,aside,details,figcaption,figure, section,header,footer,hgroup,menu,nav { @@ -65,6 +91,7 @@ hr { border:0; border-top:1px solid #cccccc; padding:0; + margin: 0; } button, input, select { @@ -76,3 +103,12 @@ button, input, select { ins.adsbygoogle { display: none !important; } + +.hidden { + display: none !important; +} + +/* Icons */ +i:hover { + transition: all 0.3s ease; +} diff --git a/cdn/dev/css/kmw-header.css b/cdn/dev/css/kmw-header.css new file mode 100644 index 0000000..5732306 --- /dev/null +++ b/cdn/dev/css/kmw-header.css @@ -0,0 +1,1166 @@ +/* + Name: KeymanWeb's Header Styles + Copyright: Copyright (C) 2025 SIL Global + Documentation: + Description: All of the styles for KMW's Header + Create Date: 5 November 2024 + + Modified Date: + Authors: Meng-Heng + + History: +*/ + +header > div > img { + width: 100%; + display: block; + height: 5px; +} + +.main-header { + display: grid; + grid-template-columns: 1fr 1fr; + height: 75px; + padding: 0 10px; + box-shadow: var(--box-shadow); +} +/* +============================================================= +==Left side of Header: Search Box, Keyboard Selection Menu== +============================================================= +*/ + +#searchDropdownMenu { + padding: 10px; + margin: 0; +} + +.left-header { + display: flex; + align-items: center; + width: 70vw; +} + +.left-header img { + margin-right: 10px; +} + +/* +============== +==Search Box== +============== +*/ +#searchBar { + position: relative; + width: 550px; +} + +#searchBar #searchInput { + height: 50px; + width: 100%; + text-indent: 10px; + border-radius: 16px; + text-align: left; + font-family: inherit !important; + box-shadow: none; +} + +#searchInput:hover, #searchInput:focus { + border: 1px solid var(--keyman-orange); +} + +#searchIcons { + position: absolute; + right: 5px; + top: 12px; + border-left: 1px solid gray; + background-color: white; +} + +#searchIcons > * { + margin-right: 10px; + padding-left: 15px; + opacity: 50%; +} + +#searchIcons > *:hover { + padding-left: 15px; + opacity: 100%; +} + +#magnifyingGlassIcon { + display: inline; +} + +#clearSearchIcon { + display: none; +} + +/* +========================== +== SEARCH DROPDOWN BASE == +========================== +*/ + +#searchDropdownMenu { + max-height: 88vh; + width: 550px; + overflow-y: auto; + padding: 1.5rem; + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0,0,0,0.08); +} + +/* +==================== +== SEARCH HEADING == +==================== +*/ + +.search-result-heading { + display: flex; + align-items: center; + margin-bottom: 1.75rem; +} + +.search-result-heading hr { + flex: 1; + border: none; + border-top: 2px solid black; + opacity: 50%; +} + +.keyboard-title { + margin: 0; + white-space: nowrap; + font-size: 20px; + font-weight: 500; + color: var(--keyman-orange); + text-align: center; +} + +/* +================ +== SEARCH LIST == +================ +*/ + +#kbSearchCardUI { + display: flex !important; + flex-direction: column; + gap: 1.5rem; +} + +/* +================ +== SEARCH CARD == +================ +*/ + +.card-wrap { + position: relative; + padding: 20px; + border-radius: 16px; + border: 1px solid #8d8d8d; + transition: + border-color 0.25s ease, + transform 0.25s ease, + box-shadow 0.25s ease; + overflow: hidden; + width: 100%; +} + +.card-wrap:hover { + transform: translateY(-2px); + border: 1px solid var(--keyman-orange); + cursor: pointer; +} + +/* +===================== +== ACTIVE SELECTION == +===================== +*/ + +.card-wrap.selected { + border-color: var(--keyman-orange); +} + +.selection-indicator.selected { + background: var(--keyman-orange); + color: white; + opacity: 100%; + border: none; +} + +/* +================ +== CARD HEADER == +================ +*/ + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.card-header-title { + display: flex; + flex-direction: column; + gap: 0.4rem; + cursor: pointer; + font-size: 20px; +} + +.card-header-title.selected { + color: var(--keyman-orange); +} + +/* +=================== +== KEYBOARD TITLE == +=================== +*/ + +.card-header h4 { + margin: 0; + font-size: 18px; + font-weight: 500; + color: black; + line-height: 1.1; +} + +.card-wrap.selected .card-header h4 { + color: var(--keyman-orange); +} + +.card-header h4 span { + color: black; + font-size: 14px; +} + +/* +===================== +== HEADER META ROW == +===================== +*/ + +.card-sub-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + margin: 0.5rem 0 1rem 0; +} + +.card-sub-header > * { + opacity: 75%; + font-size: 10px; + text-align: center; + margin-bottom: 0; +} + +.dot-spacing { + width: 3px; + height: 3px; + border-radius: 3px; + background-color: black; + opacity: 75%; +} + +.keyboard-id { + color: black; +} + +.monthly-download { + color: #078510; +} + +/* +================= +== ACTION ICONS == +================= +*/ + +.card-header-icon { + display: flex; + align-items: center; + text-align: center; + gap: 10px; +} + +.card-header-icon i { + width: 20px; + height: 20px; + font-size: 10px; + align-content: center; + border: 1px solid black; + border-radius: 20px; + background: transparent; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +#kbHelpIcon:hover, #kbDownloadIcon:hover { + color: var(--keyman-orange); + border: 1px solid var(--keyman-orange); +} + +/* +================= +== DESCRIPTION == +================= +*/ + +.keyboard-description { + font-size: 12px; + line-height: 1.45; + color: gray; + opacity: 75%; +} + +.keyboard-description mark { + background: #ffe08a; + padding: 0 4px; + border-radius: 4px; +} + +.selection-indicator { + width: 24px; + height: 24px; + border: 1px solid black; + opacity: 50%; + border-radius: 20px 0 10px 0; + position: absolute; + right: 0; + text-align: center; +} + +.keyboard-description:hover { + opacity: 100%; +} + +/* +================= +== SHOW MORE == +================= +*/ + +.keyboard-description a { + color: var(--keyman-blue); + text-decoration: none; + font-weight: 500; +} + +.keyboard-description a:hover { + text-decoration: underline; +} + +/* +================ +== PAGINATION == +================ +*/ + +#paginationControls { + margin-top: 2rem; + + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +} + +#paginationControls button { + width: 42px; + height: 42px; + + border-radius: 50%; + + border: 2px solid #999; + background: white; + + cursor: pointer; +} + +#paginationControls button:hover:not(:disabled) { + border-color: var(--keyman-orange); +} + +#paginationControls button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* +================== +== INSTRUCTION == +================== +*/ + +.search-instruction { + background: white; + border-radius: 20px; + padding: 1.5rem; + margin-bottom: 1.5rem; + border: 2px solid #e5e5e5; +} + +.instruction-title { + display: flex; + justify-content: space-between; + align-items: center; + + margin-bottom: 1rem; +} + +.instruction-title p { + margin: 0; + + font-size: 1.2rem; + font-weight: 600; +} + +#closeInstruction { + cursor: pointer; + font-style: normal; + + width: 28px; + height: 28px; + + display: flex; + align-items: center; + justify-content: center; + + border-radius: 50%; + + border: 1px solid #999; +} + +/* +================== +== RESPONSIVE == +================== +*/ + +@media (max-width: 768px) { + + #searchDropdownMenu { + padding: 1rem; + } + + .card-wrap { + padding: 1.5rem; + } + + .card-header { + flex-direction: column; + gap: 1rem; + } + + .card-actions { + align-self: flex-end; + } + + .card-header h4 { + font-size: 1.5rem; + } + + .keyboard-title { + font-size: 1.5rem; + } +} +/* +============== +==Pagination== +============== +*/ +#searchDropdownMenu .bottom-row-search { + background-color: white; + align-items: center; + justify-content: center; + width: fit-content; + border-radius: 10px; + width: 100%; + bottom: 0; + text-align: center; + box-shadow: rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px; +} + +.bottom-row-search > * { + margin: 10px; + cursor: pointer; +} + +#paginationControls { + display: none; +} +/* +=========================== +==Selected Keyboard Menu== +=========================== +*/ + +.keyboard-container-dropdown-menu { + display: flex; + flex-direction: row; + width: 50%; + gap: 20px; + margin: 10px; +} + +.keyboard-container-visible { + display: flex; + flex-direction: row; + width: 500px; +} + +.kb-item-keyboard { + position: relative; + border-radius: 10px; + border: 1px solid black; + padding: 10px 20px; + margin-right: 10px; + width: max-content; + overflow: hidden; +} + +.kb-item-keyboard:hover { + cursor: pointer; +} + +.kb-item-keyboard-selected { + border: var(--keyman-orange) solid 1px; +} + +.kb-item-keyboard.selected { + border-color: var(--keyman-orange); +} + +.kb-chip-remove { + position: absolute; + top: -5px; + right: 0; + border: none; + background-color: transparent; +} + +.kb-chip-selected { + display: none; +} + +.kb-item-keyboard.selected .kb-chip-selected { + display: flex; +} + +.keyboard-overflow-selection > * { + border: none; +} + +.btn-keyman-orange { + background-color: var(--keyman-orange) !important; + color: white !important; +} + +.platform span { + background-repeat: no-repeat; + padding-left: 24px; + background-size: 18px; + display: block; + margin: 4px 0; +} + +.platform span:hover, .keyboard-configs li:hover{ + cursor: help; +} + +.platform .platform-windows { + background-image: url('/cdn/dev/img/platforms/icon-windows.png'); +} + +.platform .platform-macos { + background-image: url('/cdn/dev/img/platforms/icon-macos.png'); +} + +.platform .platform-linux { + background-image: url('/cdn/dev/img/platforms/icon-linux.png'); +} + +.platform .platform-android { + background-image: url('/cdn/dev/img/platforms/icon-android.png'); +} + +.platform .platform-ios { + background-image: url('/cdn/dev/img/platforms/icon-ios.png'); +} + +.platform .platform-desktopweb { + background-image: url('/cdn/dev/img/platforms/icon-desktopWeb.png'); +} + +.platform .platform-mobileweb { + background-image: url('/cdn/dev/img/platforms/icon-mobileWeb.png'); +} +/* +========================================= +==Font size slider + Hide/show keyboard== +========================================= +*/ +.tool-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.font-size-container { + display: flex; + flex-direction: row; + align-items: center; + padding: 5px 5px; + height: fit-content; +} + +.font-size-slider { + display: flex; + flex-direction: column; + justify-content: end; + align-items: center; + height: 60px; +} + +#fontSizeRange[type=range] { + -webkit-appearance: none; + margin: 10px 0; + width: 100%; +} + +.font-large { + font-size: 20px; +} + +input[type="range"] { + width: 100px; + background-color: transparent; +} + +input[type="range"]::-webkit-slider-runnable-track { + height: 3px; + background: rgb(107, 107, 107); + border-radius: 10px; +} + +input[type="range"]::-webkit-slider-thumb { + border-radius: 50%; + height: 20px; + width: 20px; + position: relative; + bottom: 6px; + background-size: 50%; + box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.4); + cursor: grab; +} + +#fontSizeRange[type=range]::-webkit-slider-runnable-track { + width: 100%; + cursor: pointer; + transform: 0.2s; + box-shadow: var(--box-shadow); + background: gray; + border-radius: 25px; +} + +#fontSizeRange[type=range]::-webkit-slider-thumb { + height: 10px; + width: 20px; + border-radius: 7px; + background: var(--keyman-blue); + cursor: pointer; + -webkit-appearance: none; + box-shadow: 0px 0px 5px rgb(143, 143, 143); + margin-top: 3px; +} + +/* +=================================================================================== +==Right side of Header: Font Resizer, Hide keyboard + Hamburger menu== +=================================================================================== +*/ +.right-header { + display: grid; + grid-template-columns: 80% 20%; + width: 100%; + justify-items: right; + align-items: center; + margin-right: 0; +} + +.right-header i { + font-size: 24px; + opacity: 50%; +} + +.right-header i:hover { + opacity: 100%; + cursor: pointer; + color: var(--keyman-blue); +} + +/* ========================= + Dropdown Container +========================= */ +#burgerDropDownMenu { + width: 500px; + height: 90vh; + padding-top: 10px; + border-radius: 10px; + border: 1px solid #dcdcdc; + overflow: scroll; + scroll-behavior: smooth; + background: #ffffff; + box-shadow: 0 12px 40px rgba(0,0,0,0.08); +} + +/* ========================= + Wrapper +========================= */ + +.menu-wrapper { + display: flex; + flex-direction: column; + gap: 20px; + padding: 18px; +} + +/* ========================= + Section +========================= */ + +.menu-section { + display: flex; + flex-direction: column; + gap: 12px; + margin: 0 10px; +} + +.menu-section-title { + font-size: 12px; + color: #000000; + opacity: 50%; + margin: 0; +} + +.menu-card-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* ========================= + Cards +========================= */ + +.menu-card { + display: flex; + align-items: flex-start; + gap: 14px; + + padding: 15px !important; + border-radius: 10px; + + background: #ffffff; + border: 1px solid #dddddd; + + text-decoration: none; + color: inherit; + + transition: + transform 0.2s ease, + background 0.2s ease, + border-color 0.2s ease; +} + +.menu-card:hover { + background: #ffffff; + border-color: var(--keyman-orange); + transform: translateY(-1px); + opacity: 100% !important; +} + +.primary-card { + align-items: center; +} + +/* ========================= + Card Icon +========================= */ + +.menu-card-icon { + width: 28px; + height: 28px; + min-width: 20px; + border-radius: 50%; + background: #ff6b00; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 1px; +} + +.menu-card-icon i { + font-size: 18px !important; + opacity: 100%; +} + +.menu-card-icon img { + width: 20px; + height: 20px; + object-fit: contain; +} + +/* ========================= + Card Content +========================= */ + +.menu-card-content { + flex: 1; +} + +.menu-card-content h4 { + margin: 0; + height: 24px; + font-size: 14px; + color: #000000; + align-content: center; +} + +.menu-card-content p { + margin: 10px 0 0; + font-size: 12px; + line-height: 1.5; + color: #000000; + opacity: 50%; +} + +/* ========================= + Card Arrow +========================= */ + +.menu-card-arrow { + display: flex; + align-items: center; + justify-content: center; + color: #000000; +} + +.menu-card-arrow i { + font-size: 16px !important; + opacity: 100% !important; +} + +.menu-card-icon-keyman { + background-color: white; + border: 1px solid var(--keyman-orange) +} + +/* ========================= + Footer +========================= */ + +.menu-footer { + border-top: 1px solid #dddddd; + border-radius: 10px; + padding-top: 18px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +/* ========================= + Socials +========================= */ + +.menu-socials { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} + +.menu-socials a { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--keyman-blue); + color: white; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + transition: + transform 0.2s ease, + opacity 0.2s ease; +} + +.menu-socials a:hover { + transform: translateY(-2px); + background-color: transparent; + border: 1px solid var(--keyman-blue); + color: var(--keyman-blue); +} + +.menu-socials a i { + opacity: 100% !important; + font-size: 16px !important; +} +/* ========================= + SIL Logo +========================= */ + +.menu-sil { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 20px; +} + +.menu-sil img { + width: 140px; + height: auto; + object-fit: contain; +} + +.menu-sil p { + margin: 0; + font-size: 13px; + font-weight: 400; +} + +.menu-sil a { + color: black; +} + +/* ========================= + Version +========================= */ + +.menu-version { + display: flex; + gap: 8px; + font-size: 12px; + color: #000000; + opacity: 50%; + flex-wrap: wrap; + justify-content: center; +} + +.menu-version a { + color: inherit; + text-decoration: underline !important; +} + +.menu-version a:hover { + text-decoration: none !important; +} + +/* ========================= + Burger Button +========================= */ + +.burger-trigger { + border: none; + background: transparent; + box-shadow: none; +} + +.burger-trigger i { + font-size: 20px; + color: #2b2b2b; +} + +/* ========================= + Responsive +========================= */ + +@media (max-width: 480px) { + #burgerDropDownMenu { + width: min(92vw, 340px); + } + + .menu-wrapper { + padding: 14px; + } + + .menu-card { + padding: 12px; + } +} + +@font-face { + font-family: "social-logos"; + src: url("../../fonts/social-logos.ttf"); + font-weight: normal; + font-style: normal; +} + +[data-icon]:before { + font-family: "social-logos" !important; + content: attr(data-icon); + padding-right: 4px; + font-size: 16px; +} + +.kmw-version { + display: flex; + flex-direction: row; + justify-content: center; +} + +.sil-logo { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; +} +/* +================================ +==Limits of Keyboard Selection== +================================ +*/ +.warning-container { + background: rgba(0, 0, 0, 0.25); + position: fixed; + height: 100vh; + width: 100%; + z-index: 1000; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.warning-content { + width: 500px; + display: flex; + flex-direction: column; + align-items: center; + background-color: white; + color: black; + text-align: center; + padding: 30px 30px 70px; + border-radius: 20px; + box-shadow: var(--box-shadow); +} + +.warning-cancel-btn { + width: 30px; + font-size: 20px; + opacity: 50%; + align-self: flex-end; + background-color: transparent; + border: none; + margin-bottom: 10px; +} + +.warning-keyman-image { + width: 82px; + margin-bottom: 15px; +} + +.warning-text { + margin-bottom: 40px; + font-size: 18px; +} + +.warning-accept-btn { + background-color: var(--keyman-orange); + border: none; + border-radius: 5px; + width: 200px; + padding: 14px; + color: white; + box-shadow: var(--box-shadow); +} +/* +=============================== +==DELETE: Confirmation Dialog== +=============================== +*/ +.dialog-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.25); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.dialog-box { + background: #fff; + border-radius: 12px; + padding: 2rem; + width: 90%; + max-width: 400px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + text-align: center; +} + +.dialog-icon { + font-size: 2rem; + color: #e74c3c; + margin-bottom: 1rem; +} + +.dialog-title { + font-size: 1.25rem; + font-weight: 600; + margin: 0.5rem 0; +} + +.dialog-message { + font-size: 0.95rem; + color: #555; + margin-bottom: 1.5rem; +} + +.dialog-buttons { + display: flex; + justify-content: space-around; + gap: 1rem; +} + +.btn-cancel, .btn-delete { + flex: 1; + padding: 0.75rem; + border-radius: 8px; + border: none; + cursor: pointer; + font-size: 1rem; +} + +.btn-cancel { + background: #f4f4f4; + color: #333; +} + +.btn-delete { + background: var(--keyman-red); + color: white; +} + +.btn-cancel:hover { + background: #eaeaea; +} + +.btn-delete:hover { + background: #d54335; +} diff --git a/cdn/dev/css/kmw-interaction.css b/cdn/dev/css/kmw-interaction.css new file mode 100644 index 0000000..d50a438 --- /dev/null +++ b/cdn/dev/css/kmw-interaction.css @@ -0,0 +1,62 @@ +/* + Name: KeymanWeb Interaction Styles + Copyright: Copyright (C) 2025 SIL Global + Documentation: + Description: Animation & Styles of every interactable components. + Create Date: 19 Nov 2025 + + Modified Date: + Authors: Meng-Heng + + History: +*/ + +/* Warning before delete */ +.warning-content { + animation: fadeIn 0.2s ease; +} + +.dialog-box { + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +/* Enable keyboard for Keyboard Selection Animation */ +.kb-icon-plus-animate { + animation: plustoTickToMinus 2s forwards; +} + +@keyframes plustoTickToMinus { + 0% { + content: "+"; + transform: scale(1); + } + 30% { + color: lime; + content: "✓"; + transform: scale(1.5); + } + 60% { + color: lime; + content: "✓"; + transform: scale(1.3); + } + 100% { + content: "-"; + transform: scale(1); + } +} + +/* Textarea */ +.textarea-animation { + animation: highlight 3s ease; +} + +@keyframes highlight { + from {box-shadow:#2EADD3 0px 0px 0px 3px;} + to {box-shadow: var(--textarea-box);} +} \ No newline at end of file diff --git a/cdn/dev/css/kmw-mobile.css b/cdn/dev/css/kmw-mobile.css index 95ce082..82666a5 100644 --- a/cdn/dev/css/kmw-mobile.css +++ b/cdn/dev/css/kmw-mobile.css @@ -6,23 +6,20 @@ ====================== */ -html { - overflow-y: scroll; - overflow-x: hidden; -} - body { - width:100%;height:100%; + width:100%; + height:100%; font-family: Verdana, Arial, Helvetica, sans-serif; color:#2D2C2C; background-color: #eeeeee; margin:0px; - } + overflow: none; +} a { text-decoration:none; color: #AD4A28; - } +} a:hover { text-decoration:underline; @@ -37,368 +34,177 @@ p { line-height:1.5em; } /* -header, #content, footer { - margin:0px auto; - clear: both; - } -*/ - -hr { - display: none; - } - -/* -.box { - -webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; - -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); - } -*/ -/* -=================== -==Header Elements== -=================== +========== +==Header== +========== */ -header img, #headerLeft { - display: none; -} -header { - position: absolute; - top: 190px; - background: #776666; - font-size: 9pt; - padding: 6px 0; - color: white; +.main-header { width: 100%; - text-align: center; -} -header a { - color: #c0c0ff; - text-decoration: underline; + grid-template-columns: 2fr 1fr; } -/* -==================== -==Content Elements== -==================== -*/ -#content { - width: 100%; /* specific for mobile and tablet */ - margin: 0px auto; - background-color: #eeeeee; - /* min-height:375px; */ - } - - -/* KMW Main App */ -#app { - width:100%; - margin: 0px; - } -#KeymanWebControl { +.left-header img { display: none; - } - -#exampleBox,#example { - display:none; - } - -#messageContainer { - width: 80%; - float:left; - height: 50%; - } - -#message { - height:144px; - width: 100%;/*554px;*/ - float:left; - margin: 4px; - color:black; - font-size:17px; - font-family:SindhiWeb,Verdana,GeezWeb,LaoWeb,TibetanWeb,MyanmarWeb,SinhalaWeb,TamilWeb,KhmerWeb,LatinWeb,OriyaWeb,EgyptianWeb !important; - } - - #font-size{ - display: none; - } - -/* KMW App Buttons */ +} -#buttons { - float: right; - width: 40px; - margin: 8px 15px 8px 0px; - border: none; +.right-header { + grid-template-columns: 50% 50%; + padding: 10px; } -#buttons div { - /*width:50px;*/ - -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; - background: #eeeeee; - border: none; - margin: 0px 0px 4px 10px; - height:40px; - width: 41px; - padding: 0px; - padding-top: 2px; - background-repeat: no-repeat; - background-position: center center; - background-size: 34px 34px; - } +.tool-container { + margin: 10px; +} -#font{ - text-align: center; +.hamburger-menu-container { + overflow: scroll; + scroll-behavior: smooth; + height: 70vh; } -#font p{ - display: none; +#burgereMenu ul { + width: 80vw; + scroll-behavior: smooth; + flex-direction: column; + height: 60vh; + overflow: hidden; } -#buttons div:disabled { - cursor: default !important; - color: gray !important; - /*border-color: gray !important; */ - } +.burger-menu-external-links{ + display: flex !important; + flex-direction: column !important; +} -#search,#search.links{ - background-image:url('../img/btn_google.png'); +.kmw-socials-icons { + display: flex; + flex-direction: column; +} +/* +======================= +==Language Search box== +======================= +*/ +#searchBar { + width: 50%; +} +#searchInput::placeholder { + opacity: 0; } -#search.linksOff{ - background-image:url('../img/btn_google_white.png'); +#searchIcons { + border: 0; + background-color: transparent; + z-index: 0; + user-select: none; } -#copy,#copy.links{ - background-image:url('../img/btn_copy.png'); +#searchInput:focus, #keyboardSelection:active { + width: var(--mobile-search-max-width) !important; +} +#searchDropdownMenu { + width: 95vw !important; } -#copy.linksOff{ - background-image:url('../img/btn_copy_white.png'); +.textarea-container { + height: 100%; } -#buttons div * { - display:inline; - } +.mobile-font-size-container { + display: flex; + flex-direction: row; + position: absolute; + right: 50px; + top: 80px; + width: 150px; + background: white; + border: 1px solid black; + padding: 10px; + border-radius: 10px; +} -#buttons p { /* buttons have no text label on mobiles */ - display:none; - } +.large-icon-tools, .small-icon-tools { + display: none; +} -div.links { - cursor:pointer; - } +#keyboardSelection.open { + width: 70vw !important; +} +/* +==================== +==Content Elements== +==================== +*/ -div.linksOff { - cursor:default !important; - border:none; - /*border: 1px solid #cccccc !important; */ +/* KMW Main App */ +#app { + width: 100%; + margin: 0px; } -div.linksOff * { - color: gray; - -webkit-user-select:none; +#KeymanWebControl { + display: none; } -#mobile-font{ +.kmw-tools { display: none; - width: 40px; - border-radius: 4px; - position: absolute; - left: 100%; - top: 60px; - margin-left: -45px; - text-align: center; - font-size: 24pt; - background: #fff; } -#mobile-increase{ - height: 40px; - line-height: 40px; - border-bottom: solid 1px #000; +/* Divider */ +.divider-container { + cursor: pointer; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 100%; + align-items: center; + grid-template-areas: "left middle right"; + width: 100%; + height: 5vh; + padding: 10px; + border-radius: 5px; + box-shadow: var(--box-shadow); } -#mobile-decrease{ - height: 40px; - line-height: 40px; +.left-divider { + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + height: 100%; } -/* Aside Elements */ -aside, aside div { - display:none; - } - -/* -=================== -==Footer Elements== -=================== -*/ -.footer,footer { - display:none; - } - -#mobile-footer { - position: fixed; - bottom: 0; +.left-divider > * { + margin: 0 10px; +} +/* Keyboard */ +#exampleBox { + background-color: gray; + height: 10vh; width: 100%; } -#mobile-footer #mobile-version { - font-size: 7pt; - text-align: center; +.keyboard-container { width: 100%; + height: 100% !important; } -.messageBox { - box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -o-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -khtml-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - margin-left: 10%; - margin-top: 10%; - width: 80%; - height: 150px; - position: fixed; -} - -.messageBox div { - width: 85%; - height: 84px; - left: 5%; - top: 10px; - padding: 8px; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; - background: white; +.desktop-keyboard { + display: none; } -.messageBox button { - width:83px; - position: absolute; - right: 10px; - bottom: 4px; - background: #414141; - border-top: 1px solid #383838; - border-right: 1px solid #1F1F1F; - border-bottom: 1px solid #151515; - border-left: 1px solid #1F1F1F; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - color: white; - font: bold 12px Helvetica, Verdana, Arial, sans-serif; - text-shadow: 0px -1px 1px #1E2D4D; - text-align:center; - margin: 0 auto 6px; - padding: 3px; - } - -.messageBox button:disabled { - cursor: default !important; - color: gray !important; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; -} - -.messageBox button:hover { - -webkit-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -moz-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -ms-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -o-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - cursor: pointer; -} - -.messageBox button:active { - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; -} - -.progressBox { - box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -o-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -khtml-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - top: 10%; - margin-left: 10%; - width: 80%; - height: 70px; - position: fixed; -} - -.progressBox div { - width: 85%; - height: 34px; - left: 5%; - top: 10px; - padding: 8px; - background: url('/img/wait.gif') no-repeat 354px 8px white; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; +.phone-keyboard { + width: 100%; + display: block; } - /* Additional rules for portrait orientation */ -@media only screen and (orientation: portrait) { - - #message { - height: 170px; - } - - #messageContainer { - float: left; - width: 80%; - height: 40%; - } +.keyboard-download-box { + display: none; } - - /* Additional rules for landscape orientation */ -@media only screen and (orientation: landscape) { - - #messageContainer { - float: left; - width: 85%; - height: 50%; - } - #message { - height: 50%; - } - #buttons { - margin: 4px 15px 4px 0; - } - - #font{ - display: none; - } +.spacing-purpose { + display: none; } - /* ====================== ==Keyboard Styling== @@ -413,8 +219,68 @@ aside, aside div { background-color: gray !important; } - .phone .kmw-keyboard-dari_clra .kmw-key-default .kmw-key-text { top: -33%; font-size: 0.8em !important; } + +.keyboard-area { + border: none; +} + +/* +============ +==Viewport== +============ +*/ +/* Additional rules for portrait orientation */ +@media only screen and (orientation: portrait) { + .keyboard-container > * { + width: 100%; + height: 100%; + } +} + + /* Additional rules for landscape orientation */ +@media only screen and (orientation: landscape) { + body { + overflow: auto; + } + + .main-header { + height: 65px; + } + + .textarea-container { + height: 15vh; + margin-bottom: 10px; + } + + .divider-container { + height: 10vh; + } + + .middle-divider { + align-content: center; + font-size: 20px; + } + + #example { + font-size: 10px; + } + + .phone.android.kmw-osk-frame > * { + height: 100% !important; + } + + .keyboard-container { + width: 100vw; + height: 100% !important; + } + + .keyboard-area { + border: none; + width: 100%; + height: 100%; + } +} diff --git a/cdn/dev/css/kmw-screen.css b/cdn/dev/css/kmw-screen.css new file mode 100644 index 0000000..61ee41d --- /dev/null +++ b/cdn/dev/css/kmw-screen.css @@ -0,0 +1,71 @@ +/* + Name: KeymanWeb Media Screen Styles + Copyright: Copyright (C) 2025 SIL Global + Documentation: + Description: Different media screens to allow components to display uniquely from the index.php. + Create Date: 19 Nov 2025 + + Modified Date: + Authors: Meng-Heng + + History: +*/ +@media only screen and (max-width: 632px) { + /* Smaller Search Box */ + #searchBar #searchInput { + min-width: 50px; + } +} + +@media only screen and (max-width: 456px) { + /* Smaller Keyboard list in Selection Menu */ + .kb-item-keyboard { + min-height: fit-content; + } + + .kb-item-keyboard span { + min-width: 112px; + } + + .kb-item-keyboard { + overflow-x: scroll; + } +} + +@media screen and (width < 565px) { + /* Show Mobile's font resizer */ + .font-size-mobile { + display: flex; + flex-direction: row; + align-items: center; + border-radius: 24px; + opacity: 50%; + } + + .font-size-mobile button { + border: 0; + background-color: #fff; + border-style: none; + } + + .font-size-mobile > *{ + margin: 0 5px; + } +} + +@media screen and (max-width: 400px) { + .example-box p { + font-size: 10px; + } +} + +@media all and (min-width: 1320px) { + ins.adsbygoogle { + display: inline-block !important; + height: 600px; + position: absolute; + right: 16px; + top: 120px; + width: 160px; + } +} diff --git a/cdn/dev/css/kmw-tablet.css b/cdn/dev/css/kmw-tablet.css index d48202d..a97b534 100644 --- a/cdn/dev/css/kmw-tablet.css +++ b/cdn/dev/css/kmw-tablet.css @@ -6,27 +6,23 @@ ====================== */ -html { - overflow-y: scroll; - overflow-x: hidden; -} - body { width:100%; + height:100%; font-family: Verdana, Arial, Helvetica, sans-serif; color:#2D2C2C; background-color: #eeeeee; margin:0px; - } +} a { text-decoration:none; color: #AD4A28; - } +} a:hover { text-decoration:underline; - } +} h3 { font-size:18px; @@ -43,398 +39,149 @@ header, footer { /*clear: both;*/ } -hr { - display: none; - } - .box { -webkit-border-radius: 8px;-moz-border-radius: 8px; border-radius: 8px; -moz-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); } -/* -=================== -==Header Elements== -=================== -*/ -header { - margin-top:0px; - } - -#headerLeft img { - display: block; /* default (inline) display inserts an unwanted bottom border */ - margin-top:0px; - height:60px; - /*-webkit-border-radius: 0px 0px 8px 8px; -moz-border-radius: 0px 0px 8px 8px; border-radius: 0px 0px 8px 8px;*/ - } - -header > div > img { - display: block; - width: 100%; - height: 8px; +.container-flex { + display: flex; + flex-direction: column; + height: 100%; } /* -==================== -==Content Elements== -==================== +========== +==Header== +========== */ -#content { - width: 100%; /* specific for tablet and mobile */ - margin: 0px auto; - background-color: #eeeeee; - /*min-height:375px;*/ - } - - -/* KMW Main App */ -#app { - /*float:left;*/ - /*height:330px;*/ - width:100%; /*710px; */ - margin: 0px;/*30px 15px 15px;*/ - } - -#KeymanWebControl { - display: none; - } - -#exampleBox, #example { - display: none; - } - -#messageContainer { - width: 80%; - float:left; - } - -#message { - height:195px; - width: 100%;/*554px;*/ - float:left; - margin: 10px; - color:black; - font-size:17px; - font-family:SindhiWeb,Verdana,GeezWeb,LaoWeb,TibetanWeb,MyanmarWeb,SinhalaWeb,TamilWeb,KhmerWeb,LatinWeb,OriyaWeb,EgyptianWeb !important; - } - -/* KMW App Buttons */ - -#buttons { - float: right; - width:15%; - margin: 0px 20px; - margin-top: -6px; - } - -#buttons div { - width:140px; - -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; - background: #fff; - border: 1px solid #888888; - margin: 0px 10px; - padding: 3px; - padding-top: 9px; - height: 41px; - line-height: 36px; - background-repeat: no-repeat; - background-position: center left; - } - -#buttons div:disabled { - cursor: default !important; - color: gray !important; - border-color: gray !important; - } - -#buttons p{ - margin-left: 41px; -} - -#search.links, #search{ - background-image:url('../img/btn_google_small.png'); - -} - -#search.linksOff{ - background-image:url('../img/btn_google_small_white.png'); +.main-header { + width: 100%; + grid-template-columns: 3fr 1fr; } -#copy.links, #copy{ - background-image:url('../img/btn_copy_small.png'); - +.right-header { + grid-template-columns: 70% 30%; } -#copy.linksOff{ - background-image:url('../img/btn_copy_small_white.png'); +#searchDropdownMenu { + width: 50vw; } -#buttons div * { - display:inline; - } - -div.links { - cursor:pointer; - } - -div.linksOff { - cursor:default !important; - border: 1px solid #cccccc !important; - } - -div.linksOff * { - color: gray; - -webkit-user-select:none; - } - -/* Aside Elements */ - -aside { - display:none; - } - -#font-size{ - display: none; +.burger-menu-external-links { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + padding: 0; + list-style-type: none; } /* - -#font, -=================== -==Footer Elements== -=================== +================== +==Tool Container== +================== */ -.footer, footer { - display: none; - background-color: #6A2915; - height:50px; - left:0px; - bottom:0px; - position: fixed; - /* -webkit-border-radius: 8px 8px 0px 0px; -moz-border-radius: 8px 8px 0px 0px; border-radius: 8px 8px 0px 0px; */ - } - -nav ul { - list-style: none outside none; - width:90%; - margin:0px auto; - padding:15px 0px; - } - -nav li { - display:inline; - padding:20px 39px; - } - -nav a { - color:#eee; - font-family:Tahoma; - font-weight:bold; - font-size:10.7px; - } - -.messageBox { - box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -o-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -khtml-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - position: fixed; - left: 50%; - top: 20%; - z-index: 999 !important; - margin-left: -250px; - margin-top: -100px; - width: 500px; - height: 200px; +.tool-menu button { + width: 120px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: 10px; + border: 1px solid gray; + opacity: 50%; + padding: 0 10px; } -.messageBox div { - width: 464px; - height: 134px; - left: 10px; - top: 10px; - padding: 8px; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; - background: white; +.tool-menu button:hover { + opacity: 100%; } -.messageBox button { - width:83px; - position: absolute; - right: 10px; - bottom: 4px; - background: #414141; - border-top: 1px solid #383838; - border-right: 1px solid #1F1F1F; - border-bottom: 1px solid #151515; - border-left: 1px solid #1F1F1F; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - color: white; - font: bold 12px Helvetica, Verdana, Arial, sans-serif; - text-shadow: 0px -1px 1px #1E2D4D; - text-align:center; - margin: 0 auto 6px; - padding: 3px; - } - -.messageBox button:disabled { - cursor: default !important; - color: gray !important; - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; - box-shadow: 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111 !important; +.tool-menu .dropdown-menu { + width: 25vw; + right: 30px; } -.messageBox button:hover { - -webkit-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -moz-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -ms-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - -o-box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - box-shadow: inset 0 0px 20px 1px #878787, 0px 1px 0 #1D1D1D, 0 2px 0px #1F1F1F, 0 2px 4px 1px #111; - cursor: pointer; +.tool-menu .dropdown-menu > * { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 50px; + padding: 10px; } -.messageBox button:active { - -webkit-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -moz-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -ms-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - -o-box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; - box-shadow: 0px 1px 0 #1D1D1D, 0 0px 0px #1F1F1F, 0 1px 4px 1px #111; +.tool-menu .dropdown-menu > div:hover { + color: var(--keyman-blue); + cursor: pointer; } -.progressBox { - box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -o-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - -khtml-box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.5); - background: #c4c4c4; - position: fixed; - left: 50%; - top: 20%; - z-index: 999; - margin-left: -200px; - margin-top: -54px; - width: 400px; - height: 54px; +#copyTool { + top: 110px; } -.progressBox div { - width: 364px; - height: 18px; - left: 10px; - top: 10px; - padding: 8px; - background: url('../img/wait.gif') no-repeat 354px 8px white; - font-size: 13px; - position: absolute; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 3px; +.copy-container span, .eraser-container span, .expand-container span { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; } -/* Elements modified for tablet */ -#headerBackground { - width: 100%; - background-color:white; +.copy-container span p, .eraser-container span p, .expand-container span p { + margin: 0; } - -#headerLeft { - float: left; - margin-left: 28px; +/* +========================= +==Section/Body Elements== +========================= +*/ +.example-box { + display: block; + width: 100vw; + height: 7vh; + user-select: none; + align-items: center; + position: sticky; + background-color: var(--tablet--kb-bg-color); } -#headerRight { - text-align: right; - height: 40px; - float: right; - padding-bottom: 33px; - padding-right: 10px; +.left-divider { + display: none; } -#headerRight-beta { - display: block; - font-weight: bold; - color: red; - font-size: 16pt; - padding: 16px 12px 0; +.right-divider { + display: none; } -#headerRight-link { - padding: 0 12px 0; +.tablet.kmw-osk-frame { + display: block !important; + position: absolute !important; + z-index: 1 !important; + height: 100% !important; + width: 100% !important; } - -#instructions { - margin:10px; - font:12pt Helvetica,Verdana,Arial,sans-serif; - color: white; +/* Keyboard */ +.keyboard-container { + display: flex; + width: 100%; } -/* Additional rules for portrait orientation */ -@media only screen and (orientation: landscape) { - #buttons{ - padding-top: 11px; - } - #buttons div { - margin: 4px 20px 10px 0px; - padding: 0px; - } - #font span:first-child{ - margin-left: 3px; - } - #font p{ - margin-left: 8px; - position: relative; - top: -5px; - } - #mobile-font{ - display: none; - width: 136px; - border-radius: 4px; - position: absolute; - left: 100%; - top: 136px; - margin-left: -170px; - text-align: center; - font-size: 24pt; - background: #fff; - } - - #mobile-increase{ - height: 40px; - line-height: 40px; - border-bottom: solid 1px #000; - } +.keyboard-area { + border: none; + width: 100%; + height: 100%; + overflow: hidden !important; + transition: transform 0.2s ease-out !important; + transform: translateY(0) !important; +} - #mobile-decrease{ - height: 40px; - line-height: 40px; - } +.keyboard-area.hidden { + transform: translateY(100%) !important; } /* Additional rules for portrait orientation */ @media only screen and (orientation: portrait) { - #app { width: 97%; } @@ -507,6 +254,12 @@ nav a { } } +@media only screen and (orientation: landscape) { + body { + overflow: auto; + } +} + /* ====================== ==Keyboard Styling== diff --git a/cdn/dev/img/instruction/instruction_card_1.png b/cdn/dev/img/instruction/instruction_card_1.png new file mode 100644 index 0000000..2779d51 Binary files /dev/null and b/cdn/dev/img/instruction/instruction_card_1.png differ diff --git a/cdn/dev/img/instruction/instruction_card_2.png b/cdn/dev/img/instruction/instruction_card_2.png new file mode 100644 index 0000000..659875e Binary files /dev/null and b/cdn/dev/img/instruction/instruction_card_2.png differ diff --git a/cdn/dev/img/instruction/instruction_card_3.png b/cdn/dev/img/instruction/instruction_card_3.png new file mode 100644 index 0000000..d0f98c1 Binary files /dev/null and b/cdn/dev/img/instruction/instruction_card_3.png differ diff --git a/cdn/dev/img/keymanweb-mini-logo-88 copy.png b/cdn/dev/img/keymanweb-mini-logo-88 copy.png new file mode 100644 index 0000000..c1b274e Binary files /dev/null and b/cdn/dev/img/keymanweb-mini-logo-88 copy.png differ diff --git a/cdn/dev/img/keymanweb-mini-logo-88.png b/cdn/dev/img/keymanweb-mini-logo-88.png new file mode 100644 index 0000000..c1b274e Binary files /dev/null and b/cdn/dev/img/keymanweb-mini-logo-88.png differ diff --git a/cdn/dev/img/platforms/icon-android.png b/cdn/dev/img/platforms/icon-android.png new file mode 100644 index 0000000..7cdf8b4 Binary files /dev/null and b/cdn/dev/img/platforms/icon-android.png differ diff --git a/cdn/dev/img/platforms/icon-desktopWeb.png b/cdn/dev/img/platforms/icon-desktopWeb.png new file mode 100644 index 0000000..457804b Binary files /dev/null and b/cdn/dev/img/platforms/icon-desktopWeb.png differ diff --git a/cdn/dev/img/platforms/icon-ios.png b/cdn/dev/img/platforms/icon-ios.png new file mode 100644 index 0000000..817edd6 Binary files /dev/null and b/cdn/dev/img/platforms/icon-ios.png differ diff --git a/cdn/dev/img/platforms/icon-linux.png b/cdn/dev/img/platforms/icon-linux.png new file mode 100644 index 0000000..1776e7f Binary files /dev/null and b/cdn/dev/img/platforms/icon-linux.png differ diff --git a/cdn/dev/img/platforms/icon-macos.png b/cdn/dev/img/platforms/icon-macos.png new file mode 100644 index 0000000..12bd6f6 Binary files /dev/null and b/cdn/dev/img/platforms/icon-macos.png differ diff --git a/cdn/dev/img/platforms/icon-mobileWeb.png b/cdn/dev/img/platforms/icon-mobileWeb.png new file mode 100644 index 0000000..16e6b54 Binary files /dev/null and b/cdn/dev/img/platforms/icon-mobileWeb.png differ diff --git a/cdn/dev/img/platforms/icon-windows.png b/cdn/dev/img/platforms/icon-windows.png new file mode 100644 index 0000000..97d76a5 Binary files /dev/null and b/cdn/dev/img/platforms/icon-windows.png differ diff --git a/cdn/dev/js/api.js b/cdn/dev/js/api.js new file mode 100644 index 0000000..e7b968f --- /dev/null +++ b/cdn/dev/js/api.js @@ -0,0 +1,6 @@ +export function getKeymanWeb() { + if(window.tavultesoft) { + return window.tavultesoft.keymanweb; + } + return window.keyman; +} \ No newline at end of file diff --git a/cdn/dev/js/feature/copy.js b/cdn/dev/js/feature/copy.js new file mode 100644 index 0000000..d08391a --- /dev/null +++ b/cdn/dev/js/feature/copy.js @@ -0,0 +1,36 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * element and interaction for the copy tool + */ +export function copyTool() { + // Copy tool + const copyDiv = document.querySelector('#copyTool') + const copyBtn = copyDiv.children[0] + + copyDiv?.addEventListener('click', async function() { + let textToCopy = textArea.value.trim() + + // Show X icon when there's no text to copy + if(!textToCopy) { + copyBtn.classList.replace('fa-copy', 'fa-xmark') + setTimeout(() => { + copyBtn.classList.replace('fa-xmark', 'fa-copy') + copyBtn.textContent = '' + }, 3000) + return; + } + + await navigator.clipboard.writeText(textToCopy) + + // Once copied, the icon shows a check mark + copyBtn.classList.replace('fa-copy', 'fa-check'); + + // After copied, the icon goes back to its default + setTimeout(() => { + copyBtn.classList.replace('fa-check', 'fa-copy'); + }, 1000); + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/eraser.js b/cdn/dev/js/feature/eraser.js new file mode 100644 index 0000000..8335ed3 --- /dev/null +++ b/cdn/dev/js/feature/eraser.js @@ -0,0 +1,96 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * element and interaction for the eraser tool + */ +export function eraserTool() { + const eraseDiv = document.querySelector('#eraseTool') + const eraseBtn = eraseDiv.children[0] + + eraseDiv?.addEventListener('click', function() { + let value = textArea.value.trim(); + + // Show X icon when there's no text to erase + if(!value) { + eraseBtn.classList.replace('fa-eraser', 'fa-xmark'); + setTimeout(() => { + eraseBtn.classList.replace('fa-xmark', 'fa-eraser'); + }, 3000); + return; + } else { + confirmAndClearText(() => { + // This function is passed as onConfirmErase + + textArea.value = ''; + // Once erased, the icon shows a check mark + eraseBtn.classList.replace('fa-eraser', 'fa-check'); + // After erased, the icon goes back to its default + setTimeout(() => { + eraseBtn.classList.replace('fa-check', 'fa-eraser'); + }, 1000); + }); + } + }) + + // Check to erase all text + function confirmAndClearText(onConfirmErase) { + if (typeof onConfirmErase !== 'function') { + console.log("Expected onConfirmErase a function, got:", typeof onConfirmErase); + alert("Click on the keyboard again to enable."); + return; + } + + const overlay = eraseTextUI(() => { + onConfirmErase(); + }); + + document.body.appendChild(overlay); + } + + // UI for Keyboard Selected Limitation + function eraseTextUI(onDelete) { + const overlay = document.createElement('div'); + overlay.classList.add('dialog-overlay'); + + // Dialog box + const dialog = document.createElement('div'); + dialog.classList.add('dialog-box'); + + const icon = document.createElement('div'); + icon.classList.add('dialog-icon'); + icon.textContent = "⚠️" + + const title = document.createElement('h2'); + title.classList.add('dialog-title'); + title.textContent = 'Clear all text?'; + + const message = document.createElement('p'); + message.classList.add('dialog-message'); + message.textContent = 'Are you sure? This action cannot be undone.'; + + const btnContainer = document.createElement('div'); + btnContainer.classList.add('dialog-buttons'); + + const cancelBtn = document.createElement('button'); + cancelBtn.classList.add('btn-cancel'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.onclick = () => overlay.remove(); + + const deleteBtn = document.createElement('button'); + deleteBtn.classList.add('btn-delete'); + deleteBtn.textContent = 'Erase'; + deleteBtn.onclick = () => { + deleteBtn.disabled = true + overlay.remove(); + onDelete?.(); + }; + + btnContainer.append(cancelBtn, deleteBtn); + dialog.append(icon, title, message, btnContainer); + overlay.append(dialog); + + return overlay; + } +} \ No newline at end of file diff --git a/cdn/dev/js/feature/expand-textarea.js b/cdn/dev/js/feature/expand-textarea.js new file mode 100644 index 0000000..4551d21 --- /dev/null +++ b/cdn/dev/js/feature/expand-textarea.js @@ -0,0 +1,21 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * element and interaction for the expand textarea tool + */ +import { textAreaState } from "../state/appState.js" +import { defaultSize, fullScreenSize } from "../operation/resizeTextArea.js" + +// Identical to hide-keyboard.js but this is for Tablet & Mobile screen size +export function expandTool() { + const expandTool = document.querySelector('#expandTool') + expandTool?.addEventListener('click', () => { + if (textAreaState.isTextAreaFullHeight) { + defaultSize() + } else { + fullScreenSize() + } + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/font-slider.js b/cdn/dev/js/feature/font-slider.js new file mode 100644 index 0000000..6187935 --- /dev/null +++ b/cdn/dev/js/feature/font-slider.js @@ -0,0 +1,14 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * element and interaction for the font slider tool + */ +export function fontSliderTool() { + const textArea = document.querySelector('#textArea') + const fontSliderBtn = document.querySelector('#fontSizeRange') + fontSliderBtn.addEventListener('input', function() { + textArea.style.fontSize = `${this.value}px` + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/hide-keyboard.js b/cdn/dev/js/feature/hide-keyboard.js new file mode 100644 index 0000000..7772fd7 --- /dev/null +++ b/cdn/dev/js/feature/hide-keyboard.js @@ -0,0 +1,21 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * element and interaction for the hide keyboard tool + */ +import { textAreaState } from "../state/appState.js" +import { defaultSize, fullScreenSize } from "../operation/resizeTextArea.js" + +// Identical to hide-keyboard.js but this is for Desktop screen size +export function hideKeyboard() { + const hideKeyboardBtn = document.querySelector('#hideKeyboard') + hideKeyboardBtn?.addEventListener('click', () => { + if (textAreaState.isTextAreaFullHeight) { + defaultSize() + } else { + fullScreenSize() + } + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/instruction.js b/cdn/dev/js/feature/instruction.js new file mode 100644 index 0000000..853027e --- /dev/null +++ b/cdn/dev/js/feature/instruction.js @@ -0,0 +1,35 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-06-29 + * + * element and interactions of the Instruction dialog. + */ + +export function showPopUpDialog(ele) { + if (!ele) return + + // Open modal + ele.classList.remove('hidden') + + // Close button + const closeBtn = ele.querySelector('#instructionModalCloseBtn') + + closeBtn?.addEventListener('click', () => { + ele.classList.add('hidden') + }) + + // Close when clicking outside modal content + ele.addEventListener('click', (e) => { + if (e.target === ele) { + ele.classList.add('hidden') + } + }) + + // Close with ESC key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + ele.classList.add('hidden') + } + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/kb-container.js b/cdn/dev/js/feature/kb-container.js new file mode 100644 index 0000000..4fa912b --- /dev/null +++ b/cdn/dev/js/feature/kb-container.js @@ -0,0 +1,258 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-06-26 + * + * element and interactions of the Keyboard containers + */ +import { selectedKbList } from "../state/appState.js" +import { kbData } from "../state/appState.js" +import { setKeyboard } from "../operation/keyboardDataPackage.js" +import { setKbHelpDocHamburger } from "../operation/hamburgerMenu.js" +import { firstKbToType, setKeyboardToType } from "../operation/keyboard.js" +import { kbDataForKbContainer } from "../operation/keyboardContainer.js" +import { kbConfigMenu } from "../operation/keyboardAction.js" + +// UI for keyboard container +export function kbContainerUI(selectedKbList) { + const kbContainerMenu = document.getElementById('keyboardContainerMenu') + const kbContainerVisible = document.getElementById('keyboardContainerVisible') + + const kbOverflowBtn = document.getElementById("keyboardOverflowBtn") + const kbMenuDropdown = document.getElementById('keyboardOverflowSelection') + const kbOverflowSelection = document.getElementById("keyboardOverflowSelection") + + if (selectedKbList.length < 1) { + resetKbContainer() + return + } + + firstKbToType(selectedKbList) + + const visibleItems = [] + const overflowItems = [] + const kbContainerWidth = kbContainerVisible.clientWidth + let totalWidths = 0 + + selectedKbList.forEach(kb => { + kbContainerVisible.innerHTML = '' + kbOverflowSelection.innerHTML = '' + + const kbChip = createKeyboardChip(kb) + kbContainerVisible.appendChild(kbChip) + + let widths = Array.from(kbContainerVisible.children) + .filter(child => child.nodeType === 1) + .map(child => child.getBoundingClientRect().width) + + totalWidths += widths.reduce((a,b) => a+b, 0) + + kbChip.remove() + + if (totalWidths <= kbContainerWidth) { + visibleItems.push(kb) + } else { + overflowItems.push(kb) + } + + visibleItems.forEach(oneKb => { + kbContainerVisible.appendChild(createKeyboardChip(oneKb)) + }) + + overflowItems.forEach(oneKb => { + kbOverflowSelection.appendChild(createKeyboardChip(oneKb)) + }) + + if (overflowItems.length < 1) { + kbOverflowBtn.classList.add('hidden') + kbOverflowSelection.classList.add('hidden') + } else { + kbOverflowBtn.classList.remove('hidden') + kbOverflowSelection.classList.remove('hidden') + } + } + +)} + +function createKeyboardChip(data) { + const textArea = document.querySelector('#textArea') + + let kbdId = data.id + let langCode = Object.keys(data.supportedLanguage)[0] || "en" + let kbdName = data.name + + const kbDiv = document.createElement('div') + kbDiv.classList.add('kb-item-keyboard') + kbDiv.dataset.id = data.id + + // Keyboard Name + const kbName = document.createElement('span') + kbName.classList.add('kb-chip-name') + kbName.textContent = data.name + + // Remove button + const removeBtn = document.createElement('button') + removeBtn.classList.add('kb-chip-remove') + removeBtn.dataset.action = 'remove' + removeBtn.dataset.id = data.id + removeBtn.innerHTML = '×' + + // Selected indicator + const selectedIndicator = document.createElement('div') + selectedIndicator.classList.add('kb-chip-selected') + const checkIcon = document.createElement('i') + checkIcon.classList.add('fa-solid', 'fa-check') + + selectedIndicator.appendChild(checkIcon) + + kbDiv.appendChild(kbName) + kbDiv.appendChild(removeBtn) + kbDiv.appendChild(selectedIndicator) + + kbName.addEventListener('click', () => { + kbdId = data.id + langCode = Object.keys(data.supportedLanguage)[0] || "en" + // Set the keyboard data for OSK to fetch for display + setKeyboard(kbdId, langCode, kbdName) + // Change the OSK + setKeyboardToType() + setKbHelpDocHamburger(kbdId, kbdName) + reorderSelectedKbList(kbdId) + textArea.focus() + }) + + // Remove keyboard + removeBtn.addEventListener('click', (e) => { + e.stopPropagation() + kbConfigMenu( + 'remove', + data.id, + null + ) + }) + + const defaultKbUSParent = document.querySelector('.default-us-kb') + const defaultKbUSGrandChild = defaultKbUSParent.children[0].children + + Array.from(defaultKbUSGrandChild).forEach(grandChild => { + grandChild.addEventListener('click', (e) => { + kbdId = "basic_kbdus" + langCode = "en" + kbdName = "US Basic" + setKeyboard(kbdId, langCode, kbdName) + setKeyboardToType() + }) + }) + + return kbDiv; +} + +export function reorderSelectedKbList(kbId) { + const index = selectedKbList.findIndex( + kb => kb.id == kbId + ) + + if (index < 1) return + + selectedKbList.unshift( + selectedKbList.splice(index, 1)[0] + ) + + kbContainerUI(selectedKbList) +} + +// Set the keyboard selection menu to its default UI +function resetKbContainer() { + const kbContainerVisible = document.getElementById("keyboardContainerVisible") + kbContainerVisible.innerHTML = '' + + const kbMenuDropdown = document.getElementById('keyboardOverflowSelection') + kbMenuDropdown.innerHTML = '' + + const kbOverMenuDropdownBtn = document.getElementById('keyboardOverflowBtn') + kbOverMenuDropdownBtn.classList.add('hidden') +} + +// UI for Keyboard Selected Limitation +export function kbContainerLimits(onAccept) { + const dialogDiv = document.createElement('div') + dialogDiv.classList.add('warning-container') + + const dialogContentDiv = document.createElement('div') + dialogContentDiv.classList.add('warning-content') + + const dialogCancel = document.createElement('button') + dialogCancel.classList.add('warning-cancel-btn') + dialogCancel.setAttribute('id', 'cancelWarningBtn') + dialogCancel.textContent = '✖' + dialogCancel.onclick = () => { + dialogDiv.remove() + kbContainerUI(selectedKbList) + } + + const dialogImgTag = document.createElement('img') + dialogImgTag.classList.add('warning-keyman-image') + dialogImgTag.src = `/cdn/dev/img/keymanweb-mini-logo-88.png` + dialogImgTag.alt = "Keyman Logo" + + const firstKeyboard = selectedKbList[0].name + + const dialogPTag = document.createElement('p') + dialogPTag.classList.add('warning-text') + dialogPTag.innerHTML = `The keyboards selected exceeds the limitation of 5 keyboards. Click 'Allow' to remove the
${firstKeyboard} keyboard.` + + const dialogUlTag = document.createElement('ol') + dialogUlTag.textContent = "Your keyboards selection:" + selectedKbList.forEach((kb, index) => { + const dialogLiTag = document.createElement('li') + if (index == 0) { + const markKeyboard = document.createElement('mark') + markKeyboard.textContent = kb.name + dialogLiTag.appendChild(markKeyboard) + } else { + dialogLiTag.textContent = kb.name + } + dialogUlTag.appendChild(dialogLiTag) + }) + + const dialogAccept = document.createElement('button') + dialogAccept.classList.add('warning-accept-btn') + dialogAccept.setAttribute('id', 'acceptWarningBtn') + dialogAccept.textContent = "Allow" + dialogAccept.onclick = () => { + dialogDiv.remove() + onAccept() + } + + dialogContentDiv.appendChild(dialogCancel) + dialogContentDiv.appendChild(dialogImgTag) + dialogContentDiv.appendChild(dialogPTag) + dialogContentDiv.appendChild(dialogUlTag) + dialogContentDiv.appendChild(dialogAccept) + + dialogDiv.appendChild(dialogContentDiv) + + return dialogDiv +} + +// Highlight the keyboard in the Keyboard Container +export function highlightKbContainer(kbId) { + document.querySelectorAll('.kb-item-keyboard').forEach(el => { + el.classList.remove('kb-item-keyboard-selected') + }) + + const kbSelectedItem = document.querySelector(`[data-id=${kbId}]`) + if (kbSelectedItem) { + kbSelectedItem.classList.add('kb-item-keyboard-selected') + } +} + +// Compare and remove keyboard +export function removeFromKbContainer(kbId) { + if(!kbId) return + let filtered = selectedKbList.filter(kb => kb.id !== kbId) + + // Reset and re-enter the Keyboard selection menu + selectedKbList.length = 0 + selectedKbList.push(...filtered) +} diff --git a/cdn/dev/js/feature/pagination.js b/cdn/dev/js/feature/pagination.js new file mode 100644 index 0000000..707dc94 --- /dev/null +++ b/cdn/dev/js/feature/pagination.js @@ -0,0 +1,49 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * Element and interaction for the pagination + */ +import { getKeyboardList } from "../operation/searchAPI.js" +import { searchState } from "../state/appState.js" +import { searchKbWithQuery } from "../operation/searchLogic.js" + +/* Pagination */ +const prevBtn = document.getElementById('prevPage') +const nextBtn = document.getElementById('nextPage') +const pageInfo = document.getElementById('pageInfo') + +export function goPrevPage() { + if (searchState.currentPage > 1) { + searchState.currentPage-- + searchKbWithQuery(searchState.searchQuery, searchState.currentPage) + } +} + +export function goNextPage() { + if (searchState.currentPage < searchState.totalPage) { + searchState.currentPage++ + searchKbWithQuery(searchState.searchQuery, searchState.currentPage) + } +} + +// Load keyboard search info (pages) +export async function getTotalPage(defaultQuery = "p:popular") { + const data = await getKeyboardList(defaultQuery) // depending on the query + + if (data?.context) { + searchState.totalPage = data.context.totalPages || 1; + searchState.currentPage = data.context.pageNumber || 1; + } else { + searchState.totalPage = Math.ceil(data.keyboards.length / searchState.itemPerPage); + } + + updatePaginationCtrl() +} + +export function updatePaginationCtrl() { + pageInfo.textContent = `${searchState.currentPage} of ${searchState.totalPage}` // Number of Total page + nextBtn.disabled = searchState.currentPage >= searchState.totalPage // Check to disable next page + prevBtn.disabled = searchState.currentPage <= 1 // Check to disable previous page +} diff --git a/cdn/dev/js/feature/search.js b/cdn/dev/js/feature/search.js new file mode 100644 index 0000000..28620c5 --- /dev/null +++ b/cdn/dev/js/feature/search.js @@ -0,0 +1,205 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-06-26 + * + * Element and interactions for Search + */ +import { searchState, selectedKbList } from "../state/appState.js"; +import { highlightSearchContext, truncateDesc, getMarkedContext, showMarkedContext } from "../operation/searchCardContent.js"; +import { kbContainerUI, reorderSelectedKbList } from "./kb-container.js"; +import { validateURL } from "../operation/validURL.js"; +import { setKeyboardToType } from "../operation/keyboard.js"; +import { setKbHelpDocHamburger } from "../operation/hamburgerMenu.js"; +import { getTotalPage } from "./pagination.js"; +import { storeInKbContainer, kbDataForKbContainer } from "../operation/keyboardContainer.js"; +import { setKeyboard } from "../operation/keyboardDataPackage.js"; + +/* Search */ +const kbSearchCard = document.getElementById('kbSearchCardUI') +const searchResultCount = document.getElementById('resultCount') +const paginationCtrl = document.getElementById('paginationControls') +const magnifying = document.querySelector('#magnifyingGlassIcon') + +// Search icon changes depending on the value +export function updateSearchIcon(searchQuery) { + // Base on the value given, it will display one of the icons below + magnifying.style.display = searchQuery ? 'none' : 'inline' + clearSearchIcon.style.display = searchQuery ? 'inline' : 'none' +} + +/* + Only called when there is a query search + Display items return from search +*/ +export function displaySearch(listOfKbData, amountOfKb = 0, searchQuery = '') { + // Ensure Keyboard Search UI is empty + kbSearchCard.innerHTML = ''; + + const kbHrTitle = document.querySelector('.keyboard-title') + // Validate the keyboards data + if (!listOfKbData || listOfKbData.length == 0) { + kbSearchCard.innerHTML = `No keyboards found for ${searchQuery}.` + kbHrTitle.textContent = "Results" + paginationCtrl.style.display = 'none' + return + } + + if (amountOfKb) { + searchResultCount.innerHTML = `${amountOfKb} results` + searchResultCount.classList.remove('hidden') + } + + // Getting searched Word ready for highlight + const markedSearchText = getMarkedContext(searchQuery) + + if (!searchQuery) { + // Most Download UI + kbHrTitle.textContent = "Most Downloads" + paginationCtrl.style.display = 'none' + } else if (amountOfKb < 2) { + // Total results < 2 UI + kbHrTitle.textContent = "Results" + paginationCtrl.style.display = 'none' + } else { + // Result of search query + kbHrTitle.textContent = "Results" + paginationCtrl.style.display = 'flex' + } + + // Display each keyboards into the card UI + listOfKbData.forEach(eachKb => { + const card = searchMenu(eachKb, markedSearchText, selectedKbList, listOfKbData) + kbSearchCard.appendChild(card) + }) +} + +// Search card UI +function searchMenu(eachKb, markedSearchText = '', selectedKbList, listOfKbData) { + const searchInput = document.querySelector('#searchInput') + const searchDropdown = new bootstrap.Dropdown(searchInput) + const textArea = document.querySelector('#textArea') + + const kbFoundInList = selectedKbList.some(selected => selected.id == eachKb.id) // find a match between the keyboard from selection menu & search + // Keyboard card container + let cardWrap = document.createElement('div') + cardWrap.classList.add('card-wrap') + cardWrap.setAttribute('id', 'keyboardCardWrap') + + // Keyboard header container + let cardHeader = document.createElement('div') + cardHeader.classList.add('card-header') + + const {matchFound, matchField, matchValue} = highlightSearchContext(eachKb, markedSearchText) // Highlight search query + + const kbNameHeading = matchFound ? showMarkedContext(eachKb, matchField, matchValue) // Highlight search query UI + : (() => { + const heading = document.createElement('h4') + heading.innerHTML = eachKb.name + return heading + })() + + // Keyboard Help (?) icon + const kbIconDiv = document.createElement('div') + kbIconDiv.classList.add('card-header-icon') + + const kbHelpIcon = document.createElement('i') + kbHelpIcon.classList.add('fa-solid', 'fa-question') + kbHelpIcon.setAttribute('id', 'kbHelpIcon') + kbIconDiv.appendChild(kbHelpIcon) + + const kbDownloadIcon = document.createElement('i') + kbDownloadIcon.classList.add('fa-solid', 'fa-download') + kbDownloadIcon.setAttribute('id', 'kbDownloadIcon') + kbIconDiv.appendChild(kbDownloadIcon) + + // Keyboard Header + const kbHeaderTitle = document.createElement('div') + kbHeaderTitle.classList.add('card-header-title') + + const kbSubHeader = document.createElement('div') + kbSubHeader.classList.add('card-sub-header') + + // Keyboard ID + const kbIdPTag = document.createElement('p') + kbIdPTag.classList.add('keyboard-id') + kbIdPTag.textContent = eachKb.id + + // Dot for spacing + const kbDot = document.createElement('div') + kbDot.classList.add('dot-spacing') + + // Keyboard Monthly Downloads + const kbDownloadHeading = document.createElement('p') + kbDownloadHeading.textContent = `${eachKb.match.downloads} monthly downloads` + kbDownloadHeading.classList.add('monthly-download') + + // Keyboard Description + const kbDescHeading = truncateDesc(eachKb, matchField, markedSearchText) + + // Keyboard Selection Indicator + const kbSelectionIndic = document.createElement('div') + kbSelectionIndic.classList.add('selection-indicator') + + const kbTickIcon = document.createElement('i') + kbTickIcon.classList.add('fa-solid', 'fa-check') + + kbSelectionIndic.appendChild(kbTickIcon) + + // Append children + kbHeaderTitle.appendChild(kbNameHeading) + cardHeader.appendChild(kbHeaderTitle) + cardHeader.appendChild(kbIconDiv) + + kbSubHeader.appendChild(kbIdPTag) + kbSubHeader.appendChild(kbDot) + kbSubHeader.appendChild(kbDownloadHeading) + + cardWrap.appendChild(cardHeader) + cardWrap.appendChild(kbSubHeader) + cardWrap.appendChild(kbDescHeading) + cardWrap.appendChild(kbSelectionIndic) + + // Must check if the keyboard is in the selection menu + checkSearchCardStatus([cardWrap, kbSelectionIndic], eachKb) + + // Click on a keyboard name to enable the keyboard & add it into selection menu + cardWrap.onclick = () => { + const langCode = Object.keys(eachKb.languages)[0] || "en" + storeInKbContainer(cardHeader, eachKb, listOfKbData) + setKbHelpDocHamburger(eachKb.id, eachKb.name) + setKeyboard(eachKb.id, langCode, eachKb.name) + reorderSelectedKbList(eachKb.id) + setKeyboardToType() + // Must check if the keyboard is in the selection menu + checkSearchCardStatus([cardWrap, kbSelectionIndic], eachKb) + } + + // Click on the help icon on the search card to get to the keyboard help documentation + kbHelpIcon.addEventListener('click', (e) => { + e.stopPropagation() + const checkedURL = validateURL(`https://help.keyman.com/keyboard/`) + const newURL = checkedURL + eachKb.id + window.open(newURL, '_blank') + }) + + kbDownloadIcon.addEventListener('click', (e) => { + e.stopPropagation() + const checkedURL = validateURL(`https://keyman.com/keyboards/install/`) + const newURL = checkedURL + eachKb.id + window.open(newURL, '_blank') + }) + + return cardWrap +} + +// Disable or Enable Search UI +export function checkSearchCardStatus(elements, kb) { + let kbFoundInList = selectedKbList.some(selected => selected.id == kb.id) + const selectIndicator = document.querySelector('.selection-indicator') + const kbCardWrap = document.querySelector('.card-wrap') + + Object.values(elements).forEach(ele => { + ele?.classList.toggle('selected', kbFoundInList) + }) +} \ No newline at end of file diff --git a/cdn/dev/js/feature/tool-tray.js b/cdn/dev/js/feature/tool-tray.js new file mode 100644 index 0000000..bf97014 --- /dev/null +++ b/cdn/dev/js/feature/tool-tray.js @@ -0,0 +1,102 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengHeng Hav on 2026-08-05 + * + * Element and interaction for the tool tray for every screen + */ +const tabletToolElements = ` + + ` + +const desktopToolElements = ` +
+
+ A + + A +
+
+
+
+ +
+
+ +
+
+ +
+
+ ` + +const phoneToolElements = `
+ + + + + + +
+
+ +
+
+
+ A + + A +
+
` + +const phoneExpandElement = ` + + ` + +const calcScreenSize = Math.min(screen.width, screen.height) + +const toolContainer = document.querySelector('.tool-container') +const divider = document.querySelector('.divider-container') +const resizeGrip = document.querySelector('#resizeGrip') + +export function renderToolsTray() { + if (calcScreenSize < 900 && calcScreenSize >= 720) { // Tablet + toolContainer.innerHTML = tabletToolElements + divider.style.display = 'none' + } else if (calcScreenSize < 720) { // Phone + divider.innerHTML = phoneToolElements + toolContainer.innerHTML = phoneExpandElement + resizeGrip.style.display = 'none' + } else { // Desktop + toolContainer.innerHTML = desktopToolElements + } +} diff --git a/cdn/dev/js/kmwlive.js b/cdn/dev/js/kmwlive.js index 54051b4..46d806b 100644 --- a/cdn/dev/js/kmwlive.js +++ b/cdn/dev/js/kmwlive.js @@ -1,471 +1,471 @@ -$.urlParam = function(name){ - var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href); - if (!results) { return 0; } - return decodeURIComponent(results[1]).replace(/\+/g, ' ') || 0; -} - -function getKeymanWeb() { - if(window.tavultesoft) { - return window.tavultesoft.keymanweb; - } - return window.keyman; -} - -// -// If we detect a rewrite of /go//, remap that to the new #-based model. -// This will force a reload so the /return/ statement isn't really necessary... -// - -function setCookie(c_name,value,exdays){ - var exdate=new Date(); - exdate.setDate(exdate.getDate() + exdays); - var c_value=escape(value) + ((exdays==null) ? "" : "; expires="+exdate.toUTCString()); - document.cookie=c_name + "=" + c_value; -} - -var locationmatch = location.pathname.match(/^\/go\/([^/]+)\/([^/]+)/); -if(locationmatch) { - - location.href = '/#'+locationmatch[1]+',Keyboard_'+locationmatch[2]; -} - -// Perform all page-load init that is NOT dependent on KeymanWeb. -// That script may not load in time for this method. -$(document).ready(function() { - // Font size function - $( "#slider" ).slider({ - value:16, - min: 10, - max: 132, - step: 2, - slide: function( event, ui ) { - $('#message').css('font-size',ui.value); - $('#message').focus(); - } - }); - - var font = document.getElementById('font'); - var fontIncrease = document.getElementById('mobile-increase'); - var fontDecrease = document.getElementById('mobile-decrease'); - - font.addEventListener('touchstart', function(event) { - $('#mobile-font').show(); - $('#search').css('visibility','hidden'); - var r = setTimeout(function(){ - $('#mobile-font').hide(); - setTimeout(function(){ - $('#search').css('visibility','visible'); - },1000); - },3000); - var size = Number($('#mobile-font-size1').val()); - - fontIncrease.addEventListener('touchstart', function(event) { - clearInterval(r); - size = size * 1.2; - $('#mobile-font-size1').val(size); - $('textarea').css('font-size',size); - $('.keymanweb-input').css('font-size',size).focus(); - r = setTimeout(function(){ - $('#mobile-font').hide(); - setTimeout(function(){ - $('#search').css('visibility','visible'); - },600); - },2000); - }); - - fontDecrease.addEventListener('touchstart', function(event) { - clearInterval(r); - size = size / 1.2; - $('#mobile-font-size1').val(size); - $('textarea').css('font-size',size); - $('.keymanweb-input').css('font-size',size).focus(); - r = setTimeout(function(){ - $('#mobile-font').hide(); - setTimeout(function(){ - $('#search').css('visibility','visible'); - },600); - },2000); - }); - - }); - - setTimeout(function(){ - $('.kmw_button_a').click(function(){ - if ($(this).parent().attr('id') == 'kmw_btn_off') { - //$('body').removeClass('osk-always-visible'); - $('#message').blur(); - } else { - //$('body').addClass('osk-always-visible'); - } - }); - },5000); - - - /* Setup the bookmarklet */ - $('#bookmarklet div a').click( function() { - alert("Don't click this: drag it to your Bookmarks toolbar. Then you can click the '" + $(this).text() + "' bookmark on any web page to access your web keyboard on that page!") - return false; - }); - - // Detect desktop browser height and modify css - function updateSize(){ - // Set OSK size/position - p = new Object(); - - var height = $(window).height(); - var width = $(window).width(); - var appPos = $('#app').position(); - var appLeft = appPos.left; - - // We can't proceed any further if KMW hasn't loaded yet. - // No point handling resizes until that's occurred. - if(!getKeymanWeb() || !getKeymanWeb().osk) { - return; - } - - // Adjust the message box height only if a desktop browser - if(!getKeymanWeb().util.isTouchDevice()) - { - if (height <= 768) { - // Hide footer - $('footer').hide(); - $('#message').css('height', '209px'); - p['height'] = 264; - } - if(height > 768) { - $('#message').css('height', '260px'); - if (height < 820) { - p['height'] = 246; - $('footer').hide(); - }else if(height < 860){ - p['height'] = 246; - $('footer').show(); - }else{ - p['height'] = 264; - $('footer').show(); - } - } - } - p['top'] = $('#app').offset().top + $('#app').outerHeight() + 8; - p['left'] = appLeft + 15; - p['width'] = 710; - - // Update keyboard position and size - getKeymanWeb().osk.setRect(p); - } - - window.onresize = updateSize; - - //$('#example').css('height','50px'); - - /* Check for result for other modules in the query string */ - var result = $.urlParam('result'); - if(result) - { - var module = $.urlParam('module'); - var error = $.urlParam('error'); - var data = $.urlParam('data'); - if(result == 'error') - { - showError(module, error, redirectHome); - } - return; - } - - $('#search').click(function(event){ - event.preventDefault(); - if($('#search').attr('disabled') == 'disabled') return; - var newURL='https://www.google.com/search?q='+encodeURIComponent($('#message').val()); - window.open(newURL,'_blank'); - //location.href = 'http://www.google.com/search?q='+encodeURIComponent($('#message').val()); - }); - - var copy = document.getElementById('copy'); - var clipboard = new ClipboardJS(copy); - var isSupported = ClipboardJS.isSupported('copy'); - if (isSupported === false) { - $('#copy').attr('class', 'linksOff'); - $('#copy').attr('disabled', 'disabled'); - $('#copy').hide(); - } - - - clipboard.on('success', function(e) { - //console.log(e); - $('#copy').children('p').html('Copied'); - e.clearSelection(); - setTimeout(function(){ - $('#copy').children('p').html('Copy'); - },2000); - }); - - clipboard.on('error', function(e) { - //console.log(e); - $('#copy').children('p').html('Copy Failed'); - e.clearSelection(); - setTimeout(function(){ - $('#copy').children('p').html('Copy'); - },2000); - }); - - // Email subscribe form - $('.subscribe').click(function(){ - $('#mc-embedded-subscribe-form').submit(); - }); - - //Cannot detect change of content from KMW, so use a timer instead to refresh button state - //$('#message').bind("keypress keyup keydown change click focus blur", refreshButtons); - window.setInterval(refreshButtons,200); -}); - -var afterInitRun = false; -function afterInit() { - if(afterInitRun) { - return; - } - afterInitRun = true; - - // Focus on message box - if (!!$('.messageBox').length) { - getKeymanWeb().moveToElement('message'); - } - // On touch devices, this is necessary (but not sufficient) for ClipboardJS compatibility. - // Must take affect AFTER KMW has initialized. - $('#message').removeAttr('disabled'); - - getKeymanWeb().util.attachDOMEvent(window,'orientationchange', function() { - window.scrollTo(0,1); - },false); - - //getKeymanWeb().addEventListener('keyboardloaded',function(p){changeKeyboard(p['keyboardName']);}); - getKeymanWeb().addEventListener('keyboardchange',function(p){if(!pageLoading) changeKeyboard(p['internalName'],p['languageCode'],p);}); -} - -function refreshButtons() { - var len=getKMWInputLength('message'); // returns -1 for desktop page element - - if(len < 0) { - // refreshButtons runs via setInterval. It is possible for one call to persist through page reload, - // in which case the #message element is unavailable. - var message = $('#message').val(); - if(!message) { - return; - } - - len = message.length; - } - - // if message length is 0, use white SM buttons - if(len == 0) { - $('#buttons').children('div').attr('class','linksOff'); - $('#buttons div').attr('disabled', 'disabled'); - } else { - $('#buttons').children('div').attr('class','links'); - $('#buttons div').removeAttr('disabled'); - } -} - -function getKMWInputElement(id){ - var i,len; - try - { - var eList=document.getElementsByClassName('KMW_input'); - if(eList && eList.length > 0) - { - for(i=0; i'); - box.append($('
').append(message)); - $('body').append(box); - return box; -} - -var boxVisible = false; - -function showBox(module, message, callback) -{ - boxVisible = true; - var box = $('
'); - box.append($('
').append(message)); - box.append($(' + 1 + + + + + + +
+
+
+ US Basic +
+
+ +
+ +
+ + +
+ -
- + +
+ headerBar
-
- -
AA

Font Size

-
-

Copy

+ + +
+ +
+
-
- A
A +
+ +
+ +
-
-
+
-
-
- + +
+
+

No example is available for this keyboard.

+
+
+
+
- - - - - - - +