mirror of
https://github.com/slynn1324/tinypin
synced 2025-12-12 14:25:35 +00:00
306 lines
8.7 KiB
JavaScript
306 lines
8.7 KiB
JavaScript
// this currently will bind all fields with 'data-bind' attributes
|
|
// to the 'store'.
|
|
|
|
app.addSetter('databind.onInput', (data, bindPath, value) => {
|
|
// console.log(`binding ${bindPath} to ${value}`);
|
|
put(data, bindPath, value);
|
|
|
|
/*!
|
|
* Add items to an object at a specific path
|
|
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
|
* @param {Object} obj The object
|
|
* @param {String|Array} path The path to assign the value to
|
|
* @param {*} val The value to assign
|
|
*/
|
|
function put(obj, path, val) {
|
|
|
|
/**
|
|
* If the path is a string, convert it to an array
|
|
* @param {String|Array} path The path
|
|
* @return {Array} The path array
|
|
*/
|
|
var stringToPath = function (path) {
|
|
|
|
// If the path isn't a string, return it
|
|
if (typeof path !== 'string') return path;
|
|
|
|
// Create new array
|
|
var output = [];
|
|
|
|
// Split to an array with dot notation
|
|
path.split('.').forEach(function (item, index) {
|
|
|
|
// Split to an array with bracket notation
|
|
item.split(/\[([^}]+)\]/g).forEach(function (key) {
|
|
|
|
// Push to the new array
|
|
if (key.length > 0) {
|
|
output.push(key);
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return output;
|
|
|
|
};
|
|
|
|
// Convert the path to an array if not already
|
|
path = stringToPath(path);
|
|
|
|
// Cache the path length and current spot in the object
|
|
var length = path.length;
|
|
var current = obj;
|
|
|
|
// Loop through the path
|
|
path.forEach(function (key, index) {
|
|
|
|
// Check if the assigned key shoul be an array
|
|
var isArray = key.slice(-2) === '[]';
|
|
|
|
// If so, get the true key name by removing the trailing []
|
|
key = isArray ? key.slice(0, -2) : key;
|
|
|
|
// If the key should be an array and isn't, create an array
|
|
if (isArray && Object.prototype.toString.call(current[key]) !== '[object Array]') {
|
|
current[key] = [];
|
|
}
|
|
|
|
// If this is the last item in the loop, assign the value
|
|
if (index === length - 1) {
|
|
|
|
// If it's an array, push the value
|
|
// Otherwise, assign it
|
|
if (isArray) {
|
|
current[key].push(val);
|
|
} else {
|
|
current[key] = val;
|
|
}
|
|
}
|
|
|
|
// Otherwise, update the current place in the object
|
|
else {
|
|
|
|
// If the key doesn't exist, create it
|
|
if (!current[key]) {
|
|
current[key] = {};
|
|
}
|
|
|
|
// Update the current place in the object
|
|
current = current[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
Reef.databind = function(reef){
|
|
|
|
let el = document.querySelector(reef.elem);
|
|
let store = reef.store;
|
|
|
|
if ( !store ){
|
|
console.error("Databind only works when using a store.");
|
|
return;
|
|
}
|
|
|
|
// bind all elements on the page that have a data-bind item
|
|
const bindData = debounce(() => {
|
|
let elems = el.querySelectorAll("[data-bind]");
|
|
|
|
for ( let i = 0; i < elems.length; ++i ){
|
|
let elem = elems[i];
|
|
let bindName = elem.getAttribute("data-bind");
|
|
|
|
if ( bindName ){
|
|
let val = get(store.data, bindName, "");
|
|
|
|
// dom values are strings, convert back so we can compare safely
|
|
if ( typeof(val) == 'array' ){
|
|
let strs = [];
|
|
for ( let i = 0; i < val.length; ++i ){
|
|
strs.push(val[i].toString());
|
|
}
|
|
val = strs;
|
|
} else {
|
|
val = val.toString();
|
|
}
|
|
|
|
// multiple selects need special handling
|
|
if ( elem.tagName == "SELECT" && elem.matches("[multiple]") ){
|
|
let options = elem.querySelectorAll("option");
|
|
for ( let i = 0; i < options.length; ++i ){
|
|
if ( val.indexOf(options[i].value) > -1 ){
|
|
options[i].selected = true;
|
|
}
|
|
}
|
|
} else if ( elem.type == "radio" || elem.type == "checkbox" ){
|
|
if ( elem.value == val.toString() ){
|
|
elem.checked = true;
|
|
} else {
|
|
elem.checked = false;
|
|
}
|
|
} else {
|
|
elem.value = val;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
el.addEventListener('input', (evt) => {
|
|
|
|
let target = evt.target;
|
|
let bindPath = target.getAttribute("data-bind");
|
|
|
|
if (bindPath) {
|
|
let val = target.value;
|
|
|
|
if ( target.type == "checkbox" ){
|
|
if ( ! target.checked ){
|
|
val = null;
|
|
}
|
|
}
|
|
|
|
// multiple selects need special handling
|
|
if ( target.tagName == 'SELECT' && target.matches("[multiple]") ){
|
|
val = [];
|
|
let options = target.querySelectorAll("option");
|
|
for ( let i = 0; i < options.length; ++i ){
|
|
if ( options[i].selected ){
|
|
val.push(parseString(options[i].value));
|
|
}
|
|
}
|
|
}
|
|
|
|
// for checkbox, radio, and select fields - map numeric and boolean values to
|
|
// actual types instead of strings
|
|
else if ( target.type == 'checkbox' || target.type == 'radio' ){
|
|
val = parseString(val);
|
|
}
|
|
|
|
store.do('databind.onInput', bindPath, val);
|
|
//put(store.data, bindPath, val);
|
|
}
|
|
});
|
|
|
|
el.addEventListener('render', (evt) => {
|
|
bindData();
|
|
});
|
|
|
|
// convert booleans and numbers
|
|
function parseString(str){
|
|
if ( str == "true" ){
|
|
return true;
|
|
} else if ( str == "false" ){
|
|
return false;
|
|
} else {
|
|
return parseFloat(str) || str;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Debounce functions for better performance
|
|
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
|
* @param {Function} fn The function to debounce
|
|
*/
|
|
function debounce(fn) {
|
|
|
|
// Setup a timer
|
|
var timeout;
|
|
|
|
// Return a function to run debounced
|
|
return function () {
|
|
|
|
// Setup the arguments
|
|
var context = this;
|
|
var args = arguments;
|
|
|
|
// If there's a timer, cancel it
|
|
if (timeout) {
|
|
window.cancelAnimationFrame(timeout);
|
|
}
|
|
|
|
// Setup the new requestAnimationFrame()
|
|
timeout = window.requestAnimationFrame(function () {
|
|
fn.apply(context, args);
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
/*!
|
|
* Get an object value from a specific path
|
|
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
|
|
* @param {Object} obj The object
|
|
* @param {String|Array} path The path
|
|
* @param {*} def A default value to return [optional]
|
|
* @return {*} The value
|
|
*/
|
|
function get(obj, path, def) {
|
|
|
|
/**
|
|
* If the path is a string, convert it to an array
|
|
* @param {String|Array} path The path
|
|
* @return {Array} The path array
|
|
*/
|
|
var stringToPath = function (path) {
|
|
|
|
// If the path isn't a string, return it
|
|
if (typeof path !== 'string') return path;
|
|
|
|
// Create new array
|
|
var output = [];
|
|
|
|
// Split to an array with dot notation
|
|
path.split('.').forEach(function (item) {
|
|
|
|
// Split to an array with bracket notation
|
|
item.split(/\[([^}]+)\]/g).forEach(function (key) {
|
|
|
|
// Push to the new array
|
|
if (key.length > 0) {
|
|
output.push(key);
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return output;
|
|
|
|
};
|
|
|
|
// Get the path as an array
|
|
path = stringToPath(path);
|
|
|
|
// Cache the current object
|
|
var current = obj;
|
|
|
|
// For each item in the path, dig into the object
|
|
for (var i = 0; i < path.length; i++) {
|
|
|
|
// If the item isn't found, return the default (or null)
|
|
if (!current[path[i]]) return def;
|
|
|
|
// Otherwise, update the current value
|
|
current = current[path[i]];
|
|
|
|
}
|
|
|
|
return current;
|
|
|
|
};
|
|
|
|
}
|