gridstack-8.x-2.5/js/admin/gridstack.admin.js
js/admin/gridstack.admin.js
/**
* @file
* Provides GridStack admin utilities.
*/
(function ($, Drupal, drupalSettings, _, _db, GridStack) {
'use strict';
var _xsFixed = false;
Drupal.gridstack = Drupal.gridstack || {};
_.each(['base', 'ui', 'icon'], function (key) {
Drupal.gridstack[key] = Drupal.gridstack[key] || {};
});
/**
* GridStack form public methods.
*
* @namespace
*/
Drupal.gridstack.form = {
$form: null,
$main: null,
gridStacks: [],
isNested: false,
isValid: function (object) {
return !_.isNull(object) && !_.isUndefined(object);
},
hasGridStack: function (el) {
return el.length ? this.isValid(el[0].gridstack) : this.isValid(el.gridstack);
},
/**
* load a GridStack.
*
* @param {HTMLElement} root
* The GridStack HTML element.
* @param {bool} oldData
* Whether to load old data such as for a revert.
*
* @return {Drupal.gridstack.views.GridStack}
* Returns gridstack instance.
*/
load: function (root, oldData) {
var me = this;
var ui = Drupal.gridstack.ui;
var $root = $(root);
var data = {
el: root,
collection: ui.loadCollection(root, oldData, false),
isNested: me.isNested,
options: {
breakpoint: $root.data('breakpoint'),
icon: me.$form.data('icon'),
id: $root.attr('id'),
isNested: me.isNested || $root.data('framework') === 1,
storage: $root.data('storage'),
nestedStorage: $root.data('nestedStorage'),
noMargin: $('#edit-options-settings-nomargin').prop('checked')
}
};
return ui.loadGridStack(data);
},
/**
* Reverts to the last stored state.
*
* This is currently hidden, to avoid complication with multi-breakpoints
* which had added and removed elements which might be different from the
* stored data. With multi-breakpoints, this will cause different amounts,
* which means bad. However it is kept to fix broken XS breakpoint which
* is still a mistery. Likely due to at least 3 different versions of admin
* UI with and without XS, such as Bootstrap vs. Foundation vs. JS layouts.
*
* @param {HRMLElement} el
* The .gridstack--root HTML element.
*/
revert: function (el) {
var me = this;
if (me.hasGridStack(el)) {
var instance = el[0].gridstack;
var storage = el.data('storage');
var nestedStorage = el.data('nestedStorage');
// For some reason dataset is different from jquery data.
var grids = el.data('previewGrids') ? JSON.stringify(_db.parse(el[0].dataset.previewGrids)) : '';
var nested = el.data('nestedGrids') ? JSON.stringify(_db.parse(el[0].dataset.nestedGrids)) : '';
_.each(me.gridStacks, function (gridStack) {
if (gridStack.$el.is(el)) {
instance.removeAll();
instance.destroy(false);
el.empty();
// Load old data.
me.load(el, true).render();
$('[data-drupal-selector="' + storage + '"]').val(grids);
$('[data-drupal-selector="' + nestedStorage + '"]').val(nested);
}
});
}
}
};
/**
* GridStack form functions.
*
* @param {int} i
* The index of the current element.
* @param {HTMLElement} form
* The GridStack form HTML element.
*/
function gridStackForm(i, form) {
var me = Drupal.gridstack.form;
var main = '.gridstack--main';
var framework = '[data-drupal-selector="edit-options-use-framework"]';
var $framework = $(framework, form);
var $icon = $('#gridstack-icon');
var storedIconUrl = $icon.attr('data-url') || '';
me.$form = $(form);
me.$main = $(main);
me.isNested = $framework.prop('checked');
me.$form.removeClass('is-gs-nojs');
/**
* GridStack layout methods applied to the top level GridStack element.
*
* @param {int} i
* The index of the current element.
* @param {HTMLElement} root
* The GridStack HTML element.
*/
function gridStackRoot(i, root) {
var $root = $(root);
var gridStack = me.load(root, true);
/**
* Reacts on button click events.
*
* @param {jQuery.Event} e
* The event triggered by a `click` event.
*/
function onSaveIcon(e) {
e.preventDefault();
if (e.currentTarget === this) {
var $btn = $(this);
var message = $btn.data('message');
if (gridStack.collection && $root.hasClass('gridstack--main')) {
gridStack.collection.trigger('gridstack:main:' + message);
Drupal.gridstack.icon.build(form, true);
}
}
}
// Render the Backbone GridStack instance.
gridStack.render();
// Collect Backbone GridStack instances for aggregated CRUD.
me.gridStacks.push(gridStack);
// @todo remove temp fix for broken CSS breakpoint at non-js layouts.
if (!_xsFixed && me.isNested && $root.hasClass('gridstack--xs')) {
me.revert($root);
_xsFixed = true;
}
if ($root.hasClass('gridstack--main')) {
me.$form.off('click.gs.save').on('click.gs.save', '.btn--main.btn--save', onSaveIcon);
}
}
/**
* Build column selector.
*/
function onSelectColumn() {
$(this).change(function (e) {
if (e.target === this) {
var $select = $(this);
var $target = $($select.data('target'));
if (me.hasGridStack($target)) {
var instance = $target[0].gridstack;
var column = $select.val() || $target.data('gsColumn');
$target.removeClass(function (index, css) {
return (css.match(/grid-stack-(\d+)/g) || []).join(' ');
});
$target.addClass('grid-stack-' + column).attr('data-gs-column', column);
instance.column(column);
}
}
});
}
/**
* Build width selector which affects GridStack width for correct preview.
*
* @param {int} i
* The index of the current element.
* @param {HTMLElement} el
* The width input HTML element.
*/
function onInputWidth(i, el) {
var $el = $(el);
var updateWidth = function (input) {
var gs = input.data('target');
var $target = $(gs);
var $subPreview = $target.closest('.gridstack-preview--sub');
if ($target.length) {
var value = input.val();
var w = value ? parseInt(value) : parseInt($target.data('responsiveWidth'));
var width = w < 600 ? 600 : w;
if (value !== '') {
$target.css({width: width});
}
$subPreview[value === '' ? 'addClass' : 'removeClass']('form-disabled');
}
};
updateWidth($el);
$el.on('blur', function (e) {
if (e.target === this) {
var input = $(this);
updateWidth(input);
}
});
}
/**
* Sets the framework environment.
*/
function setFramework() {
me.isNested = $framework.prop('checked');
me.$form[me.isNested ? 'addClass' : 'removeClass']('is-framework');
}
/**
* Reacts on form submission.
*
* @param {jQuery.Event} e
* The event triggered by a `click` event.
*/
function onFormSubmit(e) {
me.$form.addClass('is-gs-saving');
// Some stored values dependent on :visible pseudo will not store with
// CSS display none, hence force them visible.
// Claro has class is-selected, Seven selected.
// Claro has vertical-tabs__item, Seven vertical-tabs__pane.
// @todo figure out to not rely on themes to avoid incompatibility.
$('.vertical-tabs > ul > li', me.$form).removeClass('selected is-selected');
$('.vertical-tabs > ul > li:last a', me.$form).click();
// @todo remove .vertical-tabs__item, .vertical-tabs__item .details-wrapper, .vertical-tabs__pane
$('.vertical-tabs > div > details, .vertical-tabs > div > details > div', me.$form).css('display', 'block').addClass('visually-hidden');
// @todo remove .vertical-tabs__item:last, .vertical-tabs__item:last .details-wrapper, .vertical-tabs__pane:last
$('.vertical-tabs > div > details:last, .vertical-tabs > div > details:last > div', me.$form).removeClass('visually-hidden');
// Failsafe to generate icon if "Save & continue" button is not hit.
$('.btn--gridstack[data-message="save"]').each(function () {
$(this).trigger('click');
});
}
/**
* Reacts on button click events.
*
* @param {jQuery.Event} e
* The event triggered by a `click` event.
*
* @todo move it to backbone if doable.
*/
function onBoxMultiple(e) {
e.preventDefault();
e.stopPropagation();
if (e.currentTarget === this) {
var btn = this;
var message = btn.dataset.message;
var type = btn.dataset.type || 'root';
var boxId = btn.dataset.gsBid;
_.each(me.gridStacks, function (gridStack) {
// Do not use direct child selector (>) to respect nested boxes.
// Do not use closest() to apply to all breakpoint boxes.
// Nested boxes are not backbone models, want a backbone model here,
// meaning must reference its parent box to get the model.
// The $box must be the actual referenced HTML box regardless models.
// No need to check for box validity here.
var $box = $('.box[data-gs-bid="' + boxId + '"]', gridStack.el);
var box = gridStack.getModel($box);
gridStack.collection.trigger('gridstack:' + type + ':' + message, e, box, $box);
});
}
}
/**
* Adds a new box to all breakpoints once on clicking `Add grid` button.
*
* @param {jQuery.Event} e
* The event triggered by a `click` event.
*/
function onAddMultiple(e) {
e.preventDefault();
if (e.currentTarget === this) {
Drupal.gridstack.base.lastBoxIndex++;
var box = new Drupal.gridstack.models.Box({
index: Drupal.gridstack.base.lastBoxIndex,
width: me.isNested ? 12 : 2
});
_.each(me.gridStacks, function (gridStack) {
gridStack.collection.add([box]);
});
}
}
/**
* Reacts on button `Revert` click events.
*
* @param {jQuery.Event} e
* The event triggered by a `click` event.
*/
function onRevert(e) {
e.preventDefault();
if (e.currentTarget === this && this.dataset.target) {
me.revert($(this.dataset.target));
}
}
// Loop through each GridStack root instance, not nested one.
$('.gridstack--root', form).each(gridStackRoot);
// Check if using CSS framework, or GridStack JS.
setFramework();
// Display icon if exists at public, or MODULE_NAME/images, directory.
if (storedIconUrl) {
var date = new Date();
$('#gridstack-screenshot', form).html('<img src="' + storedIconUrl + '?rand=' + date.getTime() + '" alt="Icon" />');
}
// Run actions.
me.$form.find('.form-text--width').each(onInputWidth);
me.$form.find('.form-select--column').each(onSelectColumn);
me.$form.off('click.gs.framework').on('click.gs.framework', framework, setFramework);
me.$form.off('click.gs.box').on('click.gs.box', '.btn--box', onBoxMultiple);
me.$form.off('click.gs.add').on('click.gs.add', '.btn--main.btn--add', onAddMultiple);
me.$form.off('click.gs.revert').on('click.gs.revert', '.btn--revert', onRevert);
me.$form.on('submit', onFormSubmit);
}
/**
* Attaches gridstack behavior to HTML element .form--gridstack.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.gridStackAdmin = {
attach: function (context) {
$('.form--gridstack', context).once('form-gridstack').each(gridStackForm);
}
};
})(jQuery, Drupal, drupalSettings, _, dBlazy, GridStack);
