Issue #2946603 by drpal, alexpott, dawehner: JS codestyle: no-use-before-define
parent
8b5cbee4fb
commit
7d2f3a3b76
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"extends": "./.eslintrc.json",
|
||||
"rules": {
|
||||
"no-use-before-define": "off",
|
||||
"no-shadow": "off",
|
||||
"no-new": "off",
|
||||
"no-continue": "off",
|
||||
|
|
|
@ -122,6 +122,9 @@
|
|||
response(suggestions);
|
||||
}
|
||||
|
||||
// Get the desired term and construct the autocomplete URL for it.
|
||||
const term = autocomplete.extractLastTerm(request.term);
|
||||
|
||||
/**
|
||||
* Transforms the data object into an array and update autocomplete results.
|
||||
*
|
||||
|
@ -135,9 +138,6 @@
|
|||
showSuggestions(data);
|
||||
}
|
||||
|
||||
// Get the desired term and construct the autocomplete URL for it.
|
||||
const term = autocomplete.extractLastTerm(request.term);
|
||||
|
||||
// Check if the term is already cached.
|
||||
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
|
||||
showSuggestions(autocomplete.cache[elementId][term]);
|
||||
|
|
|
@ -73,14 +73,14 @@
|
|||
response(suggestions);
|
||||
}
|
||||
|
||||
var term = autocomplete.extractLastTerm(request.term);
|
||||
|
||||
function sourceCallbackHandler(data) {
|
||||
autocomplete.cache[elementId][term] = data;
|
||||
|
||||
showSuggestions(data);
|
||||
}
|
||||
|
||||
var term = autocomplete.extractLastTerm(request.term);
|
||||
|
||||
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
|
||||
showSuggestions(autocomplete.cache[elementId][term]);
|
||||
} else {
|
||||
|
|
|
@ -65,13 +65,6 @@
|
|||
const dialog = {
|
||||
open: false,
|
||||
returnValue: undef,
|
||||
show() {
|
||||
openDialog({ modal: false });
|
||||
},
|
||||
showModal() {
|
||||
openDialog({ modal: true });
|
||||
},
|
||||
close: closeDialog,
|
||||
};
|
||||
|
||||
function openDialog(settings) {
|
||||
|
@ -91,6 +84,14 @@
|
|||
$(window).trigger('dialog:afterclose', [dialog, $element]);
|
||||
}
|
||||
|
||||
dialog.show = () => {
|
||||
openDialog({ modal: false });
|
||||
};
|
||||
dialog.showModal = () => {
|
||||
openDialog({ modal: true });
|
||||
};
|
||||
dialog.close = closeDialog;
|
||||
|
||||
return dialog;
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
|
|
|
@ -23,15 +23,7 @@
|
|||
var $element = $(element);
|
||||
var dialog = {
|
||||
open: false,
|
||||
returnValue: undef,
|
||||
show: function show() {
|
||||
openDialog({ modal: false });
|
||||
},
|
||||
showModal: function showModal() {
|
||||
openDialog({ modal: true });
|
||||
},
|
||||
|
||||
close: closeDialog
|
||||
returnValue: undef
|
||||
};
|
||||
|
||||
function openDialog(settings) {
|
||||
|
@ -51,6 +43,14 @@
|
|||
$(window).trigger('dialog:afterclose', [dialog, $element]);
|
||||
}
|
||||
|
||||
dialog.show = function () {
|
||||
openDialog({ modal: false });
|
||||
};
|
||||
dialog.showModal = function () {
|
||||
openDialog({ modal: true });
|
||||
};
|
||||
dialog.close = closeDialog;
|
||||
|
||||
return dialog;
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings);
|
|
@ -13,6 +13,31 @@
|
|||
// autoResize option will turn off resizable and draggable.
|
||||
drupalSettings.dialog = $.extend({ autoResize: true, maxHeight: '95%' }, drupalSettings.dialog);
|
||||
|
||||
/**
|
||||
* Position the dialog's center at the center of displace.offsets boundaries.
|
||||
*
|
||||
* @function Drupal.dialog~resetPosition
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object.
|
||||
*
|
||||
* @return {object}
|
||||
* Altered options object.
|
||||
*/
|
||||
function resetPosition(options) {
|
||||
const offsets = displace.offsets;
|
||||
const left = offsets.left - offsets.right;
|
||||
const top = offsets.top - offsets.bottom;
|
||||
|
||||
const leftString = `${(left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2))}px`;
|
||||
const topString = `${(top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2))}px`;
|
||||
options.position = {
|
||||
my: `center${left !== 0 ? leftString : ''} center${top !== 0 ? topString : ''}`,
|
||||
of: window,
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the current options for positioning.
|
||||
*
|
||||
|
@ -61,31 +86,6 @@
|
|||
.trigger('dialogContentResize');
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the dialog's center at the center of displace.offsets boundaries.
|
||||
*
|
||||
* @function Drupal.dialog~resetPosition
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object.
|
||||
*
|
||||
* @return {object}
|
||||
* Altered options object.
|
||||
*/
|
||||
function resetPosition(options) {
|
||||
const offsets = displace.offsets;
|
||||
const left = offsets.left - offsets.right;
|
||||
const top = offsets.top - offsets.bottom;
|
||||
|
||||
const leftString = `${(left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2))}px`;
|
||||
const topString = `${(top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2))}px`;
|
||||
options.position = {
|
||||
my: `center${left !== 0 ? leftString : ''} center${top !== 0 ? topString : ''}`,
|
||||
of: window,
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
$(window).on({
|
||||
'dialog:aftercreate': function (event, dialog, $element, settings) {
|
||||
const autoResize = debounce(resetSize, 20);
|
||||
|
|
|
@ -8,6 +8,20 @@
|
|||
(function ($, Drupal, drupalSettings, debounce, displace) {
|
||||
drupalSettings.dialog = $.extend({ autoResize: true, maxHeight: '95%' }, drupalSettings.dialog);
|
||||
|
||||
function resetPosition(options) {
|
||||
var offsets = displace.offsets;
|
||||
var left = offsets.left - offsets.right;
|
||||
var top = offsets.top - offsets.bottom;
|
||||
|
||||
var leftString = (left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2)) + 'px';
|
||||
var topString = (top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2)) + 'px';
|
||||
options.position = {
|
||||
my: 'center' + (left !== 0 ? leftString : '') + ' center' + (top !== 0 ? topString : ''),
|
||||
of: window
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
function resetSize(event) {
|
||||
var positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
|
||||
var adjustedOptions = {};
|
||||
|
@ -37,20 +51,6 @@
|
|||
event.data.$element.dialog('option', adjustedOptions).trigger('dialogContentResize');
|
||||
}
|
||||
|
||||
function resetPosition(options) {
|
||||
var offsets = displace.offsets;
|
||||
var left = offsets.left - offsets.right;
|
||||
var top = offsets.top - offsets.bottom;
|
||||
|
||||
var leftString = (left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2)) + 'px';
|
||||
var topString = (top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2)) + 'px';
|
||||
options.position = {
|
||||
my: 'center' + (left !== 0 ? leftString : '') + ' center' + (top !== 0 ? topString : ''),
|
||||
of: window
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
$(window).on({
|
||||
'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) {
|
||||
var autoResize = debounce(resetSize, 20);
|
||||
|
|
|
@ -37,110 +37,6 @@
|
|||
left: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a resize handler on the window.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.drupalDisplace = {
|
||||
attach() {
|
||||
// Mark this behavior as processed on the first pass.
|
||||
if (this.displaceProcessed) {
|
||||
return;
|
||||
}
|
||||
this.displaceProcessed = true;
|
||||
|
||||
$(window).on('resize.drupalDisplace', debounce(displace, 200));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Informs listeners of the current offset dimensions.
|
||||
*
|
||||
* @function Drupal.displace
|
||||
*
|
||||
* @prop {Drupal~displaceOffset} offsets
|
||||
*
|
||||
* @param {bool} [broadcast]
|
||||
* When true or undefined, causes the recalculated offsets values to be
|
||||
* broadcast to listeners.
|
||||
*
|
||||
* @return {Drupal~displaceOffset}
|
||||
* An object whose keys are the for sides an element -- top, right, bottom
|
||||
* and left. The value of each key is the viewport displacement distance for
|
||||
* that edge.
|
||||
*
|
||||
* @fires event:drupalViewportOffsetChange
|
||||
*/
|
||||
function displace(broadcast) {
|
||||
offsets = calculateOffsets();
|
||||
Drupal.displace.offsets = offsets;
|
||||
if (typeof broadcast === 'undefined' || broadcast) {
|
||||
$(document).trigger('drupalViewportOffsetChange', offsets);
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the viewport offsets.
|
||||
*
|
||||
* @return {Drupal~displaceOffset}
|
||||
* An object whose keys are the for sides an element -- top, right, bottom
|
||||
* and left. The value of each key is the viewport displacement distance for
|
||||
* that edge.
|
||||
*/
|
||||
function calculateOffsets() {
|
||||
return {
|
||||
top: calculateOffset('top'),
|
||||
right: calculateOffset('right'),
|
||||
bottom: calculateOffset('bottom'),
|
||||
left: calculateOffset('left'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific edge's offset.
|
||||
*
|
||||
* Any element with the attribute data-offset-{edge} e.g. data-offset-top will
|
||||
* be considered in the viewport offset calculations. If the attribute has a
|
||||
* numeric value, that value will be used. If no value is provided, one will
|
||||
* be calculated using the element's dimensions and placement.
|
||||
*
|
||||
* @function Drupal.displace.calculateOffset
|
||||
*
|
||||
* @param {string} edge
|
||||
* The name of the edge to calculate. Can be 'top', 'right',
|
||||
* 'bottom' or 'left'.
|
||||
*
|
||||
* @return {number}
|
||||
* The viewport displacement distance for the requested edge.
|
||||
*/
|
||||
function calculateOffset(edge) {
|
||||
let edgeOffset = 0;
|
||||
const displacingElements = document.querySelectorAll(`[data-offset-${edge}]`);
|
||||
const n = displacingElements.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const el = displacingElements[i];
|
||||
// If the element is not visible, do consider its dimensions.
|
||||
if (el.style.display === 'none') {
|
||||
continue;
|
||||
}
|
||||
// If the offset data attribute contains a displacing value, use it.
|
||||
let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
|
||||
// If the element's offset data attribute exits
|
||||
// but is not a valid number then get the displacement
|
||||
// dimensions directly from the element.
|
||||
if (isNaN(displacement)) {
|
||||
displacement = getRawOffset(el, edge);
|
||||
}
|
||||
// If the displacement value is larger than the current value for this
|
||||
// edge, use the displacement value.
|
||||
edgeOffset = Math.max(edgeOffset, displacement);
|
||||
}
|
||||
|
||||
return edgeOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates displacement for element based on its dimensions and placement.
|
||||
*
|
||||
|
@ -194,6 +90,110 @@
|
|||
return displacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific edge's offset.
|
||||
*
|
||||
* Any element with the attribute data-offset-{edge} e.g. data-offset-top will
|
||||
* be considered in the viewport offset calculations. If the attribute has a
|
||||
* numeric value, that value will be used. If no value is provided, one will
|
||||
* be calculated using the element's dimensions and placement.
|
||||
*
|
||||
* @function Drupal.displace.calculateOffset
|
||||
*
|
||||
* @param {string} edge
|
||||
* The name of the edge to calculate. Can be 'top', 'right',
|
||||
* 'bottom' or 'left'.
|
||||
*
|
||||
* @return {number}
|
||||
* The viewport displacement distance for the requested edge.
|
||||
*/
|
||||
function calculateOffset(edge) {
|
||||
let edgeOffset = 0;
|
||||
const displacingElements = document.querySelectorAll(`[data-offset-${edge}]`);
|
||||
const n = displacingElements.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const el = displacingElements[i];
|
||||
// If the element is not visible, do consider its dimensions.
|
||||
if (el.style.display === 'none') {
|
||||
continue;
|
||||
}
|
||||
// If the offset data attribute contains a displacing value, use it.
|
||||
let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
|
||||
// If the element's offset data attribute exits
|
||||
// but is not a valid number then get the displacement
|
||||
// dimensions directly from the element.
|
||||
if (isNaN(displacement)) {
|
||||
displacement = getRawOffset(el, edge);
|
||||
}
|
||||
// If the displacement value is larger than the current value for this
|
||||
// edge, use the displacement value.
|
||||
edgeOffset = Math.max(edgeOffset, displacement);
|
||||
}
|
||||
|
||||
return edgeOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the viewport offsets.
|
||||
*
|
||||
* @return {Drupal~displaceOffset}
|
||||
* An object whose keys are the for sides an element -- top, right, bottom
|
||||
* and left. The value of each key is the viewport displacement distance for
|
||||
* that edge.
|
||||
*/
|
||||
function calculateOffsets() {
|
||||
return {
|
||||
top: calculateOffset('top'),
|
||||
right: calculateOffset('right'),
|
||||
bottom: calculateOffset('bottom'),
|
||||
left: calculateOffset('left'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs listeners of the current offset dimensions.
|
||||
*
|
||||
* @function Drupal.displace
|
||||
*
|
||||
* @prop {Drupal~displaceOffset} offsets
|
||||
*
|
||||
* @param {bool} [broadcast]
|
||||
* When true or undefined, causes the recalculated offsets values to be
|
||||
* broadcast to listeners.
|
||||
*
|
||||
* @return {Drupal~displaceOffset}
|
||||
* An object whose keys are the for sides an element -- top, right, bottom
|
||||
* and left. The value of each key is the viewport displacement distance for
|
||||
* that edge.
|
||||
*
|
||||
* @fires event:drupalViewportOffsetChange
|
||||
*/
|
||||
function displace(broadcast) {
|
||||
offsets = calculateOffsets();
|
||||
Drupal.displace.offsets = offsets;
|
||||
if (typeof broadcast === 'undefined' || broadcast) {
|
||||
$(document).trigger('drupalViewportOffsetChange', offsets);
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a resize handler on the window.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.drupalDisplace = {
|
||||
attach() {
|
||||
// Mark this behavior as processed on the first pass.
|
||||
if (this.displaceProcessed) {
|
||||
return;
|
||||
}
|
||||
this.displaceProcessed = true;
|
||||
|
||||
$(window).on('resize.drupalDisplace', debounce(displace, 200));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the displace function to a property of the Drupal global object.
|
||||
*
|
||||
|
|
|
@ -13,58 +13,6 @@
|
|||
left: 0
|
||||
};
|
||||
|
||||
Drupal.behaviors.drupalDisplace = {
|
||||
attach: function attach() {
|
||||
if (this.displaceProcessed) {
|
||||
return;
|
||||
}
|
||||
this.displaceProcessed = true;
|
||||
|
||||
$(window).on('resize.drupalDisplace', debounce(displace, 200));
|
||||
}
|
||||
};
|
||||
|
||||
function displace(broadcast) {
|
||||
offsets = calculateOffsets();
|
||||
Drupal.displace.offsets = offsets;
|
||||
if (typeof broadcast === 'undefined' || broadcast) {
|
||||
$(document).trigger('drupalViewportOffsetChange', offsets);
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
function calculateOffsets() {
|
||||
return {
|
||||
top: calculateOffset('top'),
|
||||
right: calculateOffset('right'),
|
||||
bottom: calculateOffset('bottom'),
|
||||
left: calculateOffset('left')
|
||||
};
|
||||
}
|
||||
|
||||
function calculateOffset(edge) {
|
||||
var edgeOffset = 0;
|
||||
var displacingElements = document.querySelectorAll('[data-offset-' + edge + ']');
|
||||
var n = displacingElements.length;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var el = displacingElements[i];
|
||||
|
||||
if (el.style.display === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var displacement = parseInt(el.getAttribute('data-offset-' + edge), 10);
|
||||
|
||||
if (isNaN(displacement)) {
|
||||
displacement = getRawOffset(el, edge);
|
||||
}
|
||||
|
||||
edgeOffset = Math.max(edgeOffset, displacement);
|
||||
}
|
||||
|
||||
return edgeOffset;
|
||||
}
|
||||
|
||||
function getRawOffset(el, edge) {
|
||||
var $el = $(el);
|
||||
var documentElement = document.documentElement;
|
||||
|
@ -98,6 +46,58 @@
|
|||
return displacement;
|
||||
}
|
||||
|
||||
function calculateOffset(edge) {
|
||||
var edgeOffset = 0;
|
||||
var displacingElements = document.querySelectorAll('[data-offset-' + edge + ']');
|
||||
var n = displacingElements.length;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var el = displacingElements[i];
|
||||
|
||||
if (el.style.display === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
var displacement = parseInt(el.getAttribute('data-offset-' + edge), 10);
|
||||
|
||||
if (isNaN(displacement)) {
|
||||
displacement = getRawOffset(el, edge);
|
||||
}
|
||||
|
||||
edgeOffset = Math.max(edgeOffset, displacement);
|
||||
}
|
||||
|
||||
return edgeOffset;
|
||||
}
|
||||
|
||||
function calculateOffsets() {
|
||||
return {
|
||||
top: calculateOffset('top'),
|
||||
right: calculateOffset('right'),
|
||||
bottom: calculateOffset('bottom'),
|
||||
left: calculateOffset('left')
|
||||
};
|
||||
}
|
||||
|
||||
function displace(broadcast) {
|
||||
offsets = calculateOffsets();
|
||||
Drupal.displace.offsets = offsets;
|
||||
if (typeof broadcast === 'undefined' || broadcast) {
|
||||
$(document).trigger('drupalViewportOffsetChange', offsets);
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
Drupal.behaviors.drupalDisplace = {
|
||||
attach: function attach() {
|
||||
if (this.displaceProcessed) {
|
||||
return;
|
||||
}
|
||||
this.displaceProcessed = true;
|
||||
|
||||
$(window).on('resize.drupalDisplace', debounce(displace, 200));
|
||||
}
|
||||
};
|
||||
|
||||
Drupal.displace = displace;
|
||||
$.extend(Drupal.displace, {
|
||||
offsets: offsets,
|
||||
|
|
|
@ -4,45 +4,6 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Process elements with the .dropbutton class on page load.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches dropButton behaviors.
|
||||
*/
|
||||
Drupal.behaviors.dropButton = {
|
||||
attach(context, settings) {
|
||||
const $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
|
||||
if ($dropbuttons.length) {
|
||||
// Adds the delegated handler that will toggle dropdowns on click.
|
||||
const $body = $('body').once('dropbutton-click');
|
||||
if ($body.length) {
|
||||
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
|
||||
}
|
||||
// Initialize all buttons.
|
||||
const il = $dropbuttons.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Delegated callback for opening and closing dropbutton secondary actions.
|
||||
*
|
||||
* @function Drupal.DropButton~dropbuttonClickHandler
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
function dropbuttonClickHandler(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* A DropButton presents an HTML list as a button with a primary action.
|
||||
*
|
||||
|
@ -127,6 +88,45 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegated callback for opening and closing dropbutton secondary actions.
|
||||
*
|
||||
* @function Drupal.DropButton~dropbuttonClickHandler
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
function dropbuttonClickHandler(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process elements with the .dropbutton class on page load.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches dropButton behaviors.
|
||||
*/
|
||||
Drupal.behaviors.dropButton = {
|
||||
attach(context, settings) {
|
||||
const $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
|
||||
if ($dropbuttons.length) {
|
||||
// Adds the delegated handler that will toggle dropdowns on click.
|
||||
const $body = $('body').once('dropbutton-click');
|
||||
if ($body.length) {
|
||||
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
|
||||
}
|
||||
// Initialize all buttons.
|
||||
const il = $dropbuttons.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend the DropButton constructor.
|
||||
*/
|
||||
|
|
|
@ -6,28 +6,6 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
Drupal.behaviors.dropButton = {
|
||||
attach: function attach(context, settings) {
|
||||
var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
|
||||
if ($dropbuttons.length) {
|
||||
var $body = $('body').once('dropbutton-click');
|
||||
if ($body.length) {
|
||||
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
|
||||
}
|
||||
|
||||
var il = $dropbuttons.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function dropbuttonClickHandler(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
|
||||
}
|
||||
|
||||
function DropButton(dropbutton, settings) {
|
||||
var options = $.extend({ title: Drupal.t('List additional actions') }, settings);
|
||||
var $dropbutton = $(dropbutton);
|
||||
|
@ -60,6 +38,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
function dropbuttonClickHandler(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
|
||||
}
|
||||
|
||||
Drupal.behaviors.dropButton = {
|
||||
attach: function attach(context, settings) {
|
||||
var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
|
||||
if ($dropbuttons.length) {
|
||||
var $body = $('body').once('dropbutton-click');
|
||||
if ($body.length) {
|
||||
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
|
||||
}
|
||||
|
||||
var il = $dropbuttons.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.extend(DropButton, {
|
||||
dropbuttons: []
|
||||
});
|
||||
|
|
|
@ -22,6 +22,44 @@
|
|||
|
||||
Drupal.states = states;
|
||||
|
||||
/**
|
||||
* Inverts a (if it's not undefined) when invertState is true.
|
||||
*
|
||||
* @function Drupal.states~invert
|
||||
*
|
||||
* @param {*} a
|
||||
* The value to maybe invert.
|
||||
* @param {bool} invertState
|
||||
* Whether to invert state or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* The result.
|
||||
*/
|
||||
function invert(a, invertState) {
|
||||
return (invertState && typeof a !== 'undefined') ? !a : a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two values while ignoring undefined values.
|
||||
*
|
||||
* @function Drupal.states~compare
|
||||
*
|
||||
* @param {*} a
|
||||
* Value a.
|
||||
* @param {*} b
|
||||
* Value b.
|
||||
*
|
||||
* @return {bool}
|
||||
* The comparison result.
|
||||
*/
|
||||
function compare(a, b) {
|
||||
if (a === b) {
|
||||
return typeof a === 'undefined' ? a : true;
|
||||
}
|
||||
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the states.
|
||||
*
|
||||
|
@ -642,47 +680,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* These are helper functions implementing addition "operators" and don't
|
||||
* implement any logic that is particular to states.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Inverts a (if it's not undefined) when invertState is true.
|
||||
*
|
||||
* @function Drupal.states~invert
|
||||
*
|
||||
* @param {*} a
|
||||
* The value to maybe invert.
|
||||
* @param {bool} invertState
|
||||
* Whether to invert state or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* The result.
|
||||
*/
|
||||
function invert(a, invertState) {
|
||||
return (invertState && typeof a !== 'undefined') ? !a : a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two values while ignoring undefined values.
|
||||
*
|
||||
* @function Drupal.states~compare
|
||||
*
|
||||
* @param {*} a
|
||||
* Value a.
|
||||
* @param {*} b
|
||||
* Value b.
|
||||
*
|
||||
* @return {bool}
|
||||
* The comparison result.
|
||||
*/
|
||||
function compare(a, b) {
|
||||
if (a === b) {
|
||||
return typeof a === 'undefined' ? a : true;
|
||||
}
|
||||
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
}(jQuery, Drupal));
|
||||
|
|
|
@ -12,6 +12,18 @@
|
|||
|
||||
Drupal.states = states;
|
||||
|
||||
function invert(a, invertState) {
|
||||
return invertState && typeof a !== 'undefined' ? !a : a;
|
||||
}
|
||||
|
||||
function _compare2(a, b) {
|
||||
if (a === b) {
|
||||
return typeof a === 'undefined' ? a : true;
|
||||
}
|
||||
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
|
||||
Drupal.behaviors.states = {
|
||||
attach: function attach(context, settings) {
|
||||
var $states = $(context).find('[data-drupal-states]');
|
||||
|
@ -345,16 +357,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
function invert(a, invertState) {
|
||||
return invertState && typeof a !== 'undefined' ? !a : a;
|
||||
}
|
||||
|
||||
function _compare2(a, b) {
|
||||
if (a === b) {
|
||||
return typeof a === 'undefined' ? a : true;
|
||||
}
|
||||
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
})(jQuery, Drupal);
|
|
@ -46,6 +46,61 @@
|
|||
this.stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a set of tabbable elements.
|
||||
*
|
||||
* This constraint can be removed with the release() method.
|
||||
*
|
||||
* @constructor Drupal~TabbingContext
|
||||
*
|
||||
* @param {object} options
|
||||
* A set of initiating values
|
||||
* @param {number} options.level
|
||||
* The level in the TabbingManager's stack of this tabbingContext.
|
||||
* @param {jQuery} options.$tabbableElements
|
||||
* The DOM elements that should be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {jQuery} options.$disabledElements
|
||||
* The DOM elements that should not be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {bool} options.released
|
||||
* A released tabbingContext can never be activated again. It will be
|
||||
* cleaned up when the TabbingManager unwinds its stack.
|
||||
* @param {bool} options.active
|
||||
* When true, the tabbable elements of this tabbingContext will be reachable
|
||||
* via the tab key and the disabled elements will not. Only one
|
||||
* tabbingContext can be active at a time.
|
||||
*/
|
||||
function TabbingContext(options) {
|
||||
$.extend(this, /** @lends Drupal~TabbingContext# */{
|
||||
|
||||
/**
|
||||
* @type {?number}
|
||||
*/
|
||||
level: null,
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$tabbableElements: $(),
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$disabledElements: $(),
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
released: false,
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
active: false,
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingManager class.
|
||||
*/
|
||||
|
@ -242,61 +297,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Stores a set of tabbable elements.
|
||||
*
|
||||
* This constraint can be removed with the release() method.
|
||||
*
|
||||
* @constructor Drupal~TabbingContext
|
||||
*
|
||||
* @param {object} options
|
||||
* A set of initiating values
|
||||
* @param {number} options.level
|
||||
* The level in the TabbingManager's stack of this tabbingContext.
|
||||
* @param {jQuery} options.$tabbableElements
|
||||
* The DOM elements that should be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {jQuery} options.$disabledElements
|
||||
* The DOM elements that should not be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {bool} options.released
|
||||
* A released tabbingContext can never be activated again. It will be
|
||||
* cleaned up when the TabbingManager unwinds its stack.
|
||||
* @param {bool} options.active
|
||||
* When true, the tabbable elements of this tabbingContext will be reachable
|
||||
* via the tab key and the disabled elements will not. Only one
|
||||
* tabbingContext can be active at a time.
|
||||
*/
|
||||
function TabbingContext(options) {
|
||||
$.extend(this, /** @lends Drupal~TabbingContext# */{
|
||||
|
||||
/**
|
||||
* @type {?number}
|
||||
*/
|
||||
level: null,
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$tabbableElements: $(),
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$disabledElements: $(),
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
released: false,
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
active: false,
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingContext class.
|
||||
*/
|
||||
|
|
|
@ -10,6 +10,20 @@
|
|||
this.stack = [];
|
||||
}
|
||||
|
||||
function TabbingContext(options) {
|
||||
$.extend(this, {
|
||||
level: null,
|
||||
|
||||
$tabbableElements: $(),
|
||||
|
||||
$disabledElements: $(),
|
||||
|
||||
released: false,
|
||||
|
||||
active: false
|
||||
}, options);
|
||||
}
|
||||
|
||||
$.extend(TabbingManager.prototype, {
|
||||
constrain: function constrain(elements) {
|
||||
var il = this.stack.length;
|
||||
|
@ -109,20 +123,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
function TabbingContext(options) {
|
||||
$.extend(this, {
|
||||
level: null,
|
||||
|
||||
$tabbableElements: $(),
|
||||
|
||||
$disabledElements: $(),
|
||||
|
||||
released: false,
|
||||
|
||||
active: false
|
||||
}, options);
|
||||
}
|
||||
|
||||
$.extend(TabbingContext.prototype, {
|
||||
release: function release() {
|
||||
if (!this.released) {
|
||||
|
|
|
@ -4,91 +4,6 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, displace) {
|
||||
/**
|
||||
* Attaches sticky table headers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the sticky table header behavior.
|
||||
*/
|
||||
Drupal.behaviors.tableHeader = {
|
||||
attach(context) {
|
||||
$(window).one('scroll.TableHeaderInit', { context }, tableHeaderInitHandler);
|
||||
},
|
||||
};
|
||||
|
||||
function scrollValue(position) {
|
||||
return document.documentElement[position] || document.body[position];
|
||||
}
|
||||
|
||||
// Select and initialize sticky table headers.
|
||||
function tableHeaderInitHandler(e) {
|
||||
const $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
|
||||
const il = $tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
TableHeader.tables.push(new TableHeader($tables[i]));
|
||||
}
|
||||
forTables('onScroll');
|
||||
}
|
||||
|
||||
// Helper method to loop through tables and execute a method.
|
||||
function forTables(method, arg) {
|
||||
const tables = TableHeader.tables;
|
||||
const il = tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
tables[i][method](arg);
|
||||
}
|
||||
}
|
||||
|
||||
function tableHeaderResizeHandler(e) {
|
||||
forTables('recalculateSticky');
|
||||
}
|
||||
|
||||
function tableHeaderOnScrollHandler(e) {
|
||||
forTables('onScroll');
|
||||
}
|
||||
|
||||
function tableHeaderOffsetChangeHandler(e, offsets) {
|
||||
forTables('stickyPosition', offsets.top);
|
||||
}
|
||||
|
||||
// Bind event that need to change all tables.
|
||||
$(window).on({
|
||||
|
||||
/**
|
||||
* When resizing table width can change, recalculate everything.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'resize.TableHeader': tableHeaderResizeHandler,
|
||||
|
||||
/**
|
||||
* Bind only one event to take care of calling all scroll callbacks.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'scroll.TableHeader': tableHeaderOnScrollHandler,
|
||||
});
|
||||
// Bind to custom Drupal events.
|
||||
$(document).on({
|
||||
|
||||
/**
|
||||
* Recalculate columns width when window is resized and when show/hide
|
||||
* weight is triggered.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'columnschange.TableHeader': tableHeaderResizeHandler,
|
||||
|
||||
/**
|
||||
* Recalculate TableHeader.topOffset when viewport is resized.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructor for the tableHeader object. Provides sticky table headers.
|
||||
*
|
||||
|
@ -143,6 +58,91 @@
|
|||
this.createSticky();
|
||||
}
|
||||
|
||||
// Helper method to loop through tables and execute a method.
|
||||
function forTables(method, arg) {
|
||||
const tables = TableHeader.tables;
|
||||
const il = tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
tables[i][method](arg);
|
||||
}
|
||||
}
|
||||
|
||||
// Select and initialize sticky table headers.
|
||||
function tableHeaderInitHandler(e) {
|
||||
const $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
|
||||
const il = $tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
TableHeader.tables.push(new TableHeader($tables[i]));
|
||||
}
|
||||
forTables('onScroll');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches sticky table headers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the sticky table header behavior.
|
||||
*/
|
||||
Drupal.behaviors.tableHeader = {
|
||||
attach(context) {
|
||||
$(window).one('scroll.TableHeaderInit', { context }, tableHeaderInitHandler);
|
||||
},
|
||||
};
|
||||
|
||||
function scrollValue(position) {
|
||||
return document.documentElement[position] || document.body[position];
|
||||
}
|
||||
|
||||
function tableHeaderResizeHandler(e) {
|
||||
forTables('recalculateSticky');
|
||||
}
|
||||
|
||||
function tableHeaderOnScrollHandler(e) {
|
||||
forTables('onScroll');
|
||||
}
|
||||
|
||||
function tableHeaderOffsetChangeHandler(e, offsets) {
|
||||
forTables('stickyPosition', offsets.top);
|
||||
}
|
||||
|
||||
// Bind event that need to change all tables.
|
||||
$(window).on({
|
||||
|
||||
/**
|
||||
* When resizing table width can change, recalculate everything.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'resize.TableHeader': tableHeaderResizeHandler,
|
||||
|
||||
/**
|
||||
* Bind only one event to take care of calling all scroll callbacks.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'scroll.TableHeader': tableHeaderOnScrollHandler,
|
||||
});
|
||||
// Bind to custom Drupal events.
|
||||
$(document).on({
|
||||
|
||||
/**
|
||||
* Recalculate columns width when window is resized and when show/hide
|
||||
* weight is triggered.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'columnschange.TableHeader': tableHeaderResizeHandler,
|
||||
|
||||
/**
|
||||
* Recalculate TableHeader.topOffset when viewport is resized.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
|
||||
});
|
||||
|
||||
/**
|
||||
* Store the state of TableHeader.
|
||||
*/
|
||||
|
|
|
@ -6,14 +6,37 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, displace) {
|
||||
Drupal.behaviors.tableHeader = {
|
||||
attach: function attach(context) {
|
||||
$(window).one('scroll.TableHeaderInit', { context: context }, tableHeaderInitHandler);
|
||||
}
|
||||
};
|
||||
function TableHeader(table) {
|
||||
var $table = $(table);
|
||||
|
||||
function scrollValue(position) {
|
||||
return document.documentElement[position] || document.body[position];
|
||||
this.$originalTable = $table;
|
||||
|
||||
this.$originalHeader = $table.children('thead');
|
||||
|
||||
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
|
||||
|
||||
this.displayWeight = null;
|
||||
this.$originalTable.addClass('sticky-table');
|
||||
this.tableHeight = $table[0].clientHeight;
|
||||
this.tableOffset = this.$originalTable.offset();
|
||||
|
||||
this.$originalTable.on('columnschange', { tableHeader: this }, function (e, display) {
|
||||
var tableHeader = e.data.tableHeader;
|
||||
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
|
||||
tableHeader.recalculateSticky();
|
||||
}
|
||||
tableHeader.displayWeight = display;
|
||||
});
|
||||
|
||||
this.createSticky();
|
||||
}
|
||||
|
||||
function forTables(method, arg) {
|
||||
var tables = TableHeader.tables;
|
||||
var il = tables.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
tables[i][method](arg);
|
||||
}
|
||||
}
|
||||
|
||||
function tableHeaderInitHandler(e) {
|
||||
|
@ -25,12 +48,14 @@
|
|||
forTables('onScroll');
|
||||
}
|
||||
|
||||
function forTables(method, arg) {
|
||||
var tables = TableHeader.tables;
|
||||
var il = tables.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
tables[i][method](arg);
|
||||
Drupal.behaviors.tableHeader = {
|
||||
attach: function attach(context) {
|
||||
$(window).one('scroll.TableHeaderInit', { context: context }, tableHeaderInitHandler);
|
||||
}
|
||||
};
|
||||
|
||||
function scrollValue(position) {
|
||||
return document.documentElement[position] || document.body[position];
|
||||
}
|
||||
|
||||
function tableHeaderResizeHandler(e) {
|
||||
|
@ -57,31 +82,6 @@
|
|||
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler
|
||||
});
|
||||
|
||||
function TableHeader(table) {
|
||||
var $table = $(table);
|
||||
|
||||
this.$originalTable = $table;
|
||||
|
||||
this.$originalHeader = $table.children('thead');
|
||||
|
||||
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
|
||||
|
||||
this.displayWeight = null;
|
||||
this.$originalTable.addClass('sticky-table');
|
||||
this.tableHeight = $table[0].clientHeight;
|
||||
this.tableOffset = this.$originalTable.offset();
|
||||
|
||||
this.$originalTable.on('columnschange', { tableHeader: this }, function (e, display) {
|
||||
var tableHeader = e.data.tableHeader;
|
||||
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
|
||||
tableHeader.recalculateSticky();
|
||||
}
|
||||
tableHeader.displayWeight = display;
|
||||
});
|
||||
|
||||
this.createSticky();
|
||||
}
|
||||
|
||||
$.extend(TableHeader, {
|
||||
tables: []
|
||||
});
|
||||
|
|
|
@ -4,26 +4,6 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
/**
|
||||
* Attach the tableResponsive function to {@link Drupal.behaviors}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches tableResponsive functionality.
|
||||
*/
|
||||
Drupal.behaviors.tableResponsive = {
|
||||
attach(context, settings) {
|
||||
const $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
|
||||
if ($tables.length) {
|
||||
const il = $tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
TableResponsive.tables.push(new TableResponsive($tables[i]));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The TableResponsive object optimizes table presentation for screen size.
|
||||
*
|
||||
|
@ -60,6 +40,26 @@
|
|||
.trigger('resize.tableresponsive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the tableResponsive function to {@link Drupal.behaviors}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches tableResponsive functionality.
|
||||
*/
|
||||
Drupal.behaviors.tableResponsive = {
|
||||
attach(context, settings) {
|
||||
const $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
|
||||
if ($tables.length) {
|
||||
const il = $tables.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
TableResponsive.tables.push(new TableResponsive($tables[i]));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend the TableResponsive function with a list of managed tables.
|
||||
*/
|
||||
|
|
|
@ -6,18 +6,6 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
Drupal.behaviors.tableResponsive = {
|
||||
attach: function attach(context, settings) {
|
||||
var $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
|
||||
if ($tables.length) {
|
||||
var il = $tables.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
TableResponsive.tables.push(new TableResponsive($tables[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function TableResponsive(table) {
|
||||
this.table = table;
|
||||
this.$table = $(table);
|
||||
|
@ -33,6 +21,18 @@
|
|||
$(window).on('resize.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility')).trigger('resize.tableresponsive');
|
||||
}
|
||||
|
||||
Drupal.behaviors.tableResponsive = {
|
||||
attach: function attach(context, settings) {
|
||||
var $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
|
||||
if ($tables.length) {
|
||||
var il = $tables.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
TableResponsive.tables.push(new TableResponsive($tables[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.extend(TableResponsive, {
|
||||
tables: []
|
||||
});
|
||||
|
|
|
@ -4,6 +4,28 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX response.
|
||||
*
|
||||
* @param {string} content
|
||||
* The text content of a <script type="application/vnd.drupal-ajax"> DOM node.
|
||||
* @return {Array|boolean}
|
||||
* The parsed Ajax response containing an array of Ajax commands, or false in
|
||||
* case the DOM node hasn't fully arrived yet.
|
||||
*/
|
||||
function mapTextContentToAjaxResponse(content) {
|
||||
if (content === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Ajax commands in <script type="application/vnd.drupal-ajax"> tag.
|
||||
*
|
||||
|
@ -46,27 +68,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX response.
|
||||
*
|
||||
* @param {string} content
|
||||
* The text content of a <script type="application/vnd.drupal-ajax"> DOM node.
|
||||
* @return {Array|boolean}
|
||||
* The parsed Ajax response containing an array of Ajax commands, or false in
|
||||
* case the DOM node hasn't fully arrived yet.
|
||||
*/
|
||||
function mapTextContentToAjaxResponse(content) {
|
||||
if (content === '') {
|
||||
return false;
|
||||
}
|
||||
// The frequency with which to check for newly arrived BigPipe placeholders.
|
||||
// Hence 50 ms means we check 20 times per second. Setting this to 100 ms or
|
||||
// more would cause the user to see content appear noticeably slower.
|
||||
const interval = drupalSettings.bigPipeInterval || 50;
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// The internal ID to contain the watcher service.
|
||||
let timeoutID;
|
||||
|
||||
/**
|
||||
* Processes a streamed HTML document receiving placeholder replacements.
|
||||
|
@ -109,13 +117,6 @@
|
|||
}, interval);
|
||||
}
|
||||
|
||||
// The frequency with which to check for newly arrived BigPipe placeholders.
|
||||
// Hence 50 ms means we check 20 times per second. Setting this to 100 ms or
|
||||
// more would cause the user to see content appear noticeably slower.
|
||||
const interval = drupalSettings.bigPipeInterval || 50;
|
||||
// The internal ID to contain the watcher service.
|
||||
let timeoutID;
|
||||
|
||||
bigPipeProcess();
|
||||
|
||||
// If something goes wrong, make sure everything is cleaned up and has had a
|
||||
|
|
|
@ -6,6 +6,18 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
function mapTextContentToAjaxResponse(content) {
|
||||
if (content === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function bigPipeProcessPlaceholderReplacement(index, placeholderReplacement) {
|
||||
var placeholderId = placeholderReplacement.getAttribute('data-big-pipe-replacement-for-placeholder-with-id');
|
||||
var content = this.textContent.trim();
|
||||
|
@ -28,17 +40,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
function mapTextContentToAjaxResponse(content) {
|
||||
if (content === '') {
|
||||
return false;
|
||||
}
|
||||
var interval = drupalSettings.bigPipeInterval || 50;
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var timeoutID = void 0;
|
||||
|
||||
function bigPipeProcessDocument(context) {
|
||||
if (!context.querySelector('script[data-big-pipe-event="start"]')) {
|
||||
|
@ -65,10 +69,6 @@
|
|||
}, interval);
|
||||
}
|
||||
|
||||
var interval = drupalSettings.bigPipeInterval || 50;
|
||||
|
||||
var timeoutID = void 0;
|
||||
|
||||
bigPipeProcess();
|
||||
|
||||
$(window).on('load', function () {
|
||||
|
|
|
@ -215,6 +215,7 @@
|
|||
* Closes the dialog when the user cancels or supplies valid data.
|
||||
*/
|
||||
function shutdown() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
dialog.close(action);
|
||||
|
||||
// The processing marker can be deleted since the dialog has been
|
||||
|
@ -346,6 +347,7 @@
|
|||
$(event.target).remove();
|
||||
},
|
||||
});
|
||||
|
||||
// A modal dialog is used because the user must provide a button group
|
||||
// name or cancel the button placement before taking any other action.
|
||||
dialog.showModal();
|
||||
|
|
|
@ -14,6 +14,77 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused image2 widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrates the drupalimage widget with the drupallink plugin.
|
||||
*
|
||||
* Makes images linkable.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
// Nothing to integrate with if the drupallink plugin is not loaded.
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
// Override 'drupalunlink' only when link truly belongs to the widget. If
|
||||
// wrapped inline widget in a link, let default unlink work.
|
||||
// @see https://dev.ckeditor.com/ticket/11814
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
// Selection (which is fake) may not change if unlinked image in focused
|
||||
// widget, i.e. if captioned image. Let's refresh command state manually
|
||||
// here.
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Override default refresh of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that widget may be wrapped in a link, which
|
||||
// does not belong to that widget (#11814).
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
|
||||
CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimage', {
|
||||
requires: 'image2',
|
||||
icons: 'drupalimage',
|
||||
|
@ -289,77 +360,6 @@
|
|||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Integrates the drupalimage widget with the drupallink plugin.
|
||||
*
|
||||
* Makes images linkable.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
// Nothing to integrate with if the drupallink plugin is not loaded.
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
// Override 'drupalunlink' only when link truly belongs to the widget. If
|
||||
// wrapped inline widget in a link, let default unlink work.
|
||||
// @see https://dev.ckeditor.com/ticket/11814
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
// Selection (which is fake) may not change if unlinked image in focused
|
||||
// widget, i.e. if captioned image. Let's refresh command state manually
|
||||
// here.
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Override default refresh of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that widget may be wrapped in a link, which
|
||||
// does not belong to that widget (#11814).
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
|
||||
CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused image2 widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupalimage widgets.
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget,
|
||||
|
|
|
@ -6,6 +6,48 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimage', {
|
||||
requires: 'image2',
|
||||
icons: 'drupalimage',
|
||||
|
@ -204,48 +246,6 @@
|
|||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget: getFocusedWidget
|
||||
};
|
||||
|
|
|
@ -11,6 +11,36 @@
|
|||
*/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
* Function will check first the passed element itself and then all its
|
||||
* children in DFS order.
|
||||
*
|
||||
* @param {CKEDITOR.htmlParser.element} element
|
||||
* The element to search.
|
||||
* @param {string} name
|
||||
* The element name to search for.
|
||||
*
|
||||
* @return {?CKEDITOR.htmlParser.element}
|
||||
* The found element, or null.
|
||||
*/
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
let found = null;
|
||||
element.forEach((el) => {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
|
@ -263,34 +293,4 @@
|
|||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
* Function will check first the passed element itself and then all its
|
||||
* children in DFS order.
|
||||
*
|
||||
* @param {CKEDITOR.htmlParser.element} element
|
||||
* The element to search.
|
||||
* @param {string} name
|
||||
* The element name to search for.
|
||||
*
|
||||
* @return {?CKEDITOR.htmlParser.element}
|
||||
* The found element, or null.
|
||||
*/
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
let found = null;
|
||||
element.forEach((el) => {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
}(CKEDITOR));
|
||||
|
|
|
@ -6,6 +6,22 @@
|
|||
**/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
|
@ -192,20 +208,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
})(CKEDITOR);
|
|
@ -54,6 +54,42 @@
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
* The following selection will all return the link element.
|
||||
*
|
||||
* @example
|
||||
* <a href="#">li^nk</a>
|
||||
* <a href="#">[link]</a>
|
||||
* text[<a href="#">link]</a>
|
||||
* <a href="#">li[nk</a>]
|
||||
* [<b><a href="#">li]nk</a></b>]
|
||||
* [<a href="#"><b>li]nk</b></a>
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* The CKEditor editor object
|
||||
*
|
||||
* @return {?HTMLElement}
|
||||
* The selected link element, or null.
|
||||
*
|
||||
*/
|
||||
function getSelectedLink(editor) {
|
||||
const selection = editor.getSelection();
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
const range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
|
@ -248,42 +284,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
* The following selection will all return the link element.
|
||||
*
|
||||
* @example
|
||||
* <a href="#">li^nk</a>
|
||||
* <a href="#">[link]</a>
|
||||
* text[<a href="#">link]</a>
|
||||
* <a href="#">li[nk</a>]
|
||||
* [<b><a href="#">li]nk</a></b>]
|
||||
* [<a href="#"><b>li]nk</b></a>
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* The CKEditor editor object
|
||||
*
|
||||
* @return {?HTMLElement}
|
||||
* The selected link element, or null.
|
||||
*
|
||||
*/
|
||||
function getSelectedLink(editor) {
|
||||
const selection = editor.getSelection();
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
const range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupallink widgets.
|
||||
// (Compatible with the official CKEditor link plugin's API:
|
||||
// http://dev.ckeditor.com/ticket/13885.)
|
||||
|
|
|
@ -49,6 +49,22 @@
|
|||
};
|
||||
}
|
||||
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
var range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
|
@ -216,22 +232,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
var range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes
|
||||
|
|
|
@ -40,37 +40,6 @@
|
|||
// Build a preview.
|
||||
const height = [];
|
||||
const width = [];
|
||||
// Loop through all defined gradients.
|
||||
Object.keys(settings.gradients || {}).forEach((i) => {
|
||||
// Add element to display the gradient.
|
||||
$('.color-preview').once('color').append(`<div id="gradient-${i}"></div>`);
|
||||
const gradient = $(`.color-preview #gradient-${i}`);
|
||||
// Add height of current gradient to the list (divided by 10).
|
||||
height.push(parseInt(gradient.css('height'), 10) / 10);
|
||||
// Add width of current gradient to the list (divided by 10).
|
||||
width.push(parseInt(gradient.css('width'), 10) / 10);
|
||||
// Add rows (or columns for horizontal gradients).
|
||||
// Each gradient line should have a height (or width for horizontal
|
||||
// gradients) of 10px (because we divided the height/width by 10
|
||||
// above).
|
||||
for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) {
|
||||
gradient.append('<div class="gradient-line"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
// Set up colorScheme selector.
|
||||
form.find('#edit-scheme').on('change', function () {
|
||||
const schemes = settings.color.schemes;
|
||||
const colorScheme = this.options[this.selectedIndex].value;
|
||||
if (colorScheme !== '' && schemes[colorScheme]) {
|
||||
// Get colors of active scheme.
|
||||
colors = schemes[colorScheme];
|
||||
Object.keys(colors || {}).forEach((fieldName) => {
|
||||
callback($(`#edit-palette-${fieldName}`), colors[fieldName], false, true);
|
||||
});
|
||||
preview();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the preview.
|
||||
|
@ -79,6 +48,15 @@
|
|||
Drupal.color.callback(context, settings, form, farb, height, width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the color scheme selector.
|
||||
*/
|
||||
function resetScheme() {
|
||||
form.find('#edit-scheme').each(function () {
|
||||
this.selectedIndex = this.options.length - 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts a given color, using a reference pair (ref in HSL).
|
||||
*
|
||||
|
@ -193,14 +171,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the color scheme selector.
|
||||
*/
|
||||
function resetScheme() {
|
||||
form.find('#edit-scheme').each(function () {
|
||||
this.selectedIndex = this.options.length - 1;
|
||||
});
|
||||
}
|
||||
// Loop through all defined gradients.
|
||||
Object.keys(settings.gradients || {}).forEach((i) => {
|
||||
// Add element to display the gradient.
|
||||
$('.color-preview').once('color').append(`<div id="gradient-${i}"></div>`);
|
||||
const gradient = $(`.color-preview #gradient-${i}`);
|
||||
// Add height of current gradient to the list (divided by 10).
|
||||
height.push(parseInt(gradient.css('height'), 10) / 10);
|
||||
// Add width of current gradient to the list (divided by 10).
|
||||
width.push(parseInt(gradient.css('width'), 10) / 10);
|
||||
// Add rows (or columns for horizontal gradients).
|
||||
// Each gradient line should have a height (or width for horizontal
|
||||
// gradients) of 10px (because we divided the height/width by 10
|
||||
// above).
|
||||
for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) {
|
||||
gradient.append('<div class="gradient-line"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
// Set up colorScheme selector.
|
||||
form.find('#edit-scheme').on('change', function () {
|
||||
const schemes = settings.color.schemes;
|
||||
const colorScheme = this.options[this.selectedIndex].value;
|
||||
if (colorScheme !== '' && schemes[colorScheme]) {
|
||||
// Get colors of active scheme.
|
||||
colors = schemes[colorScheme];
|
||||
Object.keys(colors || {}).forEach((fieldName) => {
|
||||
callback($(`#edit-palette-${fieldName}`), colors[fieldName], false, true);
|
||||
});
|
||||
preview();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Focuses Farbtastic on a particular field.
|
||||
|
|
|
@ -32,35 +32,16 @@
|
|||
var height = [];
|
||||
var width = [];
|
||||
|
||||
Object.keys(settings.gradients || {}).forEach(function (i) {
|
||||
$('.color-preview').once('color').append('<div id="gradient-' + i + '"></div>');
|
||||
var gradient = $('.color-preview #gradient-' + i);
|
||||
|
||||
height.push(parseInt(gradient.css('height'), 10) / 10);
|
||||
|
||||
width.push(parseInt(gradient.css('width'), 10) / 10);
|
||||
|
||||
for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) {
|
||||
gradient.append('<div class="gradient-line"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
form.find('#edit-scheme').on('change', function () {
|
||||
var schemes = settings.color.schemes;
|
||||
var colorScheme = this.options[this.selectedIndex].value;
|
||||
if (colorScheme !== '' && schemes[colorScheme]) {
|
||||
colors = schemes[colorScheme];
|
||||
Object.keys(colors || {}).forEach(function (fieldName) {
|
||||
callback($('#edit-palette-' + fieldName), colors[fieldName], false, true);
|
||||
});
|
||||
preview();
|
||||
}
|
||||
});
|
||||
|
||||
function preview() {
|
||||
Drupal.color.callback(context, settings, form, farb, height, width);
|
||||
}
|
||||
|
||||
function resetScheme() {
|
||||
form.find('#edit-scheme').each(function () {
|
||||
this.selectedIndex = this.options.length - 1;
|
||||
});
|
||||
}
|
||||
|
||||
function shiftColor(given, ref1, ref2) {
|
||||
var d = void 0;
|
||||
|
||||
|
@ -130,11 +111,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
function resetScheme() {
|
||||
form.find('#edit-scheme').each(function () {
|
||||
this.selectedIndex = this.options.length - 1;
|
||||
});
|
||||
}
|
||||
Object.keys(settings.gradients || {}).forEach(function (i) {
|
||||
$('.color-preview').once('color').append('<div id="gradient-' + i + '"></div>');
|
||||
var gradient = $('.color-preview #gradient-' + i);
|
||||
|
||||
height.push(parseInt(gradient.css('height'), 10) / 10);
|
||||
|
||||
width.push(parseInt(gradient.css('width'), 10) / 10);
|
||||
|
||||
for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) {
|
||||
gradient.append('<div class="gradient-line"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
form.find('#edit-scheme').on('change', function () {
|
||||
var schemes = settings.color.schemes;
|
||||
var colorScheme = this.options[this.selectedIndex].value;
|
||||
if (colorScheme !== '' && schemes[colorScheme]) {
|
||||
colors = schemes[colorScheme];
|
||||
Object.keys(colors || {}).forEach(function (fieldName) {
|
||||
callback($('#edit-palette-' + fieldName), colors[fieldName], false, true);
|
||||
});
|
||||
preview();
|
||||
}
|
||||
});
|
||||
|
||||
function focus(e) {
|
||||
var input = e.target;
|
||||
|
|
|
@ -7,46 +7,6 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
/**
|
||||
* Renders "new" comment indicators wherever necessary.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches "new" comment indicators behavior.
|
||||
*/
|
||||
Drupal.behaviors.commentNewIndicator = {
|
||||
attach(context) {
|
||||
// Collect all "new" comment indicator placeholders (and their
|
||||
// corresponding node IDs) newer than 30 days ago that have not already
|
||||
// been read after their last comment timestamp.
|
||||
const nodeIDs = [];
|
||||
const $placeholders = $(context)
|
||||
.find('[data-comment-timestamp]')
|
||||
.once('history')
|
||||
.filter(function () {
|
||||
const $placeholder = $(this);
|
||||
const commentTimestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
|
||||
const nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the node read timestamps from the server.
|
||||
Drupal.history.fetchTimestamps(nodeIDs, () => {
|
||||
processCommentNewIndicators($placeholders);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes the markup for "new comment" indicators.
|
||||
*
|
||||
|
@ -88,4 +48,44 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders "new" comment indicators wherever necessary.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches "new" comment indicators behavior.
|
||||
*/
|
||||
Drupal.behaviors.commentNewIndicator = {
|
||||
attach(context) {
|
||||
// Collect all "new" comment indicator placeholders (and their
|
||||
// corresponding node IDs) newer than 30 days ago that have not already
|
||||
// been read after their last comment timestamp.
|
||||
const nodeIDs = [];
|
||||
const $placeholders = $(context)
|
||||
.find('[data-comment-timestamp]')
|
||||
.once('history')
|
||||
.filter(function () {
|
||||
const $placeholder = $(this);
|
||||
const commentTimestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
|
||||
const nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the node read timestamps from the server.
|
||||
Drupal.history.fetchTimestamps(nodeIDs, () => {
|
||||
processCommentNewIndicators($placeholders);
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal, window));
|
||||
|
|
|
@ -6,31 +6,6 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
Drupal.behaviors.commentNewIndicator = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $placeholders = $(context).find('[data-comment-timestamp]').once('history').filter(function () {
|
||||
var $placeholder = $(this);
|
||||
var commentTimestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
|
||||
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processCommentNewIndicators($placeholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function processCommentNewIndicators($placeholders) {
|
||||
var isFirstNewComment = true;
|
||||
var newCommentString = Drupal.t('new');
|
||||
|
@ -57,4 +32,29 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
Drupal.behaviors.commentNewIndicator = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $placeholders = $(context).find('[data-comment-timestamp]').once('history').filter(function () {
|
||||
var $placeholder = $(this);
|
||||
var commentTimestamp = parseInt($placeholder.attr('data-comment-timestamp'), 10);
|
||||
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, commentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processCommentNewIndicators($placeholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
})(jQuery, Drupal, window);
|
|
@ -7,51 +7,6 @@
|
|||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Render "X new comments" links wherever necessary.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches new comment links behavior.
|
||||
*/
|
||||
Drupal.behaviors.nodeNewCommentsLink = {
|
||||
attach(context) {
|
||||
// Collect all "X new comments" node link placeholders (and their
|
||||
// corresponding node IDs) newer than 30 days ago that have not already
|
||||
// been read after their last comment timestamp.
|
||||
const nodeIDs = [];
|
||||
const $placeholders = $(context)
|
||||
.find('[data-history-node-last-comment-timestamp]')
|
||||
.once('history')
|
||||
.filter(function () {
|
||||
const $placeholder = $(this);
|
||||
const lastCommentTimestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
|
||||
const nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
// Hide this placeholder link until it is certain we'll need it.
|
||||
hide($placeholder);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove this placeholder link from the DOM because we won't need
|
||||
// it.
|
||||
remove($placeholder);
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform an AJAX request to retrieve node read timestamps.
|
||||
Drupal.history.fetchTimestamps(nodeIDs, () => {
|
||||
processNodeNewCommentLinks($placeholders);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides a "new comment" element.
|
||||
*
|
||||
|
@ -173,4 +128,49 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "X new comments" links wherever necessary.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches new comment links behavior.
|
||||
*/
|
||||
Drupal.behaviors.nodeNewCommentsLink = {
|
||||
attach(context) {
|
||||
// Collect all "X new comments" node link placeholders (and their
|
||||
// corresponding node IDs) newer than 30 days ago that have not already
|
||||
// been read after their last comment timestamp.
|
||||
const nodeIDs = [];
|
||||
const $placeholders = $(context)
|
||||
.find('[data-history-node-last-comment-timestamp]')
|
||||
.once('history')
|
||||
.filter(function () {
|
||||
const $placeholder = $(this);
|
||||
const lastCommentTimestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
|
||||
const nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
// Hide this placeholder link until it is certain we'll need it.
|
||||
hide($placeholder);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove this placeholder link from the DOM because we won't need
|
||||
// it.
|
||||
remove($placeholder);
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform an AJAX request to retrieve node read timestamps.
|
||||
Drupal.history.fetchTimestamps(nodeIDs, () => {
|
||||
processNodeNewCommentLinks($placeholders);
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
|
|
|
@ -6,34 +6,6 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
Drupal.behaviors.nodeNewCommentsLink = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $placeholders = $(context).find('[data-history-node-last-comment-timestamp]').once('history').filter(function () {
|
||||
var $placeholder = $(this);
|
||||
var lastCommentTimestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
|
||||
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
|
||||
hide($placeholder);
|
||||
return true;
|
||||
}
|
||||
|
||||
remove($placeholder);
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processNodeNewCommentLinks($placeholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function hide($placeholder) {
|
||||
return $placeholder.closest('.comment-new-comments').prev().addClass('last').end().hide();
|
||||
}
|
||||
|
@ -90,4 +62,32 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drupal.behaviors.nodeNewCommentsLink = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $placeholders = $(context).find('[data-history-node-last-comment-timestamp]').once('history').filter(function () {
|
||||
var $placeholder = $(this);
|
||||
var lastCommentTimestamp = parseInt($placeholder.attr('data-history-node-last-comment-timestamp'), 10);
|
||||
var nodeID = $placeholder.closest('[data-history-node-id]').attr('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
|
||||
hide($placeholder);
|
||||
return true;
|
||||
}
|
||||
|
||||
remove($placeholder);
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processNodeNewCommentLinks($placeholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings);
|
|
@ -29,6 +29,46 @@
|
|||
storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a contextual link is nested & overlapping, if so: adjusts it.
|
||||
*
|
||||
* This only deals with two levels of nesting; deeper levels are not touched.
|
||||
*
|
||||
* @param {jQuery} $contextual
|
||||
* A contextual links placeholder DOM element, containing the actual
|
||||
* contextual links as rendered by the server.
|
||||
*/
|
||||
function adjustIfNestedAndOverlapping($contextual) {
|
||||
const $contextuals = $contextual
|
||||
// @todo confirm that .closest() is not sufficient
|
||||
.parents('.contextual-region').eq(-1)
|
||||
.find('.contextual');
|
||||
|
||||
// Early-return when there's no nesting.
|
||||
if ($contextuals.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the two contextual links overlap, then we move the second one.
|
||||
const firstTop = $contextuals.eq(0).offset().top;
|
||||
const secondTop = $contextuals.eq(1).offset().top;
|
||||
if (firstTop === secondTop) {
|
||||
const $nestedContextual = $contextuals.eq(1);
|
||||
|
||||
// Retrieve height of nested contextual link.
|
||||
let height = 0;
|
||||
const $trigger = $nestedContextual.find('.trigger');
|
||||
// Elements with the .visually-hidden class have no dimensions, so this
|
||||
// class must be temporarily removed to the calculate the height.
|
||||
$trigger.removeClass('visually-hidden');
|
||||
height = $nestedContextual.height();
|
||||
$trigger.addClass('visually-hidden');
|
||||
|
||||
// Adjust nested contextual link's position.
|
||||
$nestedContextual.css({ top: $nestedContextual.position().top + height });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a contextual link: updates its DOM, sets up model and views.
|
||||
*
|
||||
|
@ -89,46 +129,6 @@
|
|||
adjustIfNestedAndOverlapping($contextual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a contextual link is nested & overlapping, if so: adjusts it.
|
||||
*
|
||||
* This only deals with two levels of nesting; deeper levels are not touched.
|
||||
*
|
||||
* @param {jQuery} $contextual
|
||||
* A contextual links placeholder DOM element, containing the actual
|
||||
* contextual links as rendered by the server.
|
||||
*/
|
||||
function adjustIfNestedAndOverlapping($contextual) {
|
||||
const $contextuals = $contextual
|
||||
// @todo confirm that .closest() is not sufficient
|
||||
.parents('.contextual-region').eq(-1)
|
||||
.find('.contextual');
|
||||
|
||||
// Early-return when there's no nesting.
|
||||
if ($contextuals.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the two contextual links overlap, then we move the second one.
|
||||
const firstTop = $contextuals.eq(0).offset().top;
|
||||
const secondTop = $contextuals.eq(1).offset().top;
|
||||
if (firstTop === secondTop) {
|
||||
const $nestedContextual = $contextuals.eq(1);
|
||||
|
||||
// Retrieve height of nested contextual link.
|
||||
let height = 0;
|
||||
const $trigger = $nestedContextual.find('.trigger');
|
||||
// Elements with the .visually-hidden class have no dimensions, so this
|
||||
// class must be temporarily removed to the calculate the height.
|
||||
$trigger.removeClass('visually-hidden');
|
||||
height = $nestedContextual.height();
|
||||
$trigger.addClass('visually-hidden');
|
||||
|
||||
// Adjust nested contextual link's position.
|
||||
$nestedContextual.css({ top: $nestedContextual.position().top + height });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches outline behavior for regions associated with contextual links.
|
||||
*
|
||||
|
|
|
@ -26,6 +26,29 @@
|
|||
storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
|
||||
}
|
||||
|
||||
function adjustIfNestedAndOverlapping($contextual) {
|
||||
var $contextuals = $contextual.parents('.contextual-region').eq(-1).find('.contextual');
|
||||
|
||||
if ($contextuals.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTop = $contextuals.eq(0).offset().top;
|
||||
var secondTop = $contextuals.eq(1).offset().top;
|
||||
if (firstTop === secondTop) {
|
||||
var $nestedContextual = $contextuals.eq(1);
|
||||
|
||||
var height = 0;
|
||||
var $trigger = $nestedContextual.find('.trigger');
|
||||
|
||||
$trigger.removeClass('visually-hidden');
|
||||
height = $nestedContextual.height();
|
||||
$trigger.addClass('visually-hidden');
|
||||
|
||||
$nestedContextual.css({ top: $nestedContextual.position().top + height });
|
||||
}
|
||||
}
|
||||
|
||||
function initContextual($contextual, html) {
|
||||
var $region = $contextual.closest('.contextual-region');
|
||||
var contextual = Drupal.contextual;
|
||||
|
@ -61,29 +84,6 @@
|
|||
adjustIfNestedAndOverlapping($contextual);
|
||||
}
|
||||
|
||||
function adjustIfNestedAndOverlapping($contextual) {
|
||||
var $contextuals = $contextual.parents('.contextual-region').eq(-1).find('.contextual');
|
||||
|
||||
if ($contextuals.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTop = $contextuals.eq(0).offset().top;
|
||||
var secondTop = $contextuals.eq(1).offset().top;
|
||||
if (firstTop === secondTop) {
|
||||
var $nestedContextual = $contextuals.eq(1);
|
||||
|
||||
var height = 0;
|
||||
var $trigger = $nestedContextual.find('.trigger');
|
||||
|
||||
$trigger.removeClass('visually-hidden');
|
||||
height = $nestedContextual.height();
|
||||
$trigger.addClass('visually-hidden');
|
||||
|
||||
$nestedContextual.css({ top: $nestedContextual.position().top + height });
|
||||
}
|
||||
}
|
||||
|
||||
Drupal.behaviors.contextual = {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
|
|
|
@ -88,6 +88,20 @@
|
|||
* Whether the given feature is allowed by the current filters.
|
||||
*/
|
||||
featureIsAllowedByFilters(feature) {
|
||||
/**
|
||||
* Provided a section of a feature or filter rule, checks if no property
|
||||
* values are defined for all properties: attributes, classes and styles.
|
||||
*
|
||||
* @param {object} section
|
||||
* The section to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if the section has empty properties, false otherwise.
|
||||
*/
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the universe U of possible values that can result from the
|
||||
* feature's rules' requirements.
|
||||
|
@ -182,79 +196,6 @@
|
|||
return universe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a section of a feature or filter rule, checks if no property
|
||||
* values are defined for all properties: attributes, classes and styles.
|
||||
*
|
||||
* @param {object} section
|
||||
* The section to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if the section has empty properties, false otherwise.
|
||||
*/
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValueOnTag on the given tag for every property value
|
||||
* that is listed in the "propertyValues" parameter. Supports the wildcard
|
||||
* tag.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
|
||||
let atLeastOneFound = false;
|
||||
_.each(propertyValues, (propertyValue) => {
|
||||
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValuesOnAllTags for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
let atLeastOneFound = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if a specific property value (potentially containing
|
||||
* wildcards) exists on the given tag. When the "allowing" parameter
|
||||
|
@ -316,6 +257,86 @@
|
|||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValuesOnAllTags for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
let atLeastOneFound = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValueOnTag on the given tag for every property value
|
||||
* that is listed in the "propertyValues" parameter. Supports the wildcard
|
||||
* tag.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to check.
|
||||
* @param {string} tag
|
||||
* The tag to look for.
|
||||
* @param {string} property
|
||||
* The property to check.
|
||||
* @param {Array} propertyValues
|
||||
* Values of the property to check.
|
||||
* @param {bool} allowing
|
||||
* Whether to update the universe or not.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if found, false otherwise.
|
||||
*/
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
|
||||
let atLeastOneFound = false;
|
||||
_.each(propertyValues, (propertyValue) => {
|
||||
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls deleteFromUniverseIfAllowed for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
let atLeastOneDeleted = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
if (deleteFromUniverseIfAllowed(universe, tag)) {
|
||||
atLeastOneDeleted = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tag from the universe if the tag itself and each of its
|
||||
* properties are marked as allowed.
|
||||
|
@ -340,25 +361,6 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls deleteFromUniverseIfAllowed for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* The universe to delete from.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether something was deleted from the universe.
|
||||
*/
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
let atLeastOneDeleted = false;
|
||||
_.each(_.keys(universe), (tag) => {
|
||||
if (deleteFromUniverseIfAllowed(universe, tag)) {
|
||||
atLeastOneDeleted = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any filter rule forbids either a tag or a tag property value
|
||||
* that exists in the universe.
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
$(document).trigger('drupalEditorFeatureModified', feature);
|
||||
},
|
||||
featureIsAllowedByFilters: function featureIsAllowedByFilters(feature) {
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
function generateUniverseFromFeatureRequirements(feature) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
var universe = {};
|
||||
|
@ -51,34 +55,6 @@
|
|||
return universe;
|
||||
}
|
||||
|
||||
function emptyProperties(section) {
|
||||
return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
|
||||
}
|
||||
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
if (tag === '*') {
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
|
||||
var atLeastOneFound = false;
|
||||
_.each(propertyValues, function (propertyValue) {
|
||||
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
var atLeastOneFound = false;
|
||||
_.each(_.keys(universe), function (tag) {
|
||||
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
|
||||
if (!_.has(universe, tag)) {
|
||||
return false;
|
||||
|
@ -114,15 +90,28 @@
|
|||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
function deleteFromUniverseIfAllowed(universe, tag) {
|
||||
function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
|
||||
var atLeastOneFound = false;
|
||||
_.each(_.keys(universe), function (tag) {
|
||||
if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
if (tag === '*') {
|
||||
return deleteAllTagsFromUniverseIfAllowed(universe);
|
||||
return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
|
||||
}
|
||||
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
|
||||
delete universe[tag];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
var atLeastOneFound = false;
|
||||
_.each(propertyValues, function (propertyValue) {
|
||||
if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
|
||||
atLeastOneFound = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
|
@ -135,6 +124,17 @@
|
|||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
function deleteFromUniverseIfAllowed(universe, tag) {
|
||||
if (tag === '*') {
|
||||
return deleteAllTagsFromUniverseIfAllowed(universe);
|
||||
}
|
||||
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
|
||||
delete universe[tag];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function anyForbiddenFilterRuleMatches(universe, filterStatus) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
|
|
|
@ -20,6 +20,46 @@
|
|||
return $(`#${fieldId}`).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter away XSS attack vectors when switching text formats.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} originalFormatID
|
||||
* The text format ID of the original text format.
|
||||
* @param {function} callback
|
||||
* A callback to be called (with no parameters) after the field's value has
|
||||
* been XSS filtered.
|
||||
*/
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
// A text editor that already is XSS-safe needs no additional measures.
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
}
|
||||
// Otherwise, ensure XSS safety: let the server XSS filter this value.
|
||||
else {
|
||||
$.ajax({
|
||||
url: Drupal.url(`editor/filter_xss/${format.format}`),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(xssFilteredValue) {
|
||||
// If the server returns false, then no XSS filtering is needed.
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the text editor on a text area.
|
||||
*
|
||||
|
@ -271,44 +311,4 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter away XSS attack vectors when switching text formats.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} originalFormatID
|
||||
* The text format ID of the original text format.
|
||||
* @param {function} callback
|
||||
* A callback to be called (with no parameters) after the field's value has
|
||||
* been XSS filtered.
|
||||
*/
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
// A text editor that already is XSS-safe needs no additional measures.
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
}
|
||||
// Otherwise, ensure XSS safety: let the server XSS filter this value.
|
||||
else {
|
||||
$.ajax({
|
||||
url: Drupal.url(`editor/filter_xss/${format.format}`),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(xssFilteredValue) {
|
||||
// If the server returns false, then no XSS filtering is needed.
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
|
|
|
@ -12,6 +12,28 @@
|
|||
return $('#' + fieldId).get(0);
|
||||
}
|
||||
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
} else {
|
||||
$.ajax({
|
||||
url: Drupal.url('editor/filter_xss/' + format.format),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(xssFilteredValue) {
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function changeTextEditor(field, newFormatID) {
|
||||
var previousFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
|
||||
|
@ -168,26 +190,4 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
} else {
|
||||
$.ajax({
|
||||
url: Drupal.url('editor/filter_xss/' + format.format),
|
||||
type: 'POST',
|
||||
data: {
|
||||
value: field.value,
|
||||
original_format_id: originalFormatID
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(xssFilteredValue) {
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})(jQuery, Drupal, drupalSettings);
|
|
@ -189,17 +189,17 @@
|
|||
const editorModel = this.model;
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
// Create an AJAX object for the form associated with the field.
|
||||
let formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving({
|
||||
nocssjs: false,
|
||||
other_view_modes: fieldModel.findOtherViewModes(),
|
||||
}, $submit);
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
// Successfully saved.
|
||||
formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
|
||||
cleanUpAjax();
|
||||
|
|
|
@ -122,16 +122,16 @@
|
|||
var editorModel = this.model;
|
||||
var fieldModel = this.fieldModel;
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
var formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving({
|
||||
nocssjs: false,
|
||||
other_view_modes: fieldModel.findOtherViewModes()
|
||||
}, $submit);
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
|
||||
cleanUpAjax();
|
||||
|
||||
|
|
|
@ -60,6 +60,401 @@
|
|||
*/
|
||||
const entityInstancesTracker = {};
|
||||
|
||||
/**
|
||||
* Initialize the Quick Edit app.
|
||||
*
|
||||
* @param {HTMLElement} bodyElement
|
||||
* This document's body element.
|
||||
*/
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
// Instantiate AppModel (application state) and AppView, which is the
|
||||
// controller of the whole in-place editing experience.
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the entity an instance ID.
|
||||
*
|
||||
* @param {HTMLElement} entityElement
|
||||
* A Drupal Entity API entity's DOM element with a data-quickedit-entity-id
|
||||
* attribute.
|
||||
*/
|
||||
function processEntity(entityElement) {
|
||||
const entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
}
|
||||
else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
// Set the calculated entity instance ID for this element.
|
||||
const entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a field; create FieldModel.
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* The field's DOM element.
|
||||
* @param {string} fieldID
|
||||
* The field's ID.
|
||||
* @param {string} entityID
|
||||
* The field's entity's ID.
|
||||
* @param {string} entityInstanceID
|
||||
* The field's entity's instance ID.
|
||||
*/
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
const entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
// The FieldModel stores the state of an in-place editable entity field.
|
||||
const field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
id: `${fieldID}[${entity.get('entityInstanceID')}]`,
|
||||
entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app),
|
||||
});
|
||||
|
||||
// Track all fields on the page.
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads missing in-place editor's attachments (JavaScript and CSS files).
|
||||
*
|
||||
* Missing in-place editors are those whose fields are actively being used on
|
||||
* the page but don't have.
|
||||
*
|
||||
* @param {function} callback
|
||||
* Callback function to be called when the missing in-place editors (if any)
|
||||
* have been inserted into the DOM. i.e. they may still be loading.
|
||||
*/
|
||||
function loadMissingEditors(callback) {
|
||||
const loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
let missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each((fieldModel) => {
|
||||
const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
// Set a stub, to prevent subsequent calls to loadMissingEditors() from
|
||||
// loading the same in-place editor again. Loading an in-place editor
|
||||
// requires talking to a server, to download its JavaScript, then
|
||||
// executing its JavaScript, and only then its Drupal.quickedit.editors
|
||||
// entry will be set.
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// @see https://www.drupal.org/node/2029999.
|
||||
// Create a Drupal.Ajax instance to load the form.
|
||||
const loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors },
|
||||
});
|
||||
// Implement a scoped insert AJAX command: calls the callback after all AJAX
|
||||
// command functions have been executed (hence the deferred calling).
|
||||
const realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
// Trigger the AJAX request, which will should return AJAX commands to
|
||||
// insert any missing attachments.
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set up a "Quick edit" link and corresponding EntityModel.
|
||||
*
|
||||
* @param {object} contextualLink
|
||||
* An object with the following properties:
|
||||
* - String entityID: a Quick Edit entity identifier, e.g. "node/1" or
|
||||
* "block_content/5".
|
||||
* - String entityInstanceID: a Quick Edit entity instance identifier,
|
||||
* e.g. 0, 1 or n (depending on whether it's the first, second, or n+1st
|
||||
* instance of this entity).
|
||||
* - DOM el: element pointing to the contextual links placeholder for this
|
||||
* entity.
|
||||
* - DOM region: element pointing to the contextual region of this entity.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true when a contextual the given contextual link metadata can be
|
||||
* removed from the queue (either because the contextual link has been set
|
||||
* up or because it is certain that in-place editing is not allowed for any
|
||||
* of its fields). Returns false otherwise.
|
||||
*/
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
// Check if the user has permission to edit at least one of them.
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
const fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if the metadata for all given field IDs exists.
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
// Find all fields for this entity instance and collect their field IDs.
|
||||
const fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
});
|
||||
const fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
// No fields found yet.
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// The entity for the given contextual link contains at least one field that
|
||||
// the current user may edit in-place; instantiate EntityModel,
|
||||
// EntityDecorationView and ContextualLinkView.
|
||||
else if (hasFieldWithPermission(fieldIDs)) {
|
||||
const entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: `${contextualLink.entityID}[${contextualLink.entityInstanceID}]`,
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label'),
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
// Create an EntityDecorationView associated with the root DOM node of the
|
||||
// entity.
|
||||
const entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel,
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
// Initialize all queued fields within this entity (creates FieldModels).
|
||||
_.each(fields, (field) => {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
// Initialization should only be called once. Use Underscore's once method
|
||||
// to get a one-time use version of the function.
|
||||
const initContextualLink = _.once(() => {
|
||||
const $links = $(contextualLink.el).find('.contextual-links');
|
||||
const contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model,
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
// Set up ContextualLinkView after loading any missing in-place editors.
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
}
|
||||
// There was not at least one field that the current user may edit in-place,
|
||||
// even though the metadata for all fields within this entity is available.
|
||||
else if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from a field ID.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* A field ID: a string of the format
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the field's metadata; queue or initialize it (if EntityModel exists).
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* A Drupal Field API field's DOM element with a data-quickedit-field-id
|
||||
* attribute.
|
||||
*/
|
||||
function processField(fieldElement) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
const fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
const entityID = extractEntityID(fieldID);
|
||||
// Figure out the instance ID by looking at the ancestor
|
||||
// [data-quickedit-entity-id] element's data-quickedit-entity-instance-id
|
||||
// attribute.
|
||||
const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`;
|
||||
const $entityElement = $(entityElementSelector);
|
||||
|
||||
// If there are no elements returned from `entityElementSelector`
|
||||
// throw an error. Check the browser console for this message.
|
||||
if (!$entityElement.length) {
|
||||
throw new Error(`Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`);
|
||||
}
|
||||
let entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
// In the case of a full entity view page, the entity title is rendered
|
||||
// outside of "the entity DOM node": it's rendered as the page title. So in
|
||||
// this case, we find the lowest common parent element (deepest in the tree)
|
||||
// and consider that the entity element.
|
||||
if (entityElement.length === 0) {
|
||||
const $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
const entityInstanceID = entityElement
|
||||
.get(0)
|
||||
.getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
// Early-return if metadata for this field is missing.
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Early-return if the user is not allowed to in-place edit this field.
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an EntityModel for this field already exists (and hence also a "Quick
|
||||
// edit" contextual link), then initialize it immediately.
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID, entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
}
|
||||
// Otherwise: queue the field. It is now available to be set up when its
|
||||
// corresponding entity becomes in-place editable.
|
||||
else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID, entityID, entityInstanceID });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models and queue items that are contained within a given context.
|
||||
*
|
||||
* Deletes any contained EntityModels (plus their associated FieldModels and
|
||||
* ContextualLinkView) and FieldModels, as well as the corresponding queues.
|
||||
*
|
||||
* After EntityModels, FieldModels must also be deleted, because it is
|
||||
* possible in Drupal for a field DOM element to exist outside of the entity
|
||||
* DOM element, e.g. when viewing the full node, the title of the node is not
|
||||
* rendered within the node (the entity) but as the page title.
|
||||
*
|
||||
* Note: this will not delete an entity that is actively being in-place
|
||||
* edited.
|
||||
*
|
||||
* @param {jQuery} $context
|
||||
* The context within which to delete.
|
||||
*/
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each((index, entityElement) => {
|
||||
// Delete entity model.
|
||||
const entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
const contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
// Remove the EntityDecorationView.
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
// Destroy the EntityModel; this will also destroy its FieldModels.
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
// Filter queue.
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each((index, fieldElement) => {
|
||||
// Delete field models.
|
||||
Drupal.quickedit.collections.fields.chain()
|
||||
.filter(fieldModel => fieldModel.get('el') === fieldElement)
|
||||
.invoke('destroy');
|
||||
|
||||
// Filter queues.
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches metadata for fields whose metadata is missing.
|
||||
*
|
||||
* Fields whose metadata is missing are tracked at fieldsMetadataQueue.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that receives field elements whose metadata will just
|
||||
* have been fetched.
|
||||
*/
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
// Ensure we only request entityIDs for which we don't have metadata yet.
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
// Store the metadata.
|
||||
_.each(results, (fieldMetadata, fieldID) => {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
|
@ -288,399 +683,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from a field ID.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* A field ID: a string of the format
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Quick Edit app.
|
||||
*
|
||||
* @param {HTMLElement} bodyElement
|
||||
* This document's body element.
|
||||
*/
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
// Instantiate AppModel (application state) and AppView, which is the
|
||||
// controller of the whole in-place editing experience.
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the entity an instance ID.
|
||||
*
|
||||
* @param {HTMLElement} entityElement
|
||||
* A Drupal Entity API entity's DOM element with a data-quickedit-entity-id
|
||||
* attribute.
|
||||
*/
|
||||
function processEntity(entityElement) {
|
||||
const entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
}
|
||||
else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
// Set the calculated entity instance ID for this element.
|
||||
const entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the field's metadata; queue or initialize it (if EntityModel exists).
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* A Drupal Field API field's DOM element with a data-quickedit-field-id
|
||||
* attribute.
|
||||
*/
|
||||
function processField(fieldElement) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
const fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
const entityID = extractEntityID(fieldID);
|
||||
// Figure out the instance ID by looking at the ancestor
|
||||
// [data-quickedit-entity-id] element's data-quickedit-entity-instance-id
|
||||
// attribute.
|
||||
const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`;
|
||||
const $entityElement = $(entityElementSelector);
|
||||
|
||||
// If there are no elements returned from `entityElementSelector`
|
||||
// throw an error. Check the browser console for this message.
|
||||
if (!$entityElement.length) {
|
||||
throw new Error(`Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`);
|
||||
}
|
||||
let entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
// In the case of a full entity view page, the entity title is rendered
|
||||
// outside of "the entity DOM node": it's rendered as the page title. So in
|
||||
// this case, we find the lowest common parent element (deepest in the tree)
|
||||
// and consider that the entity element.
|
||||
if (entityElement.length === 0) {
|
||||
const $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
const entityInstanceID = entityElement
|
||||
.get(0)
|
||||
.getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
// Early-return if metadata for this field is missing.
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Early-return if the user is not allowed to in-place edit this field.
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an EntityModel for this field already exists (and hence also a "Quick
|
||||
// edit" contextual link), then initialize it immediately.
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID, entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
}
|
||||
// Otherwise: queue the field. It is now available to be set up when its
|
||||
// corresponding entity becomes in-place editable.
|
||||
else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID, entityID, entityInstanceID });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a field; create FieldModel.
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* The field's DOM element.
|
||||
* @param {string} fieldID
|
||||
* The field's ID.
|
||||
* @param {string} entityID
|
||||
* The field's entity's ID.
|
||||
* @param {string} entityInstanceID
|
||||
* The field's entity's instance ID.
|
||||
*/
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
const entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
// The FieldModel stores the state of an in-place editable entity field.
|
||||
const field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
id: `${fieldID}[${entity.get('entityInstanceID')}]`,
|
||||
entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app),
|
||||
});
|
||||
|
||||
// Track all fields on the page.
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches metadata for fields whose metadata is missing.
|
||||
*
|
||||
* Fields whose metadata is missing are tracked at fieldsMetadataQueue.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that receives field elements whose metadata will just
|
||||
* have been fetched.
|
||||
*/
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
// Ensure we only request entityIDs for which we don't have metadata yet.
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
// Store the metadata.
|
||||
_.each(results, (fieldMetadata, fieldID) => {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads missing in-place editor's attachments (JavaScript and CSS files).
|
||||
*
|
||||
* Missing in-place editors are those whose fields are actively being used on
|
||||
* the page but don't have.
|
||||
*
|
||||
* @param {function} callback
|
||||
* Callback function to be called when the missing in-place editors (if any)
|
||||
* have been inserted into the DOM. i.e. they may still be loading.
|
||||
*/
|
||||
function loadMissingEditors(callback) {
|
||||
const loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
let missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each((fieldModel) => {
|
||||
const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
// Set a stub, to prevent subsequent calls to loadMissingEditors() from
|
||||
// loading the same in-place editor again. Loading an in-place editor
|
||||
// requires talking to a server, to download its JavaScript, then
|
||||
// executing its JavaScript, and only then its Drupal.quickedit.editors
|
||||
// entry will be set.
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// @see https://www.drupal.org/node/2029999.
|
||||
// Create a Drupal.Ajax instance to load the form.
|
||||
const loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors },
|
||||
});
|
||||
// Implement a scoped insert AJAX command: calls the callback after all AJAX
|
||||
// command functions have been executed (hence the deferred calling).
|
||||
const realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
// Trigger the AJAX request, which will should return AJAX commands to
|
||||
// insert any missing attachments.
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set up a "Quick edit" link and corresponding EntityModel.
|
||||
*
|
||||
* @param {object} contextualLink
|
||||
* An object with the following properties:
|
||||
* - String entityID: a Quick Edit entity identifier, e.g. "node/1" or
|
||||
* "block_content/5".
|
||||
* - String entityInstanceID: a Quick Edit entity instance identifier,
|
||||
* e.g. 0, 1 or n (depending on whether it's the first, second, or n+1st
|
||||
* instance of this entity).
|
||||
* - DOM el: element pointing to the contextual links placeholder for this
|
||||
* entity.
|
||||
* - DOM region: element pointing to the contextual region of this entity.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true when a contextual the given contextual link metadata can be
|
||||
* removed from the queue (either because the contextual link has been set
|
||||
* up or because it is certain that in-place editing is not allowed for any
|
||||
* of its fields). Returns false otherwise.
|
||||
*/
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
// Check if the user has permission to edit at least one of them.
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
const fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if the metadata for all given field IDs exists.
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
// Find all fields for this entity instance and collect their field IDs.
|
||||
const fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
});
|
||||
const fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
// No fields found yet.
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// The entity for the given contextual link contains at least one field that
|
||||
// the current user may edit in-place; instantiate EntityModel,
|
||||
// EntityDecorationView and ContextualLinkView.
|
||||
else if (hasFieldWithPermission(fieldIDs)) {
|
||||
const entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: `${contextualLink.entityID}[${contextualLink.entityInstanceID}]`,
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label'),
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
// Create an EntityDecorationView associated with the root DOM node of the
|
||||
// entity.
|
||||
const entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel,
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
// Initialize all queued fields within this entity (creates FieldModels).
|
||||
_.each(fields, (field) => {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
// Initialization should only be called once. Use Underscore's once method
|
||||
// to get a one-time use version of the function.
|
||||
const initContextualLink = _.once(() => {
|
||||
const $links = $(contextualLink.el).find('.contextual-links');
|
||||
const contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model,
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
// Set up ContextualLinkView after loading any missing in-place editors.
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
}
|
||||
// There was not at least one field that the current user may edit in-place,
|
||||
// even though the metadata for all fields within this entity is available.
|
||||
else if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models and queue items that are contained within a given context.
|
||||
*
|
||||
* Deletes any contained EntityModels (plus their associated FieldModels and
|
||||
* ContextualLinkView) and FieldModels, as well as the corresponding queues.
|
||||
*
|
||||
* After EntityModels, FieldModels must also be deleted, because it is
|
||||
* possible in Drupal for a field DOM element to exist outside of the entity
|
||||
* DOM element, e.g. when viewing the full node, the title of the node is not
|
||||
* rendered within the node (the entity) but as the page title.
|
||||
*
|
||||
* Note: this will not delete an entity that is actively being in-place
|
||||
* edited.
|
||||
*
|
||||
* @param {jQuery} $context
|
||||
* The context within which to delete.
|
||||
*/
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each((index, entityElement) => {
|
||||
// Delete entity model.
|
||||
const entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
const contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
// Remove the EntityDecorationView.
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
// Destroy the EntityModel; this will also destroy its FieldModels.
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
// Filter queue.
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each((index, fieldElement) => {
|
||||
// Delete field models.
|
||||
Drupal.quickedit.collections.fields.chain()
|
||||
.filter(fieldModel => fieldModel.get('el') === fieldElement)
|
||||
.invoke('destroy');
|
||||
|
||||
// Filter queues.
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
}(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage));
|
||||
|
|
|
@ -20,6 +20,253 @@
|
|||
|
||||
var entityInstancesTracker = {};
|
||||
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields
|
||||
});
|
||||
}
|
||||
|
||||
function processEntity(entityElement) {
|
||||
var entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
} else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
var entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
var entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
var field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
id: fieldID + '[' + entity.get('entityInstanceID') + ']',
|
||||
entity: entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app)
|
||||
});
|
||||
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
function loadMissingEditors(callback) {
|
||||
var loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
var missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each(function (fieldModel) {
|
||||
var metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors }
|
||||
});
|
||||
|
||||
var realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (var i = 0; i < fieldIDs.length; i++) {
|
||||
var fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
var fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID
|
||||
});
|
||||
var fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
} else if (hasFieldWithPermission(fieldIDs)) {
|
||||
var entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
|
||||
var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
_.each(fields, function (field) {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
var initContextualLink = _.once(function () {
|
||||
var $links = $(contextualLink.el).find('.contextual-links');
|
||||
var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
} else if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
function processField(fieldElement) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
var fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
var entityID = extractEntityID(fieldID);
|
||||
|
||||
var entityElementSelector = '[data-quickedit-entity-id="' + entityID + '"]';
|
||||
var $entityElement = $(entityElementSelector);
|
||||
|
||||
if (!$entityElement.length) {
|
||||
throw new Error('Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="' + fieldID + '"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="' + entityID + '"]. This is typically caused by the theme\'s template for this entity type forgetting to print the attributes.');
|
||||
}
|
||||
var entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
if (entityElement.length === 0) {
|
||||
var $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
var entityInstanceID = entityElement.get(0).getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
} else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID: fieldID, entityID: entityID, entityInstanceID: entityInstanceID });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each(function (index, entityElement) {
|
||||
var entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
var contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each(function (index, fieldElement) {
|
||||
Drupal.quickedit.collections.fields.chain().filter(function (fieldModel) {
|
||||
return fieldModel.get('el') === fieldElement;
|
||||
}).invoke('destroy');
|
||||
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(results) {
|
||||
_.each(results, function (fieldMetadata, fieldID) {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drupal.behaviors.quickedit = {
|
||||
attach: function attach(context) {
|
||||
$('body').once('quickedit-init').each(initQuickEdit);
|
||||
|
@ -124,251 +371,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields
|
||||
});
|
||||
}
|
||||
|
||||
function processEntity(entityElement) {
|
||||
var entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
} else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
var entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
function processField(fieldElement) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
var fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
var entityID = extractEntityID(fieldID);
|
||||
|
||||
var entityElementSelector = '[data-quickedit-entity-id="' + entityID + '"]';
|
||||
var $entityElement = $(entityElementSelector);
|
||||
|
||||
if (!$entityElement.length) {
|
||||
throw new Error('Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="' + fieldID + '"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="' + entityID + '"]. This is typically caused by the theme\'s template for this entity type forgetting to print the attributes.');
|
||||
}
|
||||
var entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
if (entityElement.length === 0) {
|
||||
var $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
var entityInstanceID = entityElement.get(0).getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
} else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID: fieldID, entityID: entityID, entityInstanceID: entityInstanceID });
|
||||
}
|
||||
}
|
||||
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
var entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
var field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
id: fieldID + '[' + entity.get('entityInstanceID') + ']',
|
||||
entity: entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app)
|
||||
});
|
||||
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(results) {
|
||||
_.each(results, function (fieldMetadata, fieldID) {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadMissingEditors(callback) {
|
||||
var loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
var missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each(function (fieldModel) {
|
||||
var metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors }
|
||||
});
|
||||
|
||||
var realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (var i = 0; i < fieldIDs.length; i++) {
|
||||
var fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
var fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID
|
||||
});
|
||||
var fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
} else if (hasFieldWithPermission(fieldIDs)) {
|
||||
var entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
|
||||
var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
_.each(fields, function (field) {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
var initContextualLink = _.once(function () {
|
||||
var $links = $(contextualLink.el).find('.contextual-links');
|
||||
var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
} else if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each(function (index, entityElement) {
|
||||
var entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
var contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each(function (index, fieldElement) {
|
||||
Drupal.quickedit.collections.fields.chain().filter(function (fieldModel) {
|
||||
return fieldModel.get('el') === fieldElement;
|
||||
}).invoke('destroy');
|
||||
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
})(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage);
|
|
@ -20,6 +20,30 @@
|
|||
handleClose: Drupal.t('Collapse'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the open/close state of a list is a menu.
|
||||
*
|
||||
* @param {jQuery} $item
|
||||
* The li item to be toggled.
|
||||
*
|
||||
* @param {Boolean} switcher
|
||||
* A flag that forces toggleClass to add or a remove a class, rather than
|
||||
* simply toggling its presence.
|
||||
*/
|
||||
function toggleList($item, switcher) {
|
||||
const $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
|
||||
switcher = (typeof switcher !== 'undefined') ? switcher : !$item.hasClass('open');
|
||||
// Toggle the item open state.
|
||||
$item.toggleClass('open', switcher);
|
||||
// Twist the toggle.
|
||||
$toggle.toggleClass('open', switcher);
|
||||
// Adjust the toggle text.
|
||||
$toggle
|
||||
.find('.action')
|
||||
// Expand Structure, Collapse Structure.
|
||||
.text((switcher) ? ui.handleClose : ui.handleOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicks from the disclosure button on an item with sub-items.
|
||||
*
|
||||
|
@ -56,30 +80,6 @@
|
|||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the open/close state of a list is a menu.
|
||||
*
|
||||
* @param {jQuery} $item
|
||||
* The li item to be toggled.
|
||||
*
|
||||
* @param {Boolean} switcher
|
||||
* A flag that forces toggleClass to add or a remove a class, rather than
|
||||
* simply toggling its presence.
|
||||
*/
|
||||
function toggleList($item, switcher) {
|
||||
const $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
|
||||
switcher = (typeof switcher !== 'undefined') ? switcher : !$item.hasClass('open');
|
||||
// Toggle the item open state.
|
||||
$item.toggleClass('open', switcher);
|
||||
// Twist the toggle.
|
||||
$toggle.toggleClass('open', switcher);
|
||||
// Adjust the toggle text.
|
||||
$toggle
|
||||
.find('.action')
|
||||
// Expand Structure, Collapse Structure.
|
||||
.text((switcher) ? ui.handleClose : ui.handleOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add markup to the menu elements.
|
||||
*
|
||||
|
|
|
@ -14,6 +14,17 @@
|
|||
handleClose: Drupal.t('Collapse')
|
||||
};
|
||||
|
||||
function toggleList($item, switcher) {
|
||||
var $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
|
||||
switcher = typeof switcher !== 'undefined' ? switcher : !$item.hasClass('open');
|
||||
|
||||
$item.toggleClass('open', switcher);
|
||||
|
||||
$toggle.toggleClass('open', switcher);
|
||||
|
||||
$toggle.find('.action').text(switcher ? ui.handleClose : ui.handleOpen);
|
||||
}
|
||||
|
||||
function toggleClickHandler(event) {
|
||||
var $toggle = $(event.target);
|
||||
var $item = $toggle.closest('li');
|
||||
|
@ -32,17 +43,6 @@
|
|||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function toggleList($item, switcher) {
|
||||
var $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
|
||||
switcher = typeof switcher !== 'undefined' ? switcher : !$item.hasClass('open');
|
||||
|
||||
$item.toggleClass('open', switcher);
|
||||
|
||||
$toggle.toggleClass('open', switcher);
|
||||
|
||||
$toggle.find('.action').text(switcher ? ui.handleClose : ui.handleOpen);
|
||||
}
|
||||
|
||||
function initItems($menu) {
|
||||
var options = {
|
||||
class: 'toolbar-icon toolbar-handle',
|
||||
|
|
|
@ -4,6 +4,59 @@
|
|||
* May only be loaded for authenticated users, with the History module enabled.
|
||||
*/
|
||||
(function ($, Drupal, window) {
|
||||
function processNodeNewIndicators($placeholders) {
|
||||
const newNodeString = Drupal.t('new');
|
||||
const updatedNodeString = Drupal.t('updated');
|
||||
|
||||
$placeholders.each((index, placeholder) => {
|
||||
const timestamp = parseInt(placeholder.getAttribute('data-history-node-timestamp'), 10);
|
||||
const nodeID = placeholder.getAttribute('data-history-node-id');
|
||||
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
|
||||
|
||||
if (timestamp > lastViewTimestamp) {
|
||||
const message = (lastViewTimestamp === 0) ? newNodeString : updatedNodeString;
|
||||
$(placeholder).append(`<span class="marker">${message}</span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processNewRepliesIndicators($placeholders) {
|
||||
// Figure out which placeholders need the "x new" replies links.
|
||||
const placeholdersToUpdate = {};
|
||||
$placeholders.each((index, placeholder) => {
|
||||
const timestamp = parseInt(placeholder.getAttribute('data-history-node-last-comment-timestamp'), 10);
|
||||
const nodeID = placeholder.previousSibling.previousSibling.getAttribute('data-history-node-id');
|
||||
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
|
||||
|
||||
// Queue this placeholder's "X new" replies link to be downloaded from the
|
||||
// server.
|
||||
if (timestamp > lastViewTimestamp) {
|
||||
placeholdersToUpdate[nodeID] = placeholder;
|
||||
}
|
||||
});
|
||||
|
||||
// Perform an AJAX request to retrieve node view timestamps.
|
||||
const nodeIDs = Object.keys(placeholdersToUpdate);
|
||||
if (nodeIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: Drupal.url('comments/render_new_comments_node_links'),
|
||||
type: 'POST',
|
||||
data: { 'node_ids[]': nodeIDs },
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
Object.keys(results || {}).forEach((nodeID) => {
|
||||
if (placeholdersToUpdate.hasOwnProperty(nodeID)) {
|
||||
const url = results[nodeID].first_new_comment_link;
|
||||
const text = Drupal.formatPlural(results[nodeID].new_comment_count, '1 new', '@count new');
|
||||
$(placeholdersToUpdate[nodeID]).append(`<br /><a href="${url}">${text}</a>`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "new" and "updated" node indicators, as well as "X new" replies links.
|
||||
*/
|
||||
|
@ -60,57 +113,4 @@
|
|||
});
|
||||
},
|
||||
};
|
||||
|
||||
function processNodeNewIndicators($placeholders) {
|
||||
const newNodeString = Drupal.t('new');
|
||||
const updatedNodeString = Drupal.t('updated');
|
||||
|
||||
$placeholders.each((index, placeholder) => {
|
||||
const timestamp = parseInt(placeholder.getAttribute('data-history-node-timestamp'), 10);
|
||||
const nodeID = placeholder.getAttribute('data-history-node-id');
|
||||
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
|
||||
|
||||
if (timestamp > lastViewTimestamp) {
|
||||
const message = (lastViewTimestamp === 0) ? newNodeString : updatedNodeString;
|
||||
$(placeholder).append(`<span class="marker">${message}</span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processNewRepliesIndicators($placeholders) {
|
||||
// Figure out which placeholders need the "x new" replies links.
|
||||
const placeholdersToUpdate = {};
|
||||
$placeholders.each((index, placeholder) => {
|
||||
const timestamp = parseInt(placeholder.getAttribute('data-history-node-last-comment-timestamp'), 10);
|
||||
const nodeID = placeholder.previousSibling.previousSibling.getAttribute('data-history-node-id');
|
||||
const lastViewTimestamp = Drupal.history.getLastRead(nodeID);
|
||||
|
||||
// Queue this placeholder's "X new" replies link to be downloaded from the
|
||||
// server.
|
||||
if (timestamp > lastViewTimestamp) {
|
||||
placeholdersToUpdate[nodeID] = placeholder;
|
||||
}
|
||||
});
|
||||
|
||||
// Perform an AJAX request to retrieve node view timestamps.
|
||||
const nodeIDs = Object.keys(placeholdersToUpdate);
|
||||
if (nodeIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: Drupal.url('comments/render_new_comments_node_links'),
|
||||
type: 'POST',
|
||||
data: { 'node_ids[]': nodeIDs },
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
Object.keys(results || {}).forEach((nodeID) => {
|
||||
if (placeholdersToUpdate.hasOwnProperty(nodeID)) {
|
||||
const url = results[nodeID].first_new_comment_link;
|
||||
const text = Drupal.formatPlural(results[nodeID].new_comment_count, '1 new', '@count new');
|
||||
$(placeholdersToUpdate[nodeID]).append(`<br /><a href="${url}">${text}</a>`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}(jQuery, Drupal, window));
|
||||
|
|
|
@ -6,49 +6,6 @@
|
|||
**/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
Drupal.behaviors.trackerHistory = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $nodeNewPlaceholders = $(context).find('[data-history-node-timestamp]').once('history').filter(function () {
|
||||
var nodeTimestamp = parseInt(this.getAttribute('data-history-node-timestamp'), 10);
|
||||
var nodeID = this.getAttribute('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, nodeTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
var $newRepliesPlaceholders = $(context).find('[data-history-node-last-comment-timestamp]').once('history').filter(function () {
|
||||
var lastCommentTimestamp = parseInt(this.getAttribute('data-history-node-last-comment-timestamp'), 10);
|
||||
var nodeTimestamp = parseInt(this.previousSibling.previousSibling.getAttribute('data-history-node-timestamp'), 10);
|
||||
|
||||
if (lastCommentTimestamp === nodeTimestamp) {
|
||||
return false;
|
||||
}
|
||||
var nodeID = this.previousSibling.previousSibling.getAttribute('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
if (nodeIDs.indexOf(nodeID) === -1) {
|
||||
nodeIDs.push(nodeID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($nodeNewPlaceholders.length === 0 && $newRepliesPlaceholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processNodeNewIndicators($nodeNewPlaceholders);
|
||||
processNewRepliesIndicators($newRepliesPlaceholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function processNodeNewIndicators($placeholders) {
|
||||
var newNodeString = Drupal.t('new');
|
||||
var updatedNodeString = Drupal.t('updated');
|
||||
|
@ -97,4 +54,47 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
Drupal.behaviors.trackerHistory = {
|
||||
attach: function attach(context) {
|
||||
var nodeIDs = [];
|
||||
var $nodeNewPlaceholders = $(context).find('[data-history-node-timestamp]').once('history').filter(function () {
|
||||
var nodeTimestamp = parseInt(this.getAttribute('data-history-node-timestamp'), 10);
|
||||
var nodeID = this.getAttribute('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, nodeTimestamp)) {
|
||||
nodeIDs.push(nodeID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
var $newRepliesPlaceholders = $(context).find('[data-history-node-last-comment-timestamp]').once('history').filter(function () {
|
||||
var lastCommentTimestamp = parseInt(this.getAttribute('data-history-node-last-comment-timestamp'), 10);
|
||||
var nodeTimestamp = parseInt(this.previousSibling.previousSibling.getAttribute('data-history-node-timestamp'), 10);
|
||||
|
||||
if (lastCommentTimestamp === nodeTimestamp) {
|
||||
return false;
|
||||
}
|
||||
var nodeID = this.previousSibling.previousSibling.getAttribute('data-history-node-id');
|
||||
if (Drupal.history.needsServerCheck(nodeID, lastCommentTimestamp)) {
|
||||
if (nodeIDs.indexOf(nodeID) === -1) {
|
||||
nodeIDs.push(nodeID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($nodeNewPlaceholders.length === 0 && $newRepliesPlaceholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drupal.history.fetchTimestamps(nodeIDs, function () {
|
||||
processNodeNewIndicators($nodeNewPlaceholders);
|
||||
processNewRepliesIndicators($newRepliesPlaceholders);
|
||||
});
|
||||
}
|
||||
};
|
||||
})(jQuery, Drupal, window);
|
|
@ -20,6 +20,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const $summaries = $details.find('> summary');
|
||||
|
||||
function detailsToggle(matches) {
|
||||
if (matches) {
|
||||
$details.attr('open', true);
|
||||
|
@ -43,7 +45,6 @@
|
|||
detailsToggle(event.matches);
|
||||
}
|
||||
|
||||
const $summaries = $details.find('> summary');
|
||||
const mql = window.matchMedia('(min-width:48em)');
|
||||
mql.addListener(handleDetailsMQ);
|
||||
detailsToggle(mql.matches);
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
var $summaries = $details.find('> summary');
|
||||
|
||||
function detailsToggle(matches) {
|
||||
if (matches) {
|
||||
$details.attr('open', true);
|
||||
|
@ -31,7 +33,6 @@
|
|||
detailsToggle(event.matches);
|
||||
}
|
||||
|
||||
var $summaries = $details.find('> summary');
|
||||
var mql = window.matchMedia('(min-width:48em)');
|
||||
mql.addListener(handleDetailsMQ);
|
||||
detailsToggle(mql.matches);
|
||||
|
|
Loading…
Reference in New Issue