/**
* Basic options registry
*/
fw.options = (function ($, currentFwOptions) {
/**
* An object of hints
*/
var allOptionTypes = {};
currentFwOptions.get = get;
currentFwOptions.getAll = getAll;
currentFwOptions.register = register;
currentFwOptions.__unsafePatch = __unsafePatch;
currentFwOptions.getOptionDescriptor = getOptionDescriptor;
currentFwOptions.startListeningToEvents = startListeningToEvents;
currentFwOptions.getContextOptions = getContextOptions;
currentFwOptions.findOptionInContextForPath = findOptionInContextForPath;
currentFwOptions.findOptionInSameContextFor = findOptionInSameContextFor;
/**
* fw.options.getValueForEl(element)
* .then(function (values, optionDescriptor) {
* // current values for option type
* console.log(values)
* })
* .fail(function () {
* // value extraction failed for some reason
* });
*/
currentFwOptions.getValueForEl = getValueForEl;
currentFwOptions.getContextValue = getContextValue;
return currentFwOptions;
/**
* get hint object for a specific type
*/
function get (type) {
return allOptionTypes[type] || allOptionTypes['fw-undefined'];
}
function getAll () {
return allOptionTypes;
}
/**
* Returns:
* el
* ID
* type
* isRootOption
* context
* nonOptionContext
*/
function getOptionDescriptor (el) {
var data = {};
if (! el) return null;
data.context = detectDOMContext(el);
data.el = findOptionDescriptorEl(el);
data.rootContext = findNonOptionContext(data.el);
data.id = $(data.el).attr('data-fw-option-id');
data.type = $(data.el).attr('data-fw-option-type');
data.isRootOption = isRootOption(data.el, findNonOptionContext(data.el));
data.hasNestedOptions = hasNestedOptions(data.el);
data.pathToTheTopContext = data.isRootOption
? []
: findPathToTheTopContext(data.el, findNonOptionContext(data.el));
return data;
}
function findOptionInSameContextFor (el, path) {
var rootContext = getOptionDescriptor(el).rootContext;
return findOptionInContextForPath(
rootContext, path
);
}
/**
* This receives a context (option as context works too)
* and returns the option descriptor which respects the path
*
* - form
* - .fw-backend-options-virtual-context
* - .fw-backend-option-descriptor
*
* path:
* id/other_id/another_one
*/
function findOptionInContextForPath (context, path) {
var pathToTheTop = path.split('/');
return pathToTheTop.reduce(function (currentContext, path, index) {
if (! currentContext) return false;
var elOrDescriptorForPath = _.compose(
index === pathToTheTop.length - 1
? getOptionDescriptor
: _.identity,
_.partial(
maybeFindFirstLevelOptionInContext,
currentContext
)
);
return elOrDescriptorForPath(path);
}, context);
function maybeFindFirstLevelOptionInContext (context, firstLevelId) {
return (getContextOptions(context).filter(
function (optionDescriptor) {
return optionDescriptor.id === firstLevelId;
}
)[0] || {}).el;
}
}
/**
* This receives a context (option as context works too)
* and returns the first level of options underneath it.
*
* - form
* - .fw-backend-options-virtual-context
* - .fw-backend-option-descriptor
*/
function getContextOptions (el) {
el = (el instanceof jQuery) ? el[0] : el;
if (! (
el.tagName === 'FORM'
||
el.classList.contains('fw-backend-options-virtual-context')
||
el.classList.contains('fw-backend-option-descriptor')
)) {
throw "You passed an incorrect context element."
}
return $(el)
.find('.fw-backend-option-descriptor')
.not(
$(el).find('.fw-backend-options-virtual-context .fw-backend-option-descriptor')
)
.toArray()
.map(getOptionDescriptor)
.filter(function (descriptor) {
return isRootOption(descriptor.el, el)
})
}
function getContextValue (el) {
var optionDescriptors = getContextOptions(el);
var promise = $.Deferred();
fw.whenAll(optionDescriptors.map(getValueForOptionDescriptor))
.then(function (valuesAsArray) {
var values = {};
optionDescriptors.map(function (optionDescriptor, index) {
values[optionDescriptor.id] = valuesAsArray[index].value;
});
promise.resolve({
valueAsArray: valuesAsArray,
optionDescriptors: optionDescriptors,
value: values
});
})
.fail(function () {
// TODO: pass a reason
promise.reject();
});
return promise;
}
function getValueForOptionDescriptor (optionDescriptor) {
var maybePromise = get(optionDescriptor.type).getValue(optionDescriptor)
var promise = maybePromise;
/**
* A promise has a then method usually
*/
if (! promise.then) {
promise = $.Deferred();
promise.resolve(maybePromise);
}
return promise;
}
function getValueForEl (el) {
return getValueForOptionDescriptor(getOptionDescriptor(el));
}
/**
* You are not registering here a full fledge class definition for an
* option type just like we have on backend. It is more of a hint on how
* to treat the option type on frontend. Everything should be working
* almost fine even if you don't provide any hints.
*
* interface:
*
* startListeningForChanges
* getValue
*/
function register (type, hintObject) {
// TODO: maybe start triggering events on option type register
if (allOptionTypes[type]) {
throw "Can't re-register an option type again";
}
allOptionTypes[type] = jQuery.extend(
{}, defaultHintObject(),
hintObject || {}
);
}
function __unsafePatch (type, hintObject) {
allOptionTypes[type] = jQuery.extend(
{}, defaultHintObject(),
(allOptionTypes[type] || {}),
hintObject || {}
);
}
/**
* This will be automatically called at each fw:options:init event.
* This will make each option type start listening to events
*/
function startListeningToEvents (el) {
// TODO: compute path up untill non-option context
el = (el instanceof jQuery) ? el[0] : el;
[].map.call(
el.querySelectorAll('.fw-backend-option-descriptor[data-fw-option-type]'),
function (el) {
startListeningToEventsForSingle(getOptionDescriptor(el));
}
);
}
function startListeningToEventsForSingle (optionDescriptor) {
get(optionDescriptor.type).startListeningForChanges(optionDescriptor)
}
/**
* We rely on the fact that by default, when we try to register some option
* type -- the undefined and default one will be already registered.
*/
function defaultHintObject () {
return get('fw-undefined') || {};
}
function detectDOMContext (el) {
el = findOptionDescriptorEl(el);
var nonOptionContext = findNonOptionContext(el);
return isRootOption(el, nonOptionContext)
? nonOptionContext
: findOptionDescriptorEl(el.parentElement);
}
function findOptionDescriptorEl (el) {
el = (el instanceof jQuery) ? el[0] : el;
if (! el) return false;
if (el.classList.contains('fw-backend-option-descriptor')) {
return el;
} else {
var closestOption = $(el).closest(
'.fw-backend-option-descriptor'
);
if (closestOption.length === 0) {
throw "There is no option descriptor for that element."
}
return closestOption[0];
}
}
function isRootOption(el, nonOptionContext) {
var parent;
// traverse parents
while (el) {
parent = el.parentElement;
if (parent === nonOptionContext) {
return true;
}
if (parent && elementMatches(parent, '.fw-backend-option-descriptor')) {
return false;
}
el = parent;
}
}
function findPathToTheTopContext (el, nonOptionContext) {
var parent;
var result = [];
// traverse parents
while (el) {
parent = el.parentElement;
if (parent === nonOptionContext) {
return result;
}
if (parent && elementMatches(parent, '.fw-backend-option-descriptor')) {
// result.push(parent.getAttribute('data-fw-option-type'));
result.push(parent);
}
el = parent;
}
return result.reverse();
}
/**
* A non-option context has two possible values:
*
* - a form tag which encloses a list of root options
* - a virtual context is an el with `.fw-backend-options-virtual-context`
*/
function findNonOptionContext (el) {
var parent;
// traverse parents
while (el) {
parent = el.parentElement;
if (parent && elementMatches(parent, '.fw-backend-options-virtual-context, form')) {
return parent;
}
el = parent;
}
return null;
}
function hasNestedOptions (el) {
// exclude nested options within a virtual context
var optionDescriptor = findOptionDescriptorEl(el);
var hasVirtualContext = optionDescriptor.querySelector(
'.fw-backend-options-virtual-context'
);
if (! hasVirtualContext) {
return !! optionDescriptor.querySelector(
'.fw-backend-option-descriptor'
);
}
// check if we have options which are not in the virtual context
return optionDescriptor.querySelectorAll(
'.fw-backend-option-descriptor'
).length > optionDescriptor.querySelectorAll(
'.fw-backend-options-virtual-context .fw-backend-option-descriptor'
).length;
}
function elementMatches (element, selector) {
var matchesFn;
// find vendor prefix
[
'matches','webkitMatchesSelector','mozMatchesSelector',
'msMatchesSelector','oMatchesSelector'
].some(function(fn) {
if (typeof document.body[fn] === 'function') {
matchesFn = fn;
return true;
}
return false;
})
return element[matchesFn](selector);
}
})(jQuery, (fw.options || {}));