first commit

This commit is contained in:
ethan merchant 2023-04-24 06:59:47 -04:00
commit e0d2e52c93
14 changed files with 1342 additions and 0 deletions

55
icons/back.svg Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 32 32.000001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="back.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs2" /><sodipodi:namedview
id="base"
pagecolor="#808080"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="7.919596"
inkscape:cx="10.669736"
inkscape:cy="13.131983"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#808080"
showguides="true" /><metadata
id="metadata5"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1090.5197)"><path
style="fill:none;stroke:#ffffff;stroke-width:3.199;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 15.92301,1097.1929 -9.2381147,9.2381 9.4155007,9.4155"
id="path11921" /><path
style="fill:none;stroke:#ffffff;stroke-width:3.199;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 25.315019,1106.6491 H 7.0812466"
id="path11925" /></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

72
icons/copy.svg Normal file
View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 32 32.000001"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="copy.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#808080"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="18.306958"
inkscape:cy="14.832193"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="22"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1090.5197)">
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.706;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
id="rect1373"
width="15.616629"
height="15.616629"
x="3.8530145"
y="1094.2299" />
<rect
y="1103.1393"
x="12.762503"
height="15.616629"
width="15.616629"
id="rect1375"
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.706;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="192"
height="192"
viewBox="0 0 192 192.00001"
version="1.1"
id="svg8"
sodipodi:docname="salish-keyboard-icon.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="C:\dofdev\Salish Keyboard\salish-keyboard-icon.png"
inkscape:export-xdpi="256"
inkscape:export-ydpi="256">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1965">
<stop
style="stop-color:#1c161b;stop-opacity:0.5"
offset="0"
id="stop1961" />
<stop
style="stop-color:#1c161b;stop-opacity:0;"
offset="1"
id="stop1963" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1965"
id="linearGradient1971"
x1="92.405533"
y1="1010.9304"
x2="147.82085"
y2="1062.9532"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.56078431"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="66.492719"
inkscape:cy="101.6195"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-global="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="22"
inkscape:window-maximized="1"
units="px"
inkscape:showpageshadow="false"
inkscape:pagecheckerboard="false"
inkscape:snap-bbox="true"
inkscape:snap-page="true"
borderlayer="true">
<inkscape:grid
type="xygrid"
id="grid832" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-930.51965)">
<path
style="fill:#1c161b;fill-opacity:1;stroke:none;stroke-width:3.14762044;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 31.950295,959.96696 c 43.876553,8.69094 87.154865,9.20369 127.509105,-0.32554 7.89365,-1.77643 8.66265,2.45107 9.76665,8.1389 l 22.34605,103.84488 c 0,0 0.10261,8.0315 -0.26872,13.0593 -10.89084,1.4332 -181.055533,1.5621 -190.78290754,0 -0.24551432,-4.7122 0.0141647,-12.1647 0.0141647,-12.1669 L 23.378019,968.11152 c 1.011881,-10.09604 4.723823,-9.39183 8.572937,-8.13889 z"
id="path4518"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc"
inkscape:export-filename="C:\dofdev\Web Development\dofdev\v2\dof-covers\salishkeyboard.png"
inkscape:export-xdpi="256"
inkscape:export-ydpi="256" />
<path
style="fill:#f2e2c8;fill-opacity:1;stroke-width:1.14920998"
d="m 97.615778,1000.7522 c -2.570331,-0.022 -5.195887,0.1495 -7.893649,0.5072 -9.29379,1.2326 -14.97792,3.6691 -24.031342,10.3096 -5.486743,4.0244 -11.152758,6.6213 -22.386836,10.2575 -17.945921,5.8086 -20.58188,7.9444 -9.276871,7.5126 2.856277,-0.1087 8.77396,0.1305 13.149924,0.5316 7.344547,0.6734 8.801895,1.2072 18.984084,6.95 12.832991,7.2378 20.042316,9.3802 31.135002,9.2523 10.44938,-0.1195 20.60068,-3.5352 29.13701,-9.7963 8.76773,-6.4311 15.83965,-8.4179 29.95773,-8.4224 17.3159,0 15.20394,-2.2798 -7.30346,-7.8568 -12.54179,-3.1076 -14.65166,-3.932 -19.83861,-7.7645 -10.39837,-7.683 -20.49494,-11.386 -31.632982,-11.4808 z"
id="path892"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccscscssc" />
<path
style="fill:#5a2927;stroke-width:1.16148973"
d="m 97.291872,1007.5482 c -18.181882,0.026 -35.947996,10.1211 -29.106612,21.1816 1.890807,3.0566 3.608117,4.2497 9.820261,6.8159 4.141025,1.7107 10.708268,3.3939 14.592148,3.7405 11.821361,1.0551 28.091391,-5.1275 31.414891,-11.9358 2.75289,-5.6394 -0.94597,-12.0604 -9.29523,-16.1363 -5.25722,-2.5663 -11.36484,-3.6745 -17.425458,-3.6659 z"
id="path894"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccssc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
icons/tap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

BIN
icons/tung.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

91
icons/tung.svg Normal file
View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 32 32.000001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tongue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#808080"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="10.044643"
inkscape:cy="14.553571"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#808080"
showguides="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1090.5197)">
<circle
style="fill:#ffffff;fill-opacity:1;stroke-width:1.44342;stroke-dasharray:none"
id="path217"
cx="3.315028"
cy="1102.2913"
r="2.2093179" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke-width:1.44342;stroke-dasharray:none"
id="circle950"
cx="28.684973"
cy="1102.2913"
r="2.2093179" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke-width:1.44342;stroke-dasharray:none"
id="circle952"
cx="15.999997"
cy="1102.2913"
r="2.2093179" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke-width:1.44342;stroke-dasharray:none"
id="circle954"
cx="9.6575136"
cy="1111.1942"
r="2.2093179" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke-width:1.44342;stroke-dasharray:none"
id="circle956"
cx="22.342487"
cy="1111.1942"
r="2.2093179" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

57
icons/tungs.svg Normal file
View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 32 32.000001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tongues.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs2" /><sodipodi:namedview
id="base"
pagecolor="#808080"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="24.419643"
inkscape:cy="14.553571"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#808080" /><metadata
id="metadata5"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1090.5197)"><path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.55999;stroke-dasharray:none;stroke-opacity:1"
d="M 4.1490411,1098.2809 H 27.850958"
id="path1874" /><path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.55999;stroke-dasharray:none;stroke-opacity:1"
d="M 4.1490411,1106.5197 H 27.850958"
id="path1876" /><path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.55999;stroke-dasharray:none;stroke-opacity:1"
d="M 4.1490411,1114.7585 H 27.850958"
id="path1878" /></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

164
index.html Normal file
View file

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html class="notranslate" translate="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="interactive-widget=resizes-content, width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="google" content="notranslate" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/tung.svg">
<link rel="icon" type="image/svg+xml" sizes="any" href="icons/tung.svg">
<link rel="manifest" href="manifest.webmanifest">
<meta name="description" content="Augment your keyboard with the symbols you need to type in your language.">
<meta name="author" content="Ethan Merchant">
<title style="display: none;">tung-tap</title>
<link rel="stylesheet" href="style.css?v1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<!-- <div id="home" style="
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 10px 20px;
padding-top: 20px;
background-color: #79394f;
">
<div style="
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
">
<img src="icons/tongue.png" style="
width: 32px; height: 32px;
transform: translateY(-5px) rotate(-5deg);
">
<img src="icons/tap.png" style="
width: 32px; height: 32px;
margin-right: -40px;
transform: translate(-32px, 7px) rotate(-45deg) scale(0.8);
">
<h1 style="font-weight: unset; font-size: 24px; line-height: 24px; margin: 0; color: #fff">
TongueTap<span style="font-size: 14px; letter-spacing: 1px;">.app</span>
</h1>
</div>
<p style="
font-size: 14px;
line-height: 24px;
font-style: italic;
margin: 0;
color: #fff;
">
Unicode key-swatch webapp for any cultures tongue!
</p>
</div> -->
<div style="display: block; position: relative; background-color: white;">
<div hidden="true" id="tung" onclick="layout()" oninput="layout()" contenteditable="true" spellcheck="false" style="
user-select: text;
touch-action: none;
overflow: none;
white-space: pre;
width: auto;
height: 100px;
padding: 10px;
font-family: 'DM Mono', monospace;
text-align: center;
font-size: 12px;
line-height: 20px;
background-color: #ff79a1;
color: #79394f;
/* display: none; */
"></div>
<div onclick="copyToClipboard('#tung')" style="
cursor: pointer;
user-select: none;
position: absolute;
right: 10px;
bottom: 10px;
"><img class="icon-btn" src="icons/copy.svg" style="mix-blend-mode: difference;"></div>
</div>
<div style="
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
width: 40px; height: 40px;
margin: 0 auto;
margin-right: 80px;
width: 100%;
/* background-color: #eb8bb3; */
background-color: #ff79a1c0;
color: white;
user-select: none;
line-height: 40px;
">
<img onclick="$('#nav').toggle()" class="icon-btn" src="icons/tungs.svg">
<img onclick="TopToggle()" class="icon-btn" src="icons/tung.svg">
</div>
<div style="flex-grow: 1; overflow: hidden; position: relative; background-color: white;">
<div id="text" onclick="$('#text').focus()" contenteditable="true" spellcheck="false" style="
user-select: text;
touch-action: pan-y;
overflow-y: scroll;
overscroll-behavior: contain;
white-space: pre-wrap;
display: block;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
padding: 10px;
font-size: 14px;
line-height: 20px;
background-color: rgb(var(--field));
color: rgb(var(--txt));
/* padding-bottom: 600px; */
"></div>
<div onclick="copyToClipboard('#text')" style="
cursor: pointer;
user-select: none;
position: absolute;
right: 10px;
bottom: 10px;
">
<img class="icon-btn" src="icons/copy.svg" style="mix-blend-mode: difference;">
</div>
</div>
<div id="nav" hidden="true"></div>
<div id="keyboard" onclick="$('#text').focus()"></div>
</body>
</html>
<!--
TODO
migrate to textareas
contenteditable divs are a can of worms
esp. when it comes to conistent behaviour across devices
spellcheck toggle~
-->

138
inobounce.js Normal file
View file

@ -0,0 +1,138 @@
/*! iNoBounce - v0.2.0
* https://github.com/lazd/iNoBounce/
* Copyright (c) 2013 Larry Davis <lazdnet@gmail.com>; Licensed BSD */
(function(global) {
// Stores the Y position where the touch started
var startY = 0;
// Store enabled status
var enabled = false;
var supportsPassiveOption = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassiveOption = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {}
var handleTouchmove = function(evt) {
// Get the element that was scrolled upon
var el = evt.target;
// Allow zooming
var zoom = window.innerWidth / window.document.documentElement.clientWidth;
if (evt.touches.length > 1 || zoom !== 1) {
return;
}
// Check all parent elements for scrollability
while (el !== document.body && el !== document) {
// Get some style properties
var style = window.getComputedStyle(el);
if (!style) {
// If we've encountered an element we can't compute the style for, get out
break;
}
// Ignore range input element
if (el.nodeName === 'INPUT' && el.getAttribute('type') === 'range') {
return;
}
var scrolling = style.getPropertyValue('-webkit-overflow-scrolling');
var overflowY = style.getPropertyValue('overflow-y');
var height = parseInt(style.getPropertyValue('height'), 10);
// Determine if the element should scroll
var isScrollable = scrolling === 'touch' && (overflowY === 'auto' || overflowY === 'scroll');
var canScroll = el.scrollHeight > el.offsetHeight;
if (isScrollable && canScroll) {
// Get the current Y position of the touch
var curY = evt.touches ? evt.touches[0].screenY : evt.screenY;
// Determine if the user is trying to scroll past the top or bottom
// In this case, the window will bounce, so we have to prevent scrolling completely
var isAtTop = (startY <= curY && el.scrollTop === 0);
var isAtBottom = (startY >= curY && el.scrollHeight - el.scrollTop === height);
// Stop a bounce bug when at the bottom or top of the scrollable element
if (isAtTop || isAtBottom) {
evt.preventDefault();
}
// No need to continue up the DOM, we've done our job
return;
}
// Test the next parent
el = el.parentNode;
}
// Stop the bouncing -- no parents are scrollable
evt.preventDefault();
};
var handleTouchstart = function(evt) {
// Store the first Y position of the touch
startY = evt.touches ? evt.touches[0].screenY : evt.screenY;
};
var enable = function() {
// Listen to a couple key touch events
window.addEventListener('touchstart', handleTouchstart, supportsPassiveOption ? { passive : false } : false);
window.addEventListener('touchmove', handleTouchmove, supportsPassiveOption ? { passive : false } : false);
enabled = true;
};
var disable = function() {
// Stop listening
window.removeEventListener('touchstart', handleTouchstart, false);
window.removeEventListener('touchmove', handleTouchmove, false);
enabled = false;
};
var isEnabled = function() {
return enabled;
};
// Enable by default if the browser supports -webkit-overflow-scrolling
// Test this by setting the property with JavaScript on an element that exists in the DOM
// Then, see if the property is reflected in the computed style
var testDiv = document.createElement('div');
document.documentElement.appendChild(testDiv);
testDiv.style.WebkitOverflowScrolling = 'touch';
var isScrollSupported = 'getComputedStyle' in window && window.getComputedStyle(testDiv)['-webkit-overflow-scrolling'] === 'touch';
document.documentElement.removeChild(testDiv);
if (isScrollSupported) {
enable();
}
// A module to support enabling/disabling iNoBounce
var iNoBounce = {
enable: enable,
disable: disable,
isEnabled: isEnabled,
isScrollSupported: isScrollSupported
};
if (typeof module !== 'undefined' && module.exports) {
// Node.js Support
module.exports = iNoBounce;
}
if (typeof global.define === 'function') {
// AMD Support
(function(define) {
define('iNoBounce', [], function() { return iNoBounce; });
}(global.define));
}
else {
// Browser support
global.iNoBounce = iNoBounce;
}
}(this));

16
manifest.webmanifest Normal file
View file

@ -0,0 +1,16 @@
{
"background_color": "#ff79a1",
"description": "Augment your keyboard with the symbols you need to type in your language.",
"display": "fullscreen",
"icons": [
{
"src": "/icons/tung.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
],
"name": "tung-tap.app",
"short_name": "tung-tap",
"start_url": "/index.html"
}

410
script.js Normal file
View file

@ -0,0 +1,410 @@
var tung = ""
var topToggle = false
function TopToggle(show = null) {
topToggle = show === null ? !topToggle : show
if (topToggle) {
$('#tung').show()
} else {
$('#tung').hide()
}
layout()
}
const sfx = new Audio('sfx-click.ogg')
$(document).ready(() => {
if (localStorage.getItem('tung') === null || localStorage.getItem('tung') == "") {
tung = "201C 2014 00D7 00B0 00B7 2022 221E 00B1 2023 201D"
localStorage.setItem('tung', tung)
}
else {
tung = localStorage.getItem('tung')
}
$('#tung').html(tung)
layout()
// persistent text
if (localStorage.getItem('text') === null || localStorage.getItem('text') == "") {
localStorage.setItem('text', "")
}
else {
$('#text').html(localStorage.getItem('text'))
}
// $('body').on('keydown', '[contenteditable]', function(e) {
// if (e.keyCode === 13) {
// e.preventDefault() // prevent the default newline insertion
// var range = window.getSelection().getRangeAt(0) // get the current selection range
// var newline = document.createTextNode("\n") // create a new text node with the newline character
// range.insertNode(newline) // insert the new text node at the current cursor position
// range.setStartAfter(newline) // set the cursor after the new text node
// range.setEndAfter(newline) // set the end of the selection after the new text node
// window.getSelection().removeAllRanges() // remove the old selection
// window.getSelection().addRange(range) // set the new selection to the updated range
// layout()
// }
// console.log(e.keyCode)
// });
let url = '/tungs.txt'
$.get(url, function (data) {
lines = data.split('\n')
nav()
})
})
var lines = null
var indexPath = [ 0 ]
function indentation(line) {
let lineIndent = 0
while (line.charAt(lineIndent) == '\t') { lineIndent++ }
return lineIndent
}
function nav() {
let rootIndex = indexPath[indexPath.length - 1]
let root = lines[rootIndex]
let rootIndent = indentation(root)
let html = ``
for (let i = rootIndex; i < lines.length; i++) {
let line = lines[i]
let lineIndent = indentation(line)
if (lineIndent < rootIndent) {
break
}
if (lineIndent > rootIndent) {
continue
}
let option = line.trim()
let pick = false
if (option.includes(':')) {
option = option.split(':')[0].trim()
pick = true
}
let disabled = false
// if next line has doesn't have a greater indent, disable the option and make it gray
if (!pick && i + 1 < lines.length) {
let nextLineIndent = indentation(lines[i + 1])
if (nextLineIndent <= rootIndent) {
disabled = true
}
}
let classes = `class="option
${disabled ? 'disabled' : ''}
${pick ? 'pick' : ''}
${(selected != null && i == selected) ? 'selected' : ''}
"`
let click = `${disabled ? '' : `onclick="select(${i}, ${pick})"`}`
html += `<div ${classes} ${click}>${option}</div>`
}
let top = indexPath.length < 2
let from = top ? 'tungs' : lines[rootIndex - 1].trim()
let back = `
<div class="option back" onclick="back()">
<div style="">
<img class="icon-btn" src="icons/back.svg" style="margin: 0; transform: rotate(${top ? 45 : 0}deg);">
<div style="margin-top: 16px; font-size: 24px; font-weight: bold;">${from}</div>
</div>
<div><a href="https://github.com/spatialfree/tung-tap">contribute</a></div>
</div>`
let head = `
<div style="display: flex; justify-content: space-around; align-items: center; padding: 0 10px; height: 36px; background-color: #ffb5dd; color: #fff;">
<div style="font-weight: bold;">tung-tap</div>
<img onclick="$('#nav').toggle()" class="icon-btn" src="icons/tungs.svg">
</div>`
let links = `
<div id="links" style="
padding: 10px 20px;
margin: 0px;
font-size: 14px;
background-color: #ff79a1;
color: #79394f;
">
Find more character codes in
<a target="_blank" href="https://unicode.org/charts/">unicode.org</a>
reference charts
or sketching in <a target="_blank" href="https://shapecatcher.com/">shapecatcher.com</a>
drawbox search!
</div>`
$('#nav').html(links + back + html)
}
function back() {
if (indexPath.length == 1) {
$('#nav').toggle()
return
}
indexPath.pop()
nav()
}
var selected = null
function select(index, pick) {
let line = lines[index]
if (pick) {
$('#tung').html(line.split(':')[1].trim())
layout()
selected = index
} else {
indexPath.push(index + 1)
}
nav()
}
function getLines(divId) {
var div = document.getElementById(divId)
var lineHeight = parseInt(window.getComputedStyle(div).getPropertyValue('line-height'))
var height = div.getBoundingClientRect().height
var lineCount = Math.floor(height / lineHeight)
var newLines = []
for (var i = 0; i < lineCount; i++) {
// var lineTop = i * lineHeight
// var lineBottom = (i + 1) * lineHeight
var lineText = div.innerText.substring(
div.getClientRects()[i].left - div.getClientRects()[0].left,
div.getClientRects()[i].right - div.getClientRects()[0].left
)
newLines.push(lineText)
}
return newLines
}
function layout() {
// keymap (tung is the proprietary name)
let tungText = $('#tung').html() //.replace('\r\n', '\n')
tungText = tungText.replace(/<div>/g, '\n').replace(/<\/div>/g, '')
if (tung != tungText) {
tung = tungText
localStorage.setItem('tung', tung)
}
// console.log(tungText)
let html = ``
let rows = tung.split('\n')
rows.forEach(row => {
row = row.trim()
if (row.length > 0) {
html += `<div class="row">\n`
let keys = row.split(' ')
keys.forEach(key => {
// print the newkey string
// and the unicode character
html += `<div class="key" charcode="${key}" ${topToggle ? `style="position: relative; padding-top: 10px;` : ``}">
<span style="
font-family: 'DM Mono', monospace;
font-size: 12px;
line-height: 12px;
position: absolute;
top: 2px; left: 0; right: 0;
color: #aaa;
${topToggle ? `` : `display: none;`}
">${key}</span>
${String.fromCharCode("0x" + key)}
</div>\n`
})
html += `</div>\n`
}
})
$('#keyboard').html(html)
$('.key').click(key => {
// override default behavior
key.preventDefault()
// if text is not focused then set the cursor to the end of $('#text')
let textEl = $('#text')
if (document.activeElement != textEl[0] && window.getSelection().rangeCount == 0) {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(textEl[0])
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
}
textEl.focus()
// insert key character into contenteditable div $('#text') at cursor position
let char = String.fromCharCode("0x" + $(key.target).attr('charcode'))
insertTextAtCursor(char)
// navigator.vibrate(20);
// sfx.play()
})
}
function insertTextAtCursor(text) {
let selection = window.getSelection()
let range = selection.getRangeAt(0)
range.deleteContents()
let node = document.createTextNode(text)
range.insertNode(node)
range.setStartAfter(node)
range.setEndAfter(node)
selection.removeAllRanges()
selection.addRange(range)
}
function copyToClipboard(id) {
var textEl = $(id)
// select all the text on the contenteditable div textEl to show it was copied
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(textEl[0])
selection.removeAllRanges()
selection.addRange(range)
if (!navigator.clipboard) {
console.error('Clipboard API not supported');
return;
}
navigator.clipboard.writeText(textEl.html())
.then(() => {
console.log('Text copied to clipboard')
})
.catch((err) => {
console.error('Failed to copy text: ', err)
})
}
// const newPosition = toolbar.getBoundingClientRect().top
// toolbar.classList.add('down')
function getVisibleHeight() {
var pixelRatio = window.devicePixelRatio || 1
var viewportHeight = window.visualViewport.height * pixelRatio
var windowHeight = window.innerHeight * pixelRatio
var keyboardHeight = viewportHeight - windowHeight
// $('#tung').html(`viewport ${viewportHeight}px | window ${windowHeight}px | keyboard ${keyboardHeight}px`)
return Math.abs(keyboardHeight) / pixelRatio || 0
}
var out = 400;
function loop(timestamp) {
var delta = timestamp - lastRender
time += delta
let height = getVisibleHeight()
// use the bottom of the #tung element position to use for the body top
// to be able to toggle the top of the page
let top = $('#tung').outerHeight(true) //+ $('#links').outerHeight(true) // + $('#home').outerHeight(true)
if (topToggle)
top *= 0
// lerp out to top
if (time > 100) {
out = lerp(out, top, 0.01 * delta)
}
// 'bottom': height + 'px',
// $('body').css({
// 'top': -out + 'px'
// })
// $('#keyboard').css({
// 'bottom': height + 'px',
// })
// check if need to backup the text
let text = $('#text').html()
if (text != lastText) {
localStorage.setItem('text', text)
lastText = text
}
lastRender = timestamp
window.requestAnimationFrame(loop)
}
var lastText = ''
var lastRender = 0
var time = 0
window.requestAnimationFrame(loop)
let ua = navigator.userAgent.toLowerCase()
let safari = ua.indexOf('safari') != -1 && !(ua.indexOf('chrome') > -1)
// dev toggle override
// safari = !safari
$('html').css('--bg', safari ? '214, 216, 221' : '16, 16, 16')
$('html').css('--fg', safari ? '214, 216, 221' : '240, 240, 240')
$('html').css('--key', safari ? '255, 255, 255' : '48, 48, 48')
$('html').css('--key-height', safari ? '40px' : '48px')
$('html').css('--key-radius', safari ? '5px' : '8px')
$('html').css('--txt', safari ? '0, 0, 0' : '255, 255, 255')
$('html').css('--txt-size', safari ? '22px' : '26px')
$('html').css('--field', safari ? '255, 255, 255' : '0, 0, 0')
$('html').css('--gap', safari ? '6px' : '5px')
window.addEventListener("beforeinstallprompt", (e) => {
console.log('beforeinstallprompt')
// // Prevent Chrome 67 and earlier from automatically showing the prompt
// e.preventDefault()
// // Stash the event so it can be triggered later.
// deferredPrompt = e
// // // Update UI to notify the user they can add to home screen
// // addBtn.style.display = "block"
// // addBtn.addEventListener("click", (e) => {
// // // hide our user interface that shows our A2HS button
// // addBtn.style.display = "none"
// // })
// // Show the prompt
// deferredPrompt.prompt()
// // Wait for the user to respond to the prompt
// deferredPrompt.userChoice.then((choiceResult) => {
// if (choiceResult.outcome === "accepted") {
// console.log("User accepted the A2HS prompt")
// } else {
// console.log("User dismissed the A2HS prompt")
// }
// deferredPrompt = null
// })
})
function lerp(a, b, n) {
return (1 - n) * a + n * b
}

BIN
sfx-click.ogg Normal file

Binary file not shown.

215
style.css Normal file
View file

@ -0,0 +1,215 @@
@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=DM+Mono&display=swap');
:root {
/* --bg: 16, 16, 16;
--fg: 240, 240, 240;
--key: 32, 32, 32;
--txt: 255, 255, 255;
--field: 255, 255, 255; */
}
* {
/* all: unset; */
outline: none;
/* box-sizing: border-box; */
}
/* *::selection {
background-color: rgba(255, 255, 255, 0.333);
} */
/* *::marker {
color: rgb(var(--txt));
} */
html {
/* font-family: sans-serif; */
font-family: 'Atkinson Hyperlegible', sans-serif;
font-size: 16px;
background-color: rgb(var(--field));
margin: 0;
box-sizing: border-box;
height: 100%;
}
body {
display: flex;
flex-direction: column;
/* start from bottom */
justify-content: flex-end;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgb(var(--field));
max-width: 800px;
margin: 0 auto;
overflow: hidden;
touch-action: none;
}
/* hide scrollbar */
*::-webkit-scrollbar {
display: none;
}
a {
text-decoration: underline;
}
input,
textarea,
button,
select,
div,
a {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
#nav {
background-color: rgb(var(--field));
color: rgb(var(--txt));
overflow-y: auto;
max-height: -webkit-fill-available;
/* margin-top: 120px; */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
/* opacity: 0.999; weird fix... */
padding-bottom: 500px;
}
.option {
padding: 12px;
cursor: pointer;
user-select: none;
background-color: rgb(var(--bg));
border-top: 0.5px solid #80808042;
}
.option:nth-child(2n) {
background-color: rgb(var(--field));
}
.option.pick {
font-weight: bold;
}
.option.selected {
text-decoration: underline;
}
.option.disabled {
color: rgba(var(--txt), 0.333);
cursor: default;
}
.back {
display: flex;
justify-content: space-between;
align-items: baseline;
}
/* textarea {
font-family: sans-serif;
font-size: 16px;
line-height: 1.5;
padding: 1.5rem 1rem;
width: 100%;
border: 0;
border-radius: 0;
margin: 0;
box-sizing: border-box;
outline: none;
resize: none;
overflow: auto;
-webkit-overflow-scrolling: touch;
height: -webkit-fill-available;
background-color: rgb(var(--field));
color: rgb(var(--txt));
} */
#keyboard {
display: block;
margin-bottom: 0px;
background-color: rgb(var(--bg));
z-index: 100;
user-select: none;
border-top: 0.5px solid rgba(128, 128, 128, 0.333);
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1);
/* position: fixed;
bottom: 0; left: 0; right: 0; */
}
/* transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); */
.row {
display: flex;
justify-content: center;
flex-direction: row;
gap: var(--gap);
margin: 9px 3px;
}
.key {
font-family: sans-serif;
font-size: var(--txt-size);
line-height: var(--key-height);
display: inline-block;
flex: 1;
max-width: 64px;
height: var(--key-height);
text-align: center;
border-top: 0.5px solid rgba(255, 255, 255, 0);
margin-bottom: -0.5px;
border-radius: var(--key-radius);
background-color: rgba(var(--key));
color: rgb(var(--txt));
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.333);
/* box-shadow: 0 0 0.5rem 0rem rgba(255, 255, 255, 0.0) inset; */
transition: 0.1s;
transition-timing-function: cubic-bezier(.51,.12,0,-0.27);
}
.key:active {
transition: 0.0s;
/* transition-timing-function: cubic-bezier(0.1, 1, 0.77, 0.62); */
border-color: rgba(255, 255, 255, 0.333);
box-shadow: 0 7px 4px 0 rgba(0, 0, 0, 0.333);
transform: translateY(-6px);
/* box-shadow: 0 0px 6px 0 rgba(0, 0, 0, 0.333) inset;
transform: translateY(2.0px); */
/* flex: 1.2; */
}
.active {
background-color: red;
}
.icon-btn {
display: block;
width: 24px;
height: 24px;
margin: 12px;
cursor: pointer;
}
/* #copybtn:active {
transition: 0.1s;
box-shadow: 0 0 0.5rem 0rem rgba(0, 0, 0, 0.333) inset;
} */

7
tungs.txt Normal file
View file

@ -0,0 +1,7 @@
Misc.
Test : 201C 2014 00D7 00B0 00B7 2022 221E 00B1 2023 201D
Salishan
Bella Coola
Coast Salish
Interior Salish
Spokane : 0142 019B 0323 02B7 203F 0301 030C 0294 0295 0313