Files
roll20-character-sheets/backbone/source/scaffold/scripts/attribute_proxy.js
T

248 lines
9.7 KiB
JavaScript

/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033*/
//# Attribute Obj Proxy handler
const createAttrProxy = function(attrs){
//creates a proxy for the attributes object so that values can be worked with more easily.
const getCascObj = function(event,casc){
const eventName = event.triggerName || event.sourceAttribute;
let typePrefix = eventName.startsWith('clicked:') ?
'act_' :
event.removedInfo ?
'fieldset_' :
'attr_';
let cascName = `${typePrefix}${eventName.replace(/(?:removed|clicked):/,'')}`;
let cascObj = casc[cascName];
if(cascObj && typePrefix === 'attr_'){
cascObj.previousValue = event.previousValue;
}
return cascObj;
};
const triggerFunctions = function(obj,attributes,sections,casc){
if(obj.triggeredFuncs && obj.triggeredFuncs.length){
debug(`triggering functions for ${obj.name}`);
obj.triggeredFuncs && obj.triggeredFuncs.forEach(func=>funcs[func] ?
funcs[func]({trigger:obj,attributes,sections,casc}) :
debug(`!!!Warning!!! no function named ${func} found. Triggered function not called for ${obj.name}`,true));
}
};
const initialFunction = function(obj,attributes,sections){
if(obj.initialFunc){
debug(`initial functions for ${obj.name}`);
funcs[obj.initialFunc] ?
funcs[obj.initialFunc]({trigger:obj,attributes,sections}) :
debug(`!!!Warning!!! no function named ${obj.initialFunc} found. Initial function not called for ${obj.name}`,true);
}
};
const alwaysFunctions = function(trigger,attributes,sections,casc){
Object.values(allHandlers).forEach((handler)=>{
handler({trigger,attributes,sections,casc});
});
};
const processChange = function({event,trigger,attributes,sections,casc}){
if(event && !trigger){
debug(`${event.sourceAttribute} change detected. No trigger found`);
return;
}
if(!attributes || !sections || !casc){
debug(`!!! Insufficient arguments || attributes > ${!!attributes} | sections > ${!!sections} | casc > ${!!casc} !!!`);
return;
}
debug({trigger});
if(event){
debug('checking for initial & always functions');
alwaysFunctions(trigger,attributes,sections,casc);//Functions that should be run for all events.
initialFunction(trigger,attributes,sections,casc);//functions that should only be run if the attribute was the thing changed by the user
}
if(trigger){
debug(`processing ${trigger.name}`);
triggerFunctions(trigger,attributes,sections,casc);
if(!event && trigger.calculation && funcs[trigger.calculation]){
attributes[trigger.name] = funcs[trigger.calculation]({trigger,attributes,sections,casc});
}else if(trigger.calculation && !funcs[trigger.calculation]){
debug(`K-Scaffold Error: No function named ${trigger.calculation} found`);
}
if(Array.isArray(trigger.affects)){
attributes.queue.push(...trigger.affects);
}
}
attributes.set({attributes,sections,casc});
};
const attrTarget = {
updates:{},
attributes:{...attrs},
repOrders:{},
queue: [],
casc:{},
alwaysFunctions,
processChange,
triggerFunctions,
initialFunction,
getCascObj
};
const attrHandler = {
get:function(obj,prop){//gets the most value of the attribute.
//If it is a repeating order, returns the array, otherwise returns the update value or the original value
if(prop === 'set'){
return function(){
let {attributes,sections,casc,callback,vocal} = arguments[0] ? arguments[0] : {};
if(attributes && attributes.queue.length && sections && casc){
let triggerName = attributes.queue.shift();
let trigger = getCascObj({sourceAttribute:triggerName},casc);
attributes.processChange({trigger,attributes,sections,casc});
}else{
debug({updates:obj.updates});
let trueCallback = Object.keys(obj.repOrders).length ?
function(){
Object.entries(obj.repOrders).forEach(([section,order])=>{
_setSectionOrder(section,order,)
});
callback && callback();
}:
callback;
Object.keys(obj.updates).forEach((key)=>obj.attributes[key] = obj.updates[key]);
const update = obj.updates;
obj.updates = {};
set(update,vocal,trueCallback);
}
}
}else if(Object.keys(obj).some(key=>key===prop)){
return Reflect.get(...arguments)
}else{
let retValue;
switch(true){
case obj.repOrders.hasOwnProperty(prop):
retValue = obj.repOrders[prop];
break;
case obj.updates.hasOwnProperty(prop):
retValue = obj.updates[prop];
break;
default:
retValue = obj.attributes[prop];
break;
}
let cascRef = `attr_${prop.replace(/(repeating_[^_]+_)[^_]+/,'$1\$X')}`;
let numRetVal = +retValue;
if(!Number.isNaN(numRetVal) && retValue !== ''){
retValue = numRetVal;
}else if(cascades[cascRef] && (typeof cascades[cascRef].defaultValue === 'number' || cascades[cascRef].type === 'number')){
retValue = cascades[cascRef].defaultValue;
}
return retValue;
}
},
set:function(obj,prop,value){
//Sets the value. Also verifies that the value is a valid attribute value
//e.g. not undefined, null, or NaN
if(value || value===0 || value===''){
if(/reporder|^repeating_[^_]+$/.test(prop)){
let section = prop.replace(/_reporder_/,'');
obj.repOrders[section] = value;
}else if(`${obj.attributes}` !== `${value}` ||
(obj.updates[prop] && `${obj.updates}` !== `${value}`)
){
obj.updates[prop] = value;
}
}else{
debug(`!!!Warning: Attempted to set ${prop} to an invalid value:${value}; value not stored!!!`);
}
return true;
},
deleteProperty(obj,prop){
//removes the property from the original attributes, updates, and the reporders
Object.keys(obj).forEach((key)=>{
delete obj[key][prop.toLowerCase()];
});
}
};
return new Proxy(attrTarget,attrHandler);
};
/**
* Function that registers a function for being called via the funcs object. Returns true if the function was successfully registered, and false if it could not be registered for any reason.
* @param {object} funcObj - Object with keys that are names to register functions under and values that are functions.
* @param {object} optionsObj - Object that contains options to use for this registration.
* @param {string[]} optionsObj.type - Array that contains the types of specialized functions that apply to the functions being registered. Valid types are `"opener"`, `"updater"`, and `"default"`. `"default"` is always used, and never needs to be passed.
* @returns {boolean} - True if the registration succeeded, false if it failed.
* @example
* //Basic Registration
* const myFunc = function({trigger,attributes,sections,casc}){};
* k.registerFuncs({myFunc});
*
* //Register a function to run on sheet open
* const openFunc = function({trigger,attributes,sections,casc}){};
* k.registerFuncs({openFunc},{type:['opener']})
*
* //Register a function to run on all events
* const allFunc = function({trigger,attributes,sections,casc}){};
* k.registerFuncs({allFunc},{type:['all']})
*/
const registerFuncs = function(funcObj,optionsObj = {}){
if(typeof funcObj !== 'object' || typeof optionsObj !== 'object'){
debug(`!!!! K-scaffold error: Improper arguments to register functions !!!!`);
return false;
}
const typeArr = optionsObj.type ? ['default',...optionsObj.type] : ['default'];
const typeSwitch = {
'opener':openHandlers,
'updater':updateHandlers,
'new':initialSetups,
'all':allHandlers,
'default':funcs
};
let setState;
Object.entries(funcObj).map(([prop,value])=>{
typeArr.forEach((type)=>{
if(typeSwitch[type][prop]){
debug(`!!! Duplicate function name for ${prop} as ${type}!!!`);
setState = false;
}else if(typeof value === 'function'){
typeSwitch[type][prop] = value;
setState = setState !== false ? true : false;
}else{
debug(`!!! K-scaffold error: Function registration requires a function. Invalid value to register as ${type} !!!`);
setState = false;
}
});
});
return setState;
};
kFuncs.registerFuncs = registerFuncs;
const setActionCalls = function({attributes,sections}){
actionAttributes.forEach((base)=>{
let [section,,field] = k.parseTriggerName(base);
let fieldAction = field.replace(/_/g,'-');
if(section){
sections[section].forEach((id)=>{
attributes[`${section}_${id}_${field}`] = `%{${attributes.character_name}|${section}_${id}_${fieldAction}}`;
});
}else{
attributes[`${field}`] = `%{${attributes.character_name}|${fieldAction}}`;
}
});
};
funcs.setActionCalls = setActionCalls;
kFuncs.setActionCalls = setActionCalls;
/**
* Function to call a function previously registered to the funcs object. May not be used that much. Either returns the function or null if no function exists.
* @param {string} funcName - The name of the function to invoke.
* @param {...any} args - The arguments to call the function with.
* @returns {any}
* @example
* //Call myFunc with two arguments
* k.callFunc('myFunc','an argument','another argument');
*/
const callFunc = function(funcName,...args){
if(funcs[funcName]){
debug(`calling ${funcName}`);
return funcs[funcName](...args);
}else{
debug(`Invalid function name: ${funcName}`);
return null;
}
};
kFuncs.callFunc = callFunc;