Skip to content

Instantly share code, notes, and snippets.

@eeropic
Created November 10, 2021 20:51
Show Gist options
  • Save eeropic/78725e753cfd66e03c1c5741fd40feb9 to your computer and use it in GitHub Desktop.
Save eeropic/78725e753cfd66e03c1c5741fd40feb9 to your computer and use it in GitHub Desktop.
touchosc custom properties
<!-- https://forum.freecodecamp.org/t/nesting-custom-web-components/326322/4 -->
<template id="toscPropertyTemplate"><slot></slot><slot></slot></template>
<template id="toscKeyTemplate"></template>
<template id="toscValueTemplate"></template>
<div id="editor">
</div>
<div id="toolbar">
<div id="file-controls">
<div id="drop-zone">
<span id="drop-zone-text">Drop a .TOSC file here</span>
<input type="file" id="tosc-file" name="tosc_file" accept="binary/tosc" label="ktekteo">
<label id="tosc-file-label" for="tosc-file">Or click to upload .TOSC</label>
</div>
<a id="download-link" href="#" target="_blank">Finally, drag this to save as .TOSC</a>
</div>
<div id="properties-wrapper">
</div>
</div>
<div id="update-buttons">
<button id="update-xml"><< Update --</button>
<button id="update-controls">-- Update >></button>
</div>
var liveUpdate = true
if(liveUpdate){
document.getElementById("update-buttons").style = "display:none;"
}
const tosc_doc = `<?xml version='1.0' encoding='UTF-8'?><lexml version='2'><node type='GROUP'><properties><property type='s'><key>metaCreator</key><value>Created with an unofficial .tosc MK2 web editor</value></property><property type='s'><key>metaComments</key><value>Use at your own risk! eero.pitkanen@gmail.com</value></property><property type='c'><key>color</key><value><r>0</r><g>0</g><b>0</b><a>1</a></value></property><property type='r'><key>frame</key><value><x>0</x><y>0</y><w>640</w><h>860</h></value></property><property type='b'><key>myBoolean</key><value>1</value></property><property type='f'><key>myFloat</key><value>1.5</value></property><property type='i'><key>myInt</key><value>4</value></property><property type='s'><key>script</key><value>--hidden custom properties:</value></property></properties><values></values><children></children></node></lexml>`
const compToHex = (c) => (hex = c.toString(16), hex.length == 1 ? "0" + hex : hex);
const rgbToHex = (r, g, b) => "#" + compToHex(r) + compToHex(g) + compToHex(b);
const hexToRgb = (hex) => (
res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex),
res ? {r:parseInt(res[1],16),g:parseInt(res[2],16),b:parseInt(res[3],16)} : null
);
XML = {
parse: (string, type = 'text/xml') => new DOMParser().parseFromString(string, type),
stringify: DOM => new XMLSerializer().serializeToString(DOM),
// (c) https://stackoverflow.com/a/49458964
prettify: (xml,tab) => {
let formatted = '', indent= '';
tab = tab || '\t';
xml.split(/>\s*</).forEach(function(node) {
if (node.match( /^\/\w/ )) indent = indent.substring(tab.length); // decrease indent by one 'tab'
formatted += indent + '<' + node + '>\r\n';
if (node.match( /^<?\w[^>]*[^\/]$/ )) indent += tab; // increase indent
});
return formatted.substring(1, formatted.length-3);
}
}
var editor = ace.edit("editor");
editor.setTheme("ace/theme/chrome");
editor.getSession().setMode("ace/mode/xml");
editor.getSession().setValue(XML.prettify(tosc_doc.match(/<properties>[\s\S]*?<\/properties>/)[0]))
updateControls();
editor.getSession().on('change', function(e) {
if (editor.curOp && editor.curOp.command.name){
if(liveUpdate){
console.log('user change')
updateControls()
}
}
else {
console.log('other change')
}
});
var xmlStr;
var tosc_filename = "edited.tosc"
function onFileLoad(){
var arrayBuffer = this.result
var array = new Uint8Array(arrayBuffer)
console.log(array)
var tosc_xml;
var result;
try {
const result = pako.inflate(array);
tosc_xml = String.fromCharCode.apply(null, result);
} catch (err) {
tosc_xml = String.fromCharCode.apply(null, array);
}
xmlStr = tosc_xml
var tosc_properties = xmlStr.match(/<properties>[\s\S]*?<\/properties>/)[0]
tosc_properties = tosc_properties.replace(/\<!\[CDATA\[(\w*)\]\]\>/g,"$1")
editor.getSession().setValue(XML.prettify(tosc_properties))
document.getElementById('properties-wrapper').innerHTML=addToscPrefix(tosc_properties)
}
function dropHandler(ev) {
console.log(ev)
ev.preventDefault();
if (ev.dataTransfer.items) {
var file = ev.dataTransfer.items[0].getAsFile();
tosc_filename = file.name;
document.getElementById('drop-zone-text').innerHTML = file.name
var reader = new FileReader();
reader.onload = onFileLoad
reader.readAsArrayBuffer(file);
}
}
const dragOverEnterHandler = e => {
e.preventDefault()
e.stopPropagation()
}
function fileInputHandler(e){
var reader = new FileReader();
reader.onload = onFileLoad
reader.readAsArrayBuffer(this.files[0]);
}
var file_input = document.getElementById("tosc-file")
var file_dropzone = document.getElementById("drop-zone")
file_dropzone.addEventListener('drop',dropHandler)
file_dropzone.addEventListener('dragover',dragOverEnterHandler)
file_dropzone.addEventListener('dragenter',dragOverEnterHandler)
file_input.addEventListener('change', fileInputHandler)
document.getElementById('download-link').addEventListener("dragstart", function (e) {
let props = editor.getSession().getValue();
let docstring = tosc_doc.replace(/<properties>[\s\S]*?<\/properties>/,props);
var data = btoa(docstring)
e.dataTransfer.setData("DownloadURL", `text/xml:${tosc_filename}:data:image/png;base64,${data}`);
});
function updateDownload(){
let elem = document.getElementById('download-link')
let props = editor.getSession().getValue();
let docstring = tosc_doc.replace(/<properties>[\s\S]*?<\/properties>/,props);
elem.download = tosc_filename || "mypatch.tosc"
var blob = new Blob([docstring], {type: 'text/xml'});
elem.href = window.URL.createObjectURL(blob);
}
customElements.define('tosc-property', class extends HTMLElement {
constructor(){
super();
const shadow = this.attachShadow({ mode: 'open' })
shadow.appendChild(toscPropertyTemplate.content.cloneNode(true));
const type = this.getAttribute('type')
const slot = this.shadowRoot.querySelector('slot');
slot.addEventListener('slotchange', (event) => {
const children = event.target.assignedElements();
children.forEach(child => {
if (child.slot === 'key') console.log(child.innerHTML)
else if (child.slot === 'value') console.log(child.innerHTML)
});
});
}
});
customElements.define('tosc-key', class extends HTMLElement {
constructor(){
super();
const shadow = this.attachShadow({mode: "open"});
const input= document.createElement('input')
input.setAttribute('value', this.innerHTML)
input.addEventListener('change', function(){
this.getRootNode().host.innerHTML = this.value
if(liveUpdate)updateXML();
})
shadow.appendChild(toscKeyTemplate.content.cloneNode(true));
shadow.appendChild(input)
}
});
toscInputTypes = {
b: 'checkbox',
i: 'number',
f: 'number',
s: 'string',
c: 'color'
}
customElements.define('tosc-value', class extends HTMLElement {
constructor(){
super();
let type = this.parentNode.getAttribute("type")
let value = parseFloat(this.innerHTML) || this.innerHTML
const shadow = this.attachShadow({mode: "open"})
// No suitable native input element for rectangles
if(type !== 'r'){
let input= document.createElement('input')
input.setAttribute('type', toscInputTypes[type])
if(type !== 'b' && type !== 'c')
input.setAttribute('value', value)
else input.checked = value == 1 ? true : false;
if(type == 'i')
input.setAttribute('step', 1)
if(type == 'c'){
//color
let r = parseFloat(this.querySelector('tosc-r').innerHTML) * 255
let g = parseFloat(this.querySelector('tosc-g').innerHTML) * 255
let b = parseFloat(this.querySelector('tosc-b').innerHTML) * 255
let a = parseFloat(this.querySelector('tosc-a').innerHTML) * 255
input.setAttribute('value',rgbToHex(r,g,b))
}
// event listeners
if(type == 'b'){
input.addEventListener('change', function(e){
this.getRootNode().host.innerHTML = this.checked == true ? 1 : 0;
if(liveUpdate)updateXML();
})
}
if(type == 'i' || type =='f' || type == 's'){
input.addEventListener('change', function(e){
this.getRootNode().host.innerHTML = this.value
if(liveUpdate)updateXML();
})
}
if(type == 'c'){
input.addEventListener('change', function(e){
let parent = this.getRootNode().host
this.setAttribute('value', this.value)
let rgb = hexToRgb(this.value)
console.log(this.getRootNode().host.childNodes)
parent.getElementsByTagName('tosc-r')[0].innerHTML = rgb.r / 255
parent.getElementsByTagName('tosc-g')[0].innerHTML = rgb.g / 255
parent.getElementsByTagName('tosc-b')[0].innerHTML = rgb.b / 255
if(liveUpdate)updateXML();
})
}
shadow.insertBefore(input,shadow.firstChild)
}
//rectangle
else {
let inputs = ['x','y','w','h']
inputs.forEach( i => {
let input = document.createElement('input')
let val = parseFloat(this.querySelector('tosc-'+i).innerHTML)
input.setAttribute('id', i)
input.setAttribute('type', 'number')
input.setAttribute('value', val)
input.addEventListener('change',function(e){
let parent = this.getRootNode().host
parent.getElementsByTagName('tosc-'+this.id)[0].innerHTML = this.value
if(liveUpdate)updateXML();
})
shadow.appendChild(input)
})
}
shadow.appendChild(toscValueTemplate.content.cloneNode(true))
}
});
function removeToscPrefix(str){
return str.replace(/\<tosc-/g,'<')
.replace(/\<\/tosc-/g,'</')
.replace('<div xmlns="http://www.w3.org/1999/xhtml" class="tosc-properties">','<properties>')
.replace('</div>','</properties>')
}
// i'm not proud of this
function addToscPrefix(str){
return str
.replace(/<properties>/g,'<div xmlns="http://www.w3.org/1999/xhtml" class="tosc-properties">')
.replace(/<property/g,'<tosc-property')
.replace(/<key/g,'<tosc-key')
.replace(/<value/g,'<tosc-value')
.replace(/<r/g,'<tosc-r')
.replace(/<g/g,'<tosc-g')
.replace(/<b/g,'<tosc-b')
.replace(/<a/g,'<tosc-a')
.replace(/<x/g,'<tosc-x')
.replace(/<y/g,'<tosc-y')
.replace(/<w/g,'<tosc-w')
.replace(/<h/g,'<tosc-h')
.replace(/<\/properties>/g,'</div>')
.replace(/<\/property/g,'</tosc-property')
.replace(/<\/key/g,'</tosc-key')
.replace(/<\/value/g,'</tosc-value')
.replace(/<\/r/g,'</tosc-r')
.replace(/<\/g/g,'</tosc-g')
.replace(/<\/b/g,'</tosc-b')
.replace(/<\/a/g,'</tosc-a')
.replace(/<\/x/g,'</tosc-x')
.replace(/<\/y/g,'</tosc-y')
.replace(/<\/w/g,'</tosc-w')
.replace(/<\/h/g,'</tosc-h')
}
function updateXML(){
var s = new XMLSerializer();
var tosc_properties = s.serializeToString(document.querySelector('.tosc-properties'))
editor.getSession().setValue(XML.prettify(removeToscPrefix(tosc_properties)))
updateDownload();
}
function updateControls(){
let tosc_properties = editor.getSession().getValue().replace(/\t/g,'');
document.getElementById('properties-wrapper').innerHTML=addToscPrefix(tosc_properties)
updateDownload();
}
document.getElementById('update-controls').addEventListener('click', function(e){
updateControls();
})
document.getElementById('update-xml').addEventListener('click', function(e){
updateXML();
})
/*
var s = new XMLSerializer();
var str = s.serializeToString(document.querySelector('.tosc-properties'))
console.log(removeToscPrefix(str))
*/
<script src="https://unpkg.com/pako@2.0.4/dist/pako.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.13/ace.js"></script>
:root {
font-family: Monaco, monospace;
font-size: 12px;
}
#tosc-file {
display: none;
}
#tosc-file-label {
border: 1px solid gray;
border-radius: 4px;
cursor: pointer;
margin-top: 4rem;
}
#update-buttons {
display:flex;
flex-direction: column;
position:absolute;
top: 30%;
right: 30rem;
width: 8rem;
gap: 2rem;
}
#toolbar {
display: flex;
flex-direction: column;
align-items: top;
position: absolute;
top: 0;
padding: 1rem;
gap: 10px;
bottom: 0;
right: 1rem;
border-left: 1px solid gray;
}
#file-controls {
display:flex;
flex-direction: column;
height: 30%;
}
#tosc_file {
}
#drop-zone {
text-align: center;
border: 1px dashed black;
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
height: 50%;
margin-bottom: 1rem;
padding: 2rem;
}
#download-link {
border: 1px solid black;
padding: 4px;
}
#editor {
position: absolute;
top: 3rem;
right: 35%;
bottom: 0;
left: 0;
}
.ace-chrome .ace_cdata {color: darkgray;}
.ace-chrome .ace_meta.ace_tag {color: gray;}
.ace-chrome span.ace_text.ace_xml {color: blue;}
tosc-property {
font-family: Monaco, monospace;
font-size: 12px;
display: flex;
margin-bottom: 0.5rem;
}
tosc-key {
font-family: inherit;
color: gray;
margin-right: 0.5rem;
}
tosc-value {
display:flex;
flex-direction: column;
font-family: inherit;
color: gray;
font-size: 0;
}
.tosc-properties {
display: flex;
flex-direction: column;
}
input[type="number"] {
max-width: 6rem;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment