Issue #2946603 by drpal, alexpott, dawehner: JS codestyle: no-use-before-define

8.6.x
Alex Pott 2018-03-05 09:14:08 +00:00
parent 8b5cbee4fb
commit 7d2f3a3b76
50 changed files with 2055 additions and 2052 deletions

View File

@ -1,7 +1,6 @@
{
"extends": "./.eslintrc.json",
"rules": {
"no-use-before-define": "off",
"no-shadow": "off",
"no-new": "off",
"no-continue": "off",

View File

@ -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]);

View File

@ -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 {

View File

@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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.
*

View File

@ -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,

View File

@ -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.
*/

View File

@ -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: []
});

View File

@ -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));

View File

@ -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);

View File

@ -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.
*/

View File

@ -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) {

View File

@ -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.
*/

View File

@ -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: []
});

View File

@ -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.
*/

View File

@ -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: []
});

View File

@ -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

View File

@ -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 () {

View File

@ -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();

View File

@ -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,

View File

@ -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
};

View File

@ -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));

View File

@ -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);

View File

@ -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.)

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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.
*

View File

@ -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);

View File

@ -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.

View File

@ -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'];

View File

@ -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));

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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));

View File

@ -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);

View File

@ -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.
*

View File

@ -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',

View File

@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -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);