first commit

This commit is contained in:
Roman Pyrih
2023-07-24 08:30:51 +02:00
commit c2e100a763
7128 changed files with 1622619 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
jQuery(function($){
var initialized = false,
changeTimeoutId = 0,
randomIdIncrement = 0,
localized = _fw_backend_customizer_localized,
/**
* @type {Object} {'#options_wrapper_id':'~'}
*/
pendingChanges = {},
/**
* Extract all input values within option and save them to the customizer input (to trigger preview update)
*/
processPendingChanges = function(){
$.each(pendingChanges, function(optionsWrapperId){
var $optionsWrapper = $('#'+ optionsWrapperId),
$input = $optionsWrapper.closest('.fw-backend-customizer-option')
.find('> input.fw-backend-customizer-option-input'),
newValue = JSON.stringify(fixSerializedValues(
$optionsWrapper.find(':input').serializeArray()
));
if ($input.val() === newValue) {
return;
}
$input.val(newValue).trigger('change');
});
pendingChanges = {};
},
fixSerializedValues = function(values) {
var inputNameToIndex = {},
fixedValues = [];
/**
* Traverse reversed array to leave only the last values.
* This is how _POST works, if you have
* fw_options[option_name][x]: 3
* fw_options[option_name][x]: 7
* the last one "wins" and the value of $_POST['fw_options']['option_name']['x'] will be 7
*/
for (var i = values.length - 1; i >= 0; i--) {
if (values[i].name.slice(-2) === '[]') {
// this will be sent in _POST as array
} else {
if (typeof inputNameToIndex[values[i].name] === 'undefined') {
inputNameToIndex[values[i].name] = i;
} else {
continue; // skip if already added (the last overwrites others)
}
}
fixedValues.push(values[i]);
}
/**
* The array was traversed in revers order, now restore the initial order
*/
return fixedValues.reverse();
},
init = function(){
if (initialized) {
return;
}
/**
* Populate all <input class="fw-backend-customizer-option-input" ... /> with (initial) options values
*/
$('#customize-theme-controls .fw-backend-customizer-option').each(function(){
$(this).find('> input.fw-backend-customizer-option-input').val(
JSON.stringify(fixSerializedValues(
$(this).find('> .fw-backend-customizer-option-inner :input').serializeArray()
))
);
});
/**
* When something may be changed, removed, added; add to pending changes
*/
$('#customize-theme-controls').on(
'change keyup click paste',
'.fw-backend-customizer-option > .fw-backend-customizer-option-inner > .fw-backend-option > .fw-backend-option-input',
function(e){
clearTimeout(changeTimeoutId);
{
var optionsWrapperId = $(this).attr('id');
if (!optionsWrapperId) {
optionsWrapperId = 'rnid-'+ (++randomIdIncrement);
$(this).attr('id', optionsWrapperId);
}
pendingChanges[optionsWrapperId] = '~';
}
changeTimeoutId = setTimeout(
processPendingChanges,
/**
* Let css animations finish,
* to prevent block/glitch in the middle of the animation when the iframe will reload.
* Bigger than 300, which most of the css animations are.
*/
localized.change_timeout
);
}
);
initialized = true;
};
fwEvents.one('fw:options:init', function(){
setTimeout(
init,
40 // must be later than first 'fw:options:init' on body http://bit.ly/1F1dDUZ
);
});
});

View File

@@ -0,0 +1,262 @@
/**
* Included on pages where backend options are rendered
*/
var fwBackendOptions = {
/**
* @deprecated Tabs are lazy loaded https://github.com/ThemeFuse/Unyson/issues/1174
*/
openTab: function(tabId) { console.warn('deprecated'); }
};
jQuery(document).ready(function($){
var localized = _fw_backend_options_localized;
/**
* Functions
*/
{
/**
* Make fw-postbox to close/open on click
*
* (fork from /wp-admin/js/postbox.js)
*/
function addPostboxToggles($boxes) {
/** Remove events added by /wp-admin/js/postbox.js */
$boxes.find('h2, h3, .handlediv, .hndle').off('click.postboxes');
var eventNamespace = '.fw-backend-postboxes';
$boxes.find('.postbox-header .hndle, .postbox-header .handlediv').on('mouseover', function () {
$(this).off('click.postboxes');
})
// make postboxes to close/open on click
$boxes.off('click'+ eventNamespace); // remove already attached, just to be sure, prevent multiple execution
$boxes.find('.postbox-header .hndle, .postbox-header .handlediv').on( 'click', function( e ) {
var $box = $(this).closest('.fw-postbox');
if ($box.parent().is('.fw-backend-postboxes') && !$box.siblings().length) {
// Do not close if only one box https://github.com/ThemeFuse/Unyson/issues/1094
$box.removeClass('closed');
} else {
$box.toggleClass('closed');
}
var isClosed = $box.hasClass('closed');
$box.trigger('fw:box:'+ (isClosed ? 'close' : 'open'));
$box.trigger('fw:box:toggle-closed', {isClosed: isClosed});
});
}
/** Remove box header if title is empty */
function hideBoxEmptyTitles($boxes) {
$boxes.find('> .hndle > span').each(function(){
var $this = $(this);
if (!$.trim($this.html()).length) {
$this.closest('.postbox').addClass('fw-postbox-without-name');
}
});
}
}
/** Init tabs */
(function(){
var htmlAttrName = 'data-fw-tab-html',
initTab = function($tab) {
var html;
if (html = $tab.attr(htmlAttrName)) {
fwEvents.trigger('fw:options:init', {
$elements: $tab.removeAttr(htmlAttrName).html(html),
/**
* Sometimes we want to perform some action just when
* lazy tabs are rendered. It's important in those cases
* to distinguish regular fw:options:init events from
* the ones that will render tabs. Passing by this little
* detail may break some widgets because fw:options:init
* event may be fired even when tabs are not yet rendered.
*
* That's how you can be sure that you'll run a piece
* of code just when tabs will be arround 100%.
*
* fwEvents.on('fw:options:init', function (data) {
* if (! data.lazyTabsUpdated) {
* return;
* }
*
* // Do your business
* });
*
*/
lazyTabsUpdated: true
});
}
},
initAllTabs = function ($el) {
var selector = '.fw-options-tab[' + htmlAttrName + ']', $tabs;
// fixes https://github.com/ThemeFuse/Unyson/issues/1634
$el.each(function(){
if ($(this).is(selector)) {
initTab($(this));
}
});
// initialized tabs can contain tabs, so init recursive until nothing is found
while (($tabs = $el.find(selector)).length) {
$tabs.each(function(){ initTab($(this)); });
}
};
fwEvents.on('fw:options:init:tabs', function (data) {
initAllTabs(data.$elements);
});
fwEvents.on('fw:options:init', function (data) {
var $tabs = data.$elements.find('.fw-options-tabs-wrapper:not(.initialized)');
if (localized.lazy_tabs) {
$tabs.tabs({
create: function (event, ui) {
initTab(ui.panel);
},
activate: function (event, ui) {
initTab(ui.newPanel);
ui.newPanel.closest('.fw-options-tabs-contents')[0].scrollTop = 0
}
});
$tabs
.closest('form')
.off('submit.fw-tabs')
.on('submit.fw-tabs', function () {
if (!$(this).hasClass('prevent-all-tabs-init')) {
// All options needs to be present in html to be sent in POST on submit
initAllTabs($(this));
}
});
} else {
$tabs.tabs({
activate: function (event, ui) {
ui.newPanel.closest('.fw-options-tabs-contents')[0].scrollTop = 0
}
});
}
$tabs.each(function () {
var $this = $(this);
if (!$this.parent().closest('.fw-options-tabs-wrapper').length) {
// add special class to first level tabs
$this.addClass('fw-options-tabs-first-level');
}
});
$tabs.addClass('initialized');
});
})();
/** Init boxes */
fwEvents.on('fw:options:init', function (data) {
var $boxes = data.$elements.find('.fw-postbox:not(.initialized)');
hideBoxEmptyTitles(
$boxes.filter('.fw-backend-postboxes > .fw-postbox')
);
addPostboxToggles($boxes);
/**
* leave open only first boxes
*/
$boxes
.filter('.fw-backend-postboxes > .fw-postbox:not(.fw-postbox-without-name):not(:first-child):not(.prevent-auto-close)')
.addClass('closed');
$boxes.addClass('initialized');
// trigger on box custom event for others to do something after box initialized
$boxes.trigger('fw-options-box:initialized');
});
/** Init options */
fwEvents.on('fw:options:init', function (data) {
data.$elements.find('.fw-backend-option:not(.initialized)')
// do nothing, just a the initialized class to make the fadeIn css animation effect
.addClass('initialized');
});
/** Fixes */
fwEvents.on('fw:options:init', function (data) {
{
var eventNamespace = '.fw-backend-postboxes';
data.$elements.find('.postbox:not(.fw-postbox) .fw-option')
.closest('.postbox:not(.fw-postbox)')
/**
* Add special class to first level postboxes that contains framework options (on post edit page)
*/
.addClass('postbox-with-fw-options')
/**
* Prevent event to be propagated to first level WordPress sortable (on edit post page)
* If not prevented, boxes within options can be dragged out of parent box to first level boxes
*/
.off('mousedown'+ eventNamespace) // remove already attached (happens when this script is executed multiple times on the same elements)
.on('mousedown'+ eventNamespace, '.fw-postbox > .hndle, .fw-postbox > .handlediv', function(e){
e.stopPropagation();
});
}
/**
* disable sortable (drag/drop) for postboxes created by framework options
* (have no sense, the order is not saved like for first level boxes on edit post page)
*/
{
var $sortables = data.$elements
.find('.postbox:not(.fw-postbox) .fw-postbox, .fw-options-tabs-wrapper .fw-postbox')
.closest('.fw-backend-postboxes')
.not('.fw-sortable-disabled');
$sortables.each(function(){
try {
$(this).sortable('destroy');
} catch (e) {
// happens when not initialized
}
});
$sortables.addClass('fw-sortable-disabled');
}
/** hide bottom border from last option inside box */
{
data.$elements.find('.postbox-with-fw-options > .inside, .fw-postbox > .inside')
.append('<div class="fw-backend-options-last-border-hider"></div>');
}
hideBoxEmptyTitles(
data.$elements.find('.postbox-with-fw-options')
);
});
/**
* Help tips (i)
*/
(function(){
fwEvents.on('fw:options:init', function (data) {
var $helps = data.$elements.find('.fw-option-help:not(.initialized)');
fw.qtip($helps);
$helps.addClass('initialized');
});
})();
$('#side-sortables').addClass('fw-force-xs');
});

View File

@@ -0,0 +1,271 @@
/**
* Listen and trigger custom events to communicate between javascript components
*/
var fwEvents = new (function(){
var _events = {};
var currentIndentation = 1;
var debug = false;
this.countAll = function (topic) {
return _events[topic];
}
/**
* Make log helper public
*
* @param {String} [message]
* @param {Object} [data]
*/
this.log = log;
/**
* Enable/Disable Debug
* @param {Boolean} enabled
*/
this.debug = function(enabled) {
debug = Boolean(enabled);
return this;
};
/**
* Add event listener
*
* @param event {String | Object}
* Can be a:
* - single event: 'event1'
* - space separated event list: 'event1 event2 event2'
* - an object: {event1: function () {}, event2: function () {}}
*
* @param callback {Function}
*/
this.on = function(topicStringOrObject, listener) {
objectMap(
splitTopicStringOrObject(topicStringOrObject, listener),
function (eventName, listener) {
(_events[eventName] || (_events[eventName] = [])).push(
listener
);
debug && log('✚ ' + eventName);
}
);
return this;
};
/**
* Same as .on(), but callback will executed only once
*/
this.one = function(topicStringOrObject, listener) {
objectMap(
splitTopicStringOrObject(topicStringOrObject, listener),
function (eventName, listener) {
(_events[eventName] || (_events[eventName] = [])).push(
once(listener)
);
debug && log('✚ [' + eventName +']');
}
);
return this;
// https://github.com/jashkenas/underscore/blob/8fc7032295d60aff3620ef85d4aa6549a55688a0/underscore.js#L946
function once(func) {
var memo;
var times = 2;
return function() {
if (--times > 0) {
memo = func.apply(this, arguments);
}
if (times <= 1) func = null;
return memo;
};
};
};
/**
* In order to remove one single listener you should give as an argument
* the same callback function. If you want to remove *all* listeners from
* a particular event you should not pass the second argument.
*
* @param topicStringOrObject {String | Object}
* @param listener {Function | false}
*/
this.off = function(topicStringOrObject, listener) {
objectMap(
splitTopicStringOrObject(topicStringOrObject, listener),
function (eventName, listener) {
if (_events[eventName]) {
if (listener) {
_events[eventName].splice(
_events[eventName].indexOf(listener) >>> 0,
1
);
} else {
_events[eventName] = [];
}
debug && log('✖ ' + eventName);
}
}
);
return this;
};
/**
* Trigger an event. In case you provide multiple events via space-separated
* string or an object of events it will execute listeners for each event
* separatedly. You can use the "all" event to trigger all events.
*
* @param topicStringOrObject {String | Object}
* @param data {Object}
*/
this.trigger = function(eventName, data) {
objectMap(
splitTopicStringOrObject(eventName),
function (eventName) {
log('╭─ '+ eventName, data);
changeIndentation(+1);
try {
// TODO: REFACTOR THAT!!!!!!!!!
// Maybe this is an occasion for using 'all' event???
if (eventName === 'fw:options:init') {
fw.options.startListeningToEvents(
data.$elements || document.body
)
}
(_events[eventName] || []).map(dispatchSingleEvent);
(_events['all'] || []).map(dispatchSingleEvent);
} catch (e) {
console.log(
"%c [Events] Exception raised. Please contact support in https://github.com/ThemeFuse/Unyson/issues/new. Don't forget to attach this stack trace to the issue.",
"color: red; font-weight: bold;"
);
if (typeof console !== 'undefined') {
console.error(e)
} else {
throw e;
}
}
changeIndentation(-1);
log('╰─ '+ eventName, data);
function dispatchSingleEvent (listenerDescriptor) {
if (! listenerDescriptor) return;
listenerDescriptor.call(
window,
data
);
}
}
);
return this;
function changeIndentation(increment) {
if (typeof increment != 'undefined') {
currentIndentation += (increment > 0 ? +1 : -1);
}
if (currentIndentation < 0) {
currentIndentation = 0;
}
}
};
/**
* Check if an event has listeners
* @param {String} [event]
* @return {Boolean}
*/
this.hasListeners = function(eventName) {
if (! _events) {
return false;
}
return (_events[eventName] || []).length > 0;
};
/**
* Probably split string into general purpose object representation for
* event names and listeners. This function leaves objects un-modified.
*
* @param topicStringOrObject {String | Object}
* @param listener {Function | false}
*
* @returns {Object} {
* eventname: listener,
* otherevent: listener
* }
*/
function splitTopicStringOrObject (topicStringOrObject, listener) {
if (typeof topicStringOrObject !== 'string') {
return topicStringOrObject;
}
var arrayOfEvents = topicStringOrObject.replace(
/\s\s+/g, ' '
).trim().split(' ');
var len = arrayOfEvents.length;
var listenerDescriptor = Object.create(null);
for (var i = 0; i < len; i++) {
listenerDescriptor[arrayOfEvents[i]] = listener;
}
return listenerDescriptor;
}
/**
* returns a new object with the predicate applied to each value
* objectMap({a: 3, b: 5, c: 9}, (key, value) => value + 1); // {a: 4, b: 6, c: 10}
* objectMap({a: 3, b: 5, c: 9}, (key, value) => key); // {a: 'a', b: 'b', c: 'c'}
* objectMap({a: 3, b: 5, c: 9}, (key, value) => key + value); // {a: 'a3', b: 'b5', c: 'c9'}
*
* https://github.com/angus-c/just/tree/master/packages/object-map
*/
function objectMap(obj, predicate) {
var result = {};
var keys = Object.keys(obj);
var len = keys.length;
for (var i = 0; i < len; i++) {
var key = keys[i];
result[key] = predicate(key, obj[key]);
}
return result;
}
function log(message, data) {
if (! debug) {
return;
}
if (typeof data != 'undefined') {
console.log('[Event] ' + getIndentation() + message, '─', data);
} else {
console.log('[Event] ' + getIndentation() + message);
}
function getIndentation() {
return new Array(currentIndentation).join('│ ');
}
}
})();

View File

@@ -0,0 +1,271 @@
/**
* FW_Form helpers
* Dependencies: jQuery
* Note: You can include this script in frontend (for e.g. to make you contact forms ajax submittable)
*/
var fwForm = {
/**
* Make forms ajax submittable
* @param {Object} [opts] You can overwrite any
*/
initAjaxSubmit: function(opts) {
var opts = jQuery.extend({
selector: 'form[data-fw-form-id]',
ajaxUrl: (typeof ajaxurl != 'undefined')
? ajaxurl
: ((typeof fwAjaxUrl != 'undefined')
? fwAjaxUrl // wp_localize_script('fw-form-helpers', 'fwAjaxUrl', admin_url( 'admin-ajax.php', 'relative' ));
: '/wp-admin/admin-ajax.php'
),
loading: function (elements, show) {
elements.$form.css('position', 'relative');
elements.$form.find('> .fw-form-loading').remove();
if (show) {
elements.$form.append(
'<div'+
' class="fw-form-loading"'+
' style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.1);"'+
'></div>'
);
}
},
afterSubmitDelay: function(elements){},
onErrors: function (elements, data) {
if (isAdmin) {
fwForm.backend.showFlashMessages(
fwForm.backend.renderFlashMessages({error: data.errors})
);
} else {
// Frontend
jQuery.each(data.errors, function (inputName, message) {
message = '<p class="form-error" style="color: #9b2922;">{message}</p>'
.replace('{message}', message);
var $input = elements.$form.find('[name="' + inputName + '"]').last();
if (!$input.length) {
// maybe input name has array format, try to find by prefix: name[
$input = elements.$form.find('[name^="'+ inputName +'["]').last();
}
if ($input.length) {
// error message under input
$input.parent().after(message);
} else {
// if input not found, show message in form
elements.$form.prepend(message);
}
});
}
},
hideErrors: function (elements) {
elements.$form.find('.form-error').remove();
},
onAjaxError: function(elements, data) {
console.error(data.jqXHR, data.textStatus, data.errorThrown);
alert('Ajax error (more details in console)');
},
onSuccess: function (elements, ajaxData) {
if (isAdmin) {
fwForm.backend.showFlashMessages(
fwForm.backend.renderFlashMessages(ajaxData.flash_messages)
);
} else {
var html = fwForm.frontend.renderFlashMessages(ajaxData.flash_messages);
if (!html.length) {
html = '<p>Success</p>';
}
elements.$form.fadeOut(function(){
elements.$form.html(html).fadeIn();
});
// prevent multiple submit
elements.$form.on('submit', function(e){ e.preventDefault(); e.stopPropagation(); });
}
}
}, opts || {}),
isAdmin = (typeof adminpage != 'undefined' && jQuery(document.body).hasClass('wp-admin')),
isBusy = false;
jQuery(document.body).on('submit', opts.selector, function(e){
e.preventDefault();
if (isBusy) {
console.warn('Working... Try again later.');
return;
}
var $form = jQuery(this);
if (!$form.is('form[data-fw-form-id]')) {
console.error('This is not a FW_Form', 'Selector:'. opts.selector, 'Form:', $form);
return;
}
// get submit button
{
var $submitButton = $form.find(':submit:focus');
if (!$submitButton.length) {
// in case you use this solution http://stackoverflow.com/a/5721762
$submitButton = $form.find('[clicked]:submit');
}
// make sure to remove the "clicked" attribute to prevent accidental settings reset
$form.find('[clicked]:submit').removeAttr('clicked');
}
var elements = {
$form: $form,
$submitButton: $submitButton
};
opts.hideErrors(elements);
var delaySubmit = parseInt(
opts.loading(
elements,
/**
* If you want to submit your ajaxified Theme Settings form without
* any notification for the user add class fw-silent-submit to
* the form element itself. This class will be removed
* automatically after this particular submit, so that popup will
* show when the user will press Submit button next time.
*/
! $form.hasClass('fw-silent-submit')
)
);
delaySubmit = (isNaN(delaySubmit) || delaySubmit < 0) ? 0 : delaySubmit;
$form.removeClass('fw-silent-submit');
isBusy = true;
setTimeout(function(){
if (delaySubmit) {
opts.afterSubmitDelay(elements);
}
jQuery.ajax({
type: "POST",
url: opts.ajaxUrl,
data: $form.serialize() + (
$submitButton.length
? '&'+ $submitButton.attr('name') +'='+ $submitButton.attr('value')
: ''
),
dataType: 'json'
}).done(function(r){
isBusy = false;
opts.loading(elements, false);
if (r.success) {
opts.onSuccess(elements, r.data);
} else {
opts.onErrors(elements, r.data);
}
}).fail(function(jqXHR, textStatus, errorThrown){
isBusy = false;
opts.loading(elements, false);
opts.onAjaxError(elements, {
jqXHR: jqXHR,
textStatus: textStatus,
errorThrown: errorThrown
});
});
}, delaySubmit);
});
},
backend: {
showFlashMessages: function(messagesHtml) {
var $pageTitle = jQuery('.wrap h2:first');
while ($pageTitle.next().is('.fw-flash-messages, .fw-flash-message, .updated, .update-nag, .error')) {
$pageTitle.next().remove();
}
$pageTitle.after('<div class="fw-flash-messages">'+ messagesHtml +'</div>');
jQuery(document.body).animate({scrollTop: 0}, 300);
},
/**
* Html structure should be the same as generated by FW_Flash_Messages::_print_backend()
* @param {Object} flashMessages
* @returns {string}
*/
renderFlashMessages: function(flashMessages) {
var html = [],
typeHtml = [],
messageClass = '';
jQuery.each(flashMessages, function(type, messages){
typeHtml = [];
switch (type) {
case 'error':
messageClass = 'error';
break;
case 'warning':
messageClass = 'update-nag';
break;
default:
messageClass = 'updated';
}
jQuery.each(messages, function(messageId, message){
typeHtml.push('<div class="'+ messageClass +' fw-flash-message"><p>'+ message +'</p></div>');
});
if (typeHtml.length) {
html.push(
'<div class="fw-flash-type-'+ type +'">'+ typeHtml.join('</div><div class="fw-flash-type-'+ type +'">') +'</div>'
);
}
});
return html.join('');
}
},
frontend: {
/**
* Html structure is the same as generated by FW_Flash_Messages::_print_frontend()
* @param {Object} flashMessages
* @returns {string}
*/
renderFlashMessages: function(flashMessages) {
var html = [],
typeHtml = [],
messageClass = '';
jQuery.each(flashMessages, function(type, messages){
typeHtml = [];
jQuery.each(messages, function(messageId, message){
typeHtml.push('<li class="fw-flash-message">'+ message +'</li>');
});
if (typeHtml.length) {
html.push(
'<ul class="fw-flash-type-'+ type +'">'+ typeHtml.join('</ul><ul class="fw-flash-type-'+ type +'">') +'</ul>'
);
}
});
return html.join('');
}
}
};
// Usage example
if (false) {
jQuery(function ($) {
fwForm.initAjaxSubmit({
selector: 'form[data-fw-form-id][data-fw-ext-forms-type="contact-forms"]',
ajaxUrl: ajaxurl
});
});
}

View File

@@ -0,0 +1,405 @@
/**
* 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 || {}));

View File

@@ -0,0 +1,109 @@
(function($) {
var simpleInputs = [
'text',
'short-text',
'hidden',
'password',
'textarea',
'html',
'html-fixed',
'html-full',
'select',
'short-select',
'gmap-key',
'slider',
'short-slider',
];
simpleInputs.map(function(optionType) {
fw.options.register(optionType, {
getValue: getValueForSimpleInput,
});
});
function getValueForSimpleInput(optionDescriptor) {
return {
value: optionDescriptor.el.querySelector('input, textarea, select')
.value,
optionDescriptor: optionDescriptor,
};
}
fw.options.register('unique', {
getValue: function(optionDescriptor) {
var actualValue = optionDescriptor.el.querySelector(
'input, textarea, select'
).value;
return {
value: !!actualValue.trim() ? actualValue : fw.randomMD5(),
optionDescriptor: optionDescriptor,
};
},
});
fw.options.register('checkbox', {
getValue: function(optionDescriptor) {
return {
value: optionDescriptor.el.querySelector(
'input.fw-option-type-checkbox'
).checked,
optionDescriptor: optionDescriptor,
};
},
});
fw.options.register('checkboxes', {
getValue: function(optionDescriptor) {
var checkboxes = $(optionDescriptor.el)
.find('[type="checkbox"]')
.slice(1);
var value = {};
checkboxes.toArray().map(function(el) {
value[$(el).attr('data-fw-checkbox-id')] = el.checked;
});
return {
value: value,
optionDescriptor: optionDescriptor,
};
},
});
fw.options.register('radio', {
getValue: function(optionDescriptor) {
return {
value: $(optionDescriptor.el).find('input:checked').val(),
optionDescriptor: optionDescriptor,
};
},
});
fw.options.register('select-multiple', {
getValue: function(optionDescriptor) {
return {
value: $(optionDescriptor.el.querySelector('select')).val(),
optionDescriptor: optionDescriptor,
};
},
});
fw.options.register('multi', {
getValue: function(optionDescriptor) {
var promise = $.Deferred();
fw.options
.getContextValue(optionDescriptor.el)
.then(function(result) {
promise.resolve({
value: result.value,
optionDescriptor: optionDescriptor,
});
});
return promise;
},
});
})(jQuery);

View File

@@ -0,0 +1,287 @@
(function ($) {
fw.options.register('fw-undefined', {
startListeningForChanges: defaultStartListeningForChanges,
getValue: defaultGetValue
});
function defaultGetValue (optionDescriptor) {
var resultPromise = $.Deferred();
/**
* If we get a really undefined option type.
*/
if (optionDescriptor.type === 'fw-undefined') {
resultPromise.resolve({
value: '',
optionDescriptor: optionDescriptor
})
return resultPromise;
}
// 1. find all inputs and ignore virtual contexts
// this really should include nested options and properly serialize
// them together
//
// we should serialize those inputs into an object, based on their
// name attribute
var formInstance = new FormSerializer($, optionDescriptor.el);
var inputValues = formInstance.addPairs(
findInputsFromAContextAndIgnoreVirtualScopes(
optionDescriptor.el
).serializeArray()
).serialize();
// 2. remove name_prefixes from those inputs
// optionsDescriptor.id === 'laptop'
// name="fw_options[nesting][laptop]"
//
// This step should get
// inputValues['fw_options']['nesting']['laptop']
inputValues = inputValues[
Object.keys(inputValues)[0]
];
if (optionDescriptor.pathToTheTopContext.length > 0) {
var IDs = optionDescriptor.pathToTheTopContext.map(
fw.options.getOptionDescriptor
);
IDs.map(function (localDescriptor) {
inputValues = inputValues[localDescriptor.id];
});
}
var options = {};
options[optionDescriptor.id] = JSON.parse(jQuery(optionDescriptor.el).attr(
'data-fw-for-js'
)).option;
// 3. construct an AJAX request with correct options and input values
$.ajax({
type: 'POST',
dataType: "json",
url: ajaxurl,
data: {
action: 'fw_backend_options_get_values',
name_prefix: 'fw_options',
options: [
options
],
fw_options: inputValues
}
})
.then(function (response, status, request) {
if (response.success && request.status === 200) {
resultPromise.resolve(
{
value: response.data.values[optionDescriptor.id],
optionDescriptor: optionDescriptor
}
);
} else {
resultPromise.reject();
}
})
.fail(function () {
// TODO: pass a reason
resultPromise.reject();
});
return resultPromise;
}
// By default, for unknown option types do listening only once
function defaultStartListeningForChanges (optionDescriptor) {
if (optionDescriptor.el.classList.contains('fw-listening-started')) {
return;
}
optionDescriptor.el.classList.add('fw-listening-started');
listenToChangesForCurrentOptionAndPreserveScoping(
optionDescriptor.el,
_.throttle(function (e) {
fw.options.trigger.changeForEl(e.target);
}, 300)
);
if (optionDescriptor.hasNestedOptions) {
fw.options.on.changeByContext(optionDescriptor.el, function (nestedDescriptor) {
fw.options.trigger.changeForEl(optionDescriptor.el);
});
}
}
/**
* TODO
* rewrite that with:
*
* - Array.filter
* - Array.includes
* - addEventListener
* - querySelectorAll
*/
function listenToChangesForCurrentOptionAndPreserveScoping (el, callback) {
jQuery(el).find(
'input, select, textarea'
).not(
jQuery(el).find(
'.fw-backend-option-descriptor input'
).add(
jQuery(el).find(
'.fw-backend-option-descriptor select'
)
).add(
jQuery(el).find(
'.fw-backend-option-descriptor textarea'
)
).add(
jQuery(el).find(
'.fw-backend-options-virtual-context input'
)
).add(
jQuery(el).find(
'.fw-backend-options-virtual-context select'
)
).add(
jQuery(el).find(
'.fw-backend-options-virtual-context textarea'
)
)
).on('change', callback);
}
function findInputsFromAContextAndIgnoreVirtualScopes (el) {
return jQuery(el).find(
'input, select, textarea'
).not(
jQuery(el).find(
'.fw-backend-options-virtual-context input'
).add(
jQuery(el).find(
'.fw-backend-options-virtual-context select'
)
).add(
jQuery(el).find(
'.fw-backend-options-virtual-context textarea'
)
)
).not(
jQuery(el).find(
'.fw-filter-from-serialization input'
).add(
jQuery(el).find(
'.fw-filter-from-serialization select'
)
).add(
jQuery(el).find(
'.fw-filter-from-serialization textarea'
)
)
);
}
/**
* USAGE:
*
* var formInstance = new FormSerializer(jQuery, document.body);
*
* formInstance.addPairs(jQuery('input').serializeArray());
* formInstance.serialize();
*/
function FormSerializer(helper, $form) {
var patterns = {
push: /^$/,
fixed: /^\d+$/,
validate: /^[a-z][a-z0-9_-]*(?:\[(?:\d*|[a-z0-9_-]+)\])*$/i,
key: /[a-z0-9_-]+|(?=\[\])/gi,
named: /^[a-z0-9_-]+$/i
};
// private variables
var data = {},
pushes = {};
// private API
function build(base, key, value) {
base[key] = value;
return base;
}
function makeObject(root, value) {
var keys = root.match(patterns.key), k;
// nest, nest, ..., nest
while ((k = keys.pop()) !== undefined) {
// foo[]
if (patterns.push.test(k)) {
var idx = incrementPush(root.replace(/\[\]$/, ''));
value = build([], idx, value);
}
// foo[n]
else if (patterns.fixed.test(k)) {
value = build([], k, value);
}
// foo; foo[bar]
else if (patterns.named.test(k)) {
value = build({}, k, value);
}
}
return value;
}
function incrementPush(key) {
if (pushes[key] === undefined) {
pushes[key] = 0;
}
return pushes[key]++;
}
function encode(pair) {
switch ($('[name="' + pair.name + '"]', $form).attr("type")) {
case "checkbox":
return pair.value === "on" ? true : pair.value;
default:
return pair.value;
}
}
function addPair(pair) {
if (! patterns.validate.test(pair.name)) return this;
var obj = makeObject(pair.name, encode(pair));
data = helper.extend(true, data, obj);
return this;
}
function addPairs(pairs) {
if (!helper.isArray(pairs)) {
throw new Error("formSerializer.addPairs expects an Array");
}
for (var i=0, len=pairs.length; i<len; i++) {
this.addPair(pairs[i]);
}
return this;
}
function serialize() {
return data;
}
// public API
this.addPair = addPair;
this.addPairs = addPairs;
this.serialize = serialize;
};
})(jQuery);

View File

@@ -0,0 +1,274 @@
/**
* Basic mechanism that allows option types to notify the rest of the world
* about the fact that they was changed by the user.
*
* Each option type is responsible for triggering such events and they should
* at least try to supply a reasonable current value for it. Additional meta
* data about the particular option type is infered automatically and can be
* overrided by the option which triggers the event.
*
* In theory (and also in practice), options can and should trigger events
* which are different than the `change`, because complicated option types
* has many lifecycle events which everyone should be aware about and be able
* to hook into. A lot of options already trigger such events but they do that
* in an inconsistent manner which leads to poorly named and poorly namespaced
* events.
*
* TODO: document this better
*/
fw.options = (function($, currentFwOptions) {
currentFwOptions.on = on;
currentFwOptions.off = off;
currentFwOptions.trigger = trigger;
/**
* Allows:
* fw.options.trigger(...)
* fw.options.trigger.change(...)
* fw.options.trigger.forEl(...)
* fw.options.trigger.changeForEl(...)
* fw.options.trigger.scopedByType(...)
*/
currentFwOptions.trigger.change = triggerChange;
currentFwOptions.trigger.forEl = triggerForEl;
currentFwOptions.trigger.changeForEl = triggerChangeForEl;
currentFwOptions.trigger.scopedByType = triggerScopedByType;
/**
* Allows:
* fw.options.on(...)
* fw.options.on.one(...)
* fw.options.on.change(...)
* fw.options.on.changeByContext(...)
*/
currentFwOptions.on.one = one;
currentFwOptions.on.change = onChange;
currentFwOptions.on.changeByContext = onChangeByContext;
/**
* Allows:
* fw.options.off(...)
* fw.options.off.change(...)
*/
currentFwOptions.off.change = offChange;
/**
* A small little service for fetching HTML for option types from server.
* It will perform caching for results inside a key-value store.
*
* Allows:
*
* fw.options.fetchHtml(options, values)
* fw.options.fetchHtml.getCacheEntryFor(options, values)
* fw.options.fetchHtml.emptyCache();
*
* // TODO: provide a way to empty cache for a specific set of options???
*/
var htmlCache = {};
fw.options.fetchHtml = fetchHtml;
fw.options.fetchHtml.getCacheEntryFor = fetchHtmlGetCacheEntryFor;
fw.options.fetchHtml.emptyCache = fetchHtmlEmptyCache;
/**
* A helper for getting actual values for a set of options and values.
* Much better than fw.getValuesFromServer() because it doesn't require
* you to encode values as form data params. You just pass a valid JSON
* object and it just works.
*
* fw.options
* .getActualValues({a: {type: 'text', value: 'Initial'})
* .then(function (values) {
* // {
* // a: 'Initial'
* // }
* console.log(values);
* });
*
* fw.options
* .getActualValues({a: {type: 'text', value: 'Initial'}, {a: 'Changed'})
* .then(function (values) {
* // {
* // a: 'Changed'
* // }
* console.log(values);
* });
*/
fw.options.getActualValues = getActualValues;
return currentFwOptions;
function onChange(listener) {
on('change', listener);
}
/**
* Please note that you won't be able to off that listener easily because
* it rewrites the listener which gets passed to fwEvents.
*
* If you want to be able to off the listener you should attach it with
* onChange and filter based on context by yourself.
*/
function onChangeByContext(context, listener) {
onChange(function(data) {
if (data.context === findOptionDescriptorEl(context)) {
listener(data);
}
});
}
function on(eventName, listener) {
fwEvents.on('fw:options:' + eventName, listener);
}
function one(eventName, listener) {
fwEvents.one('fw:options:' + eventName, listener);
}
function off(eventName, listener) {
fwEvents.off('fw:options:' + eventName, listener);
}
function offChange(listener) {
off('change', listener);
}
/**
* data:
* optionId
* type
* value
* context
* el
*/
function trigger(eventName, data) {
fwEvents.trigger('fw:options:' + eventName, data);
}
function triggerForEl(eventName, el, data) {
trigger(eventName, getActualData(el, data));
}
function triggerChange(data) {
trigger('change', data);
}
function triggerChangeForEl(el, data) {
triggerChange(getActualData(el, data));
}
/**
* Trigger a scoped event for a specific option type, has the form:
* fw:options:{type}:{eventName}
*/
function triggerScopedByType(eventName, el, data) {
data = getActualData(el, data);
trigger(data.type + ':' + eventName, data);
}
function getActualData(el, data) {
return $.extend({}, currentFwOptions.getOptionDescriptor(el), data);
}
function findOptionDescriptorEl(el) {
el = el instanceof jQuery ? el[0] : el;
return el.classList.contains('fw-backend-option-descriptor')
? el
: $(el).closest('.fw-backend-option-descriptor')[0];
}
function fetchHtml(options, values, settings) {
var promise = $.Deferred();
if (!settings) settings = {};
settings = _.extend({ name_prefix: 'fw_edit_options_modal' }, settings);
var cacheId = fetchHtmlGetCacheId(options, values);
if (typeof htmlCache[cacheId] !== 'undefined') {
promise.resolve(htmlCache[cacheId]);
return promise;
}
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'fw_backend_options_render',
options: JSON.stringify(options),
values: JSON.stringify(
typeof values == 'undefined' ? {} : values
),
data: {
name_prefix: settings.name_prefix,
id_prefix: settings.name_prefix.replace(/_/g, '-') + '-',
},
},
dataType: 'json',
success: function(response, status, xhr) {
if (!response.success) {
promise.reject('Error: ' + response.data.message);
return;
}
htmlCache[cacheId] = response.data.html;
promise.resolve(response.data.html, response, status, xhr);
},
error: function(xhr, status, error) {
promise.reject(status + ': ' + String(error));
},
});
return promise;
}
function fetchHtmlGetCacheEntryFor(options, values) {
return htmlCache[fetchHtmlGetCacheId(options, values)];
}
function fetchHtmlEmptyCache() {
htmlCache = {};
}
function fetchHtmlGetCacheId(options, values) {
return fw.md5(
JSON.stringify(options) +
'~' +
JSON.stringify(typeof values == 'undefined' ? {} : values)
);
}
function getActualValues (options, values) {
var promise = $.Deferred();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'fw_backend_options_get_values_json',
options: JSON.stringify(options),
values: JSON.stringify(
typeof values == 'undefined' ? {} : values
)
},
dataType: 'json',
success: function(response, status, xhr) {
if (!response.success) {
promise.reject('Error: ' + response.data.message);
return;
}
promise.resolve(response.data.values, response, status, xhr);
},
error: function(xhr, status, error) {
promise.reject(status + ': ' + String(error));
},
});
return promise;
}
})(jQuery, fw.options || {});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
/**
* IE fixes for jQuery plugins to not throw errors
*/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if ( this === undefined || this === null ) {
throw new TypeError( '"this" is null or not defined' );
}
var length = this.length >>> 0; // Hack to convert object.length to a UInt32
fromIndex = +fromIndex || 0;
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}

View File

@@ -0,0 +1,31 @@
jQuery( document ).ready( function ( $ ) {
setTimeout( function () {
fwEvents.trigger( 'fw:options:init', {
$elements: $( document.body )
} );
}, 30 );
function updateContent( $content ) {
if ( tinymce.get( 'content' ) ) {
tinymce.get( 'content' ).setContent( $content );
} else {
$content.val( $content );
}
}
$( '#post-preview' ).on( 'mousedown touchend', function () {
var $content = $( '#content' ),
$contentValue = tinymce.get( 'content' ) ? tinymce.get( 'content' ).getContent() : $content.val(),
$session = '<!-- <fw_preview_session>' + new Date().getTime() + '</fw_preview_session> -->';
if ( $contentValue.indexOf( '<!-- <fw_preview_session>' ) !== -1 ) {
$contentValue = $contentValue.replace( /<!-- <fw_preview_session>(.*?)<\/fw_preview_session> -->/gi, $session );
} else {
$contentValue = $contentValue + $session;
}
updateContent( $contentValue );
updateContent( $contentValue.replace( /<!-- <fw_preview_session>(.*?)<\/fw_preview_session> -->/gi, '' ) );
} );
} );