js/src/common/UI.js
/**
*
* These is a collection of helper functions to handle
* the user interface / user interaction such as
* - Sorting
* - TinyMCE De-/Initialization
* - Tabs initialization
* - UI repainting / updating
*
* @package Kontentblocks
* @subpackage Backend/UI
* @type @exp; KB
*/
var $ = jQuery;
var Config = require('common/Config');
var Ajax = require('common/Ajax');
var TinyMCE = require('common/TinyMCE');
var Notice = require('common/Notice');
var ContextRowGrid = require('backend/Views/ContextUi/ContextRowGrid');
var Ui = {
// sorting indication
isSorting: false,
// boot up
init: function () {
var that = this;
var $body = $('body');
// init general ui components
this.initTabs();
this.initSortable();
this.initSortableAreas();
this.initToggleBoxes();
this.flexContext();
this.flushLocalStorage();
this.initTipsy();
// set the global activeField variable dynamically
// legacy
$body.on('mousedown', '.kb-field', function (e) {
activeField = this;
});
// set the global activeBlock variable dynamically
// legacy
$body.on('mousedown', '.kb-module', function (e) {
activeBlock = this.id;
});
// set the current field id as reference
$body.on('mouseenter', '[data-kbfield]', function () {
KB.currentFieldId = this.id;
});
$body.on('mouseenter', '.kb-area__list-item li', function () {
KB.currentModuleId = this.id;
});
// Bind AjaxComplete, restoring TinyMCE after global MEtaBox reordering
jQuery(document).ajaxComplete(function (e, o, settings) {
that.metaBoxReorder(e, o, settings, 'restore');
});
// Bind AjaxSend to remove TinyMCE before global MetaBox reordering
jQuery(document).ajaxSend(function (e, o, settings) {
that.metaBoxReorder(e, o, settings, 'remove');
});
},
flexContext: function () {
jQuery('.kb-context-row').each(function (index, el) {
var $el = jQuery(el);
$el.data('KB.ContextRow', new ContextRowGrid({
el: el
}));
});
},
repaint: function ($el) {
this.initTabs($el);
this.initToggleBoxes();
TinyMCE.addEditor($el);
},
initTabs: function ($cntxt) {
var $context = $cntxt || jQuery('body');
var selector = $('.kb-field--tabs', $context);
var $window = $(window);
selector.tabs({
// beforeActivate: function (event, ui) {
// console.log(ui);
//
// window.location.hash = ui.newPanel.selector;
// },
activate: function (e, ui) {
_.defer(function () {
$('.kb-nano').nanoScroller({contentClass: 'kb-nano-content'});
KB.Events.trigger('modal.recalibrate');
});
}
});
selector.each(function () {
// hide tab navigation if only one tab exists
var length = $('.ui-tabs-nav li', $(this)).length;
if (length === 1) {
$(this).find('.ui-tabs-nav').css('display', 'none');
}
// $window.on('hashchange', function () {
// if (!location.hash) {
// selector.tabs('option', 'active', 0);
// return;
// }
// $('ul > li > a', selector).each(function (index,a) {
// if ($(a).attr('href') === location.hash){
// selector.tabs('option', 'active', index);
// }
// })
// })
});
var $subtabs = $('[data-kbfsubtabs]', $context).tabs({
activate: function () {
KB.Events.trigger('modal.recalibrate');
}
});
},
initToggleBoxes: function () {
$('.kb-togglebox-header').on('click', function () {
$(this).next('div').slideToggle();
});
$('.kb_fieldtoggles div:first-child').trigger('click');
},
initSortable: function ($cntxt) {
var $context = $cntxt || jQuery('body');
var currentModule, areaOver, prevAreaOver;
var validModule = false;
var that = this;
/*
* Test if the current sorted module
* is allowed in (potentially) new area
* Checks if either the module limit of the area
* has been reached or if the current module
* type is not in the array of assigned modules
* of the area
*/
function isValidModule() {
var limit = areaOver.get('limit');
var nom = numberOfModulesInArea(areaOver.get('id'));
if (
_.indexOf(areaOver.get(
'assignedModules'), currentModule.get('settings').class) === -1) {
return false;
} else if (limit !== 0 && limit <= nom - 1) {
Notice.notice(
'Not allowed here', 'error');
return false;
} else {
return true;
}
}
/**
*
Get an
array of modules by area id
* @param
id string
*
@returns array of all found modules in that area
*/
function filterModulesByArea(id) {
return _.filter(KB.Modules.models, function (model) {
return model.get('area') === id;
}
);
}
function numberOfModulesInArea(id) {
return $('#' + id + ' li.kb-module').length;
}
var appendTo = 'parent';
if (Config.getLayoutMode() === 'default-tabs') {
appendTo = '#kb-contexts-tabs';
}
// handles sorting of the blocks.
$('.kb-module-ui__sortable', $context).sortable({
//settings
placeholder: "ui-state-highlight",
ghost: true,
connectWith: ".kb-module-ui__sortable--connect",
helper: 'clone',
handle: '.kb-move',
cancel: 'li.disabled, li.cantsort',
tolerance: 'pointer',
delay: 150,
revert: 350,
appendTo: appendTo,
// start event
start: function (event, ui) {
// set current model
that.isSorting = true;
$('body').addClass('kb-is-sorting');
currentModule = KB.Modules.get(ui.item.attr('id'));
areaOver = KB.currentArea;
$(KB).trigger('kb:sortable::start');
// close open modules, sorting on open container
// doesn't work very well
$('.kb-open').toggleClass('kb-open');
$('.kb-module__body').hide();
// tinyMCE doesn't like to be moved in the DOM
if (areaOver.View && areaOver.View.$el){
TinyMCE.removeEditors(areaOver.View.$el);
} else {
TinyMCE.removeEditors();
}
// Add a global trigger to sortable.start, maybe other Blocks might need it
$(document).trigger('kb_sortable_start', [event, ui]);
},
stop: function (event, ui) {
that.isSorting = false;
$('body').removeClass('kb-is-sorting');
// restore TinyMCE editors
TinyMCE.restoreEditors();
// global trigger when sortable is done
$(document).trigger('kb_sortable_stop', [event, ui]);
if (currentModule.get('open')) {
currentModule.View.toggleBody(155);
}
},
over: function (event, ui) {
// keep track of target area
areaOver = KB.Areas.get(this.id);
},
receive: function (event, ui) {
if (!isValidModule()) {
// inform the user
Notice.notice('Module not allowed in this area', 'error');
// cancel sorting
$(ui.sender).sortable('cancel');
}
},
update: function (ev, ui) {
if (!isValidModule()) {
return false;
}
// update will fire twice when modules are
// moved between two areas, once for each list
// this makes sure that the right action(s) are only done once
if (this === ui.item.parent('ul')[0] && !ui.sender) {
// function call applies when target area == origin
$.when(that.resort(ui.sender)).done(function (res) {
if (res.success) {
$(KB).trigger('kb:sortable::update');
Notice.notice(res.message, 'success');
} else {
Notice.notice(res.message, 'error');
return false;
}
});
} else if (ui.sender) {
// do nothing if the receiver rejected the request
if (ui.item.parent('ul')[0].id === ui.sender.attr('id')) {
return false;
}
// function call applies when target area != origin
// chain reordering and change of area
$.when(that.changeArea(areaOver, currentModule)).then(function (res) {
if (res.success) {
that.resort(ui.sender);
} else {
return false;
}
}).done(function () {
that.triggerAreaChange(areaOver, currentModule);
$(KB).trigger('kb:sortable::update');
// force recreation of any attached fields
currentModule.View.clearFields();
Notice.notice('Area change and order were updated successfully', 'success');
});
}
}
});
},
flushLocalStorage: function () {
var hash = Config.get('env').hash;
if (store.get('kbhash') !== hash) {
store.set('kbhash', hash)
}
},
/**
* Handles saving of new module order per area
* @param sender jQueryUI sortable sender list
* @returns {jqXHR}
*/
resort: function (sender) {
// serialize data
var serializedData = {};
$('.kb-module-ui__sortable').each(function () {
serializedData[this.id] = $('#' + this.id).sortable('serialize', {
attribute: 'rel'
});
});
return Ajax.send({
action: 'resortModules',
data: serializedData,
_ajax_nonce: Config.getNonce('update')
});
},
/**
*
* @param object targetArea
* @param object module
* @returns {jqXHR}
*/
changeArea: function (targetArea, module) {
return Ajax.send({
action: 'changeArea',
_ajax_nonce: Config.getNonce('update'),
mid: module.get('mid'),
area_id: targetArea.get('id'),
context: targetArea.get('context')
});
},
triggerAreaChange: function (newArea, moduleModel) {
moduleModel.unsubscribeFromArea(); // remove from current area
moduleModel.setArea(newArea);
},
toggleModule: function () {
$('body').on('click', '.kb-toggle', function () {
if (KB.isLocked() && !KB.userCan('lock_kontentblocks')) {
Notice.notice(kontentblocks.l18n.gen_no_permission, 'alert');
}
else {
$(this).parent().nextAll('.kb-module__body:first').slideToggle('fast', function () {
$('body').trigger('module::opened');
});
$('#' + activeBlock).toggleClass('kb-open', 1000);
}
});
},
initSortableAreas: function () {
jQuery('.kb-context__inner').sortable({
items: '.kb-area__wrap',
handle: '.kb-area-move-handle',
start: function (e, ui) {
TinyMCE.removeEditors();
},
stop: function (e, ui) {
var serData = jQuery('#post').serializeJSON();
var data = serData.kbcontext;
if (data) {
Ajax.send({
action: 'updateContextAreaOrder',
_ajax_nonce: Config.getNonce('update'),
data: data
}, function (res) {
if (res.success) {
Notice.notice(res.message, 'success');
} else {
Notice.notice(res.message, 'error');
}
TinyMCE.restoreEditors();
}, this);
}
}
});
},
initTipsy: function () {
jQuery('body').on('mouseenter', '[data-kbtooltip]', function (e) {
jQuery(this).qtip({
content: {
attr: 'data-kbtooltip' // Tell qTip2 to look inside this attr for its content
},
style: 'qtip-dark qtip-shadow',
show: {
event: e.type, // Show on mouse over by default
effect: true, // Use default 90ms fade effect
delay: 180, // 90ms delay before showing
solo: true, // Do not hide others when showing
ready: true // Do not show immediately
}
});
});
},
metaBoxReorder: function (e, o, settings, action) {
if (settings.data) {
var a = settings.data;
if (a && a.split) {
var b = a.split('&');
var result = {};
$.each(b, function (x, y) {
var temp = y.split('=');
result[temp[0]] = temp[1];
});
if (result.action === 'meta-box-order') {
if (action === 'restore') {
TinyMCE.restoreEditors();
}
else if (action === 'remove') {
TinyMCE.removeEditors();
}
}
}
}
}
};
module.exports = Ui;