commit e0d2e52c9334c9a6b406f5fbde9b7d5b6a069b79 Author: spatialfree Date: Mon Apr 24 06:59:47 2023 -0400 first commit diff --git a/icons/back.svg b/icons/back.svg new file mode 100644 index 0000000..29246da --- /dev/null +++ b/icons/back.svg @@ -0,0 +1,55 @@ + + + +image/svg+xml diff --git a/icons/copy.svg b/icons/copy.svg new file mode 100644 index 0000000..7741d9f --- /dev/null +++ b/icons/copy.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/salish-keyboard-icon.svg b/icons/salish-keyboard-icon.svg new file mode 100644 index 0000000..ccd7581 --- /dev/null +++ b/icons/salish-keyboard-icon.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/icons/tap.png b/icons/tap.png new file mode 100644 index 0000000..1ea906f Binary files /dev/null and b/icons/tap.png differ diff --git a/icons/tung.png b/icons/tung.png new file mode 100644 index 0000000..91fe5a7 Binary files /dev/null and b/icons/tung.png differ diff --git a/icons/tung.svg b/icons/tung.svg new file mode 100644 index 0000000..a9a60e5 --- /dev/null +++ b/icons/tung.svg @@ -0,0 +1,91 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/icons/tungs.svg b/icons/tungs.svg new file mode 100644 index 0000000..40dc468 --- /dev/null +++ b/icons/tungs.svg @@ -0,0 +1,57 @@ + + + +image/svg+xml diff --git a/index.html b/index.html new file mode 100644 index 0000000..a51fafd --- /dev/null +++ b/index.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + tung-tap + + + + + + + + + + +
+ +
+
+ +
+ + +
+ + +
+
+
+ +
+
+ + + + +
+ + + + + + \ No newline at end of file diff --git a/inobounce.js b/inobounce.js new file mode 100644 index 0000000..38ea515 --- /dev/null +++ b/inobounce.js @@ -0,0 +1,138 @@ +/*! iNoBounce - v0.2.0 +* https://github.com/lazd/iNoBounce/ +* Copyright (c) 2013 Larry Davis ; 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)); \ No newline at end of file diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..03d5cbd --- /dev/null +++ b/manifest.webmanifest @@ -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" +} \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..857978c --- /dev/null +++ b/script.js @@ -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 += `
${option}
` + } + + let top = indexPath.length < 2 + let from = top ? 'tungs' : lines[rootIndex - 1].trim() + let back = ` +
+
+ +
${from}
+
+
contribute
+
` + + let head = ` +
+
tung-tap
+ +
` + + let links = ` + ` + + $('#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(/
/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 += `
\n` + let keys = row.split(' ') + keys.forEach(key => { + // print the newkey string + // and the unicode character + html += `
+ ${key} + ${String.fromCharCode("0x" + key)} +
\n` + }) + html += `
\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 +} \ No newline at end of file diff --git a/sfx-click.ogg b/sfx-click.ogg new file mode 100644 index 0000000..f70ae62 Binary files /dev/null and b/sfx-click.ogg differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..e85206c --- /dev/null +++ b/style.css @@ -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; +} */ \ No newline at end of file diff --git a/tungs.txt b/tungs.txt new file mode 100644 index 0000000..4e791f9 --- /dev/null +++ b/tungs.txt @@ -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 \ No newline at end of file