Skip to content
Snippets Groups Projects
Commit 145981cb authored by Ivan Berdinsky's avatar Ivan Berdinsky
Browse files

Merge branch '3412125-convert-jquery-to' into '1.x'

Convert jQuery to vanilla Javascript - #3412125

See merge request !160
parents fa90f1e9 c6e1b655
No related branches found
No related tags found
No related merge requests found
Pipeline #117305 passed with warnings
......@@ -19,3 +19,13 @@
opacity: 0.675;
background: #fcfcfa;
}
.navigation-block--animated {
transition: opacity 600ms;
}
.navigation-block--hide {
opacity: 0;
}
.navigation-block--show {
opacity: 1;
}
/**
* @file
* Common admin filter by text behaviors.
*/
const FILTER_EVENT = 'navigaion-block-filter-event';
function hideElement(element) {
element.classList.add('hidden');
element.setAttribute('hidden', true);
element.style.display = 'none';
}
function showElement(element) {
element.classList.remove('hidden');
element.removeAttribute('hidden');
element.style.display = 'revert';
}
function searchMethod(target, query) {
return target?.textContent.toLowerCase().includes(query);
}
function foundInItem(query, item) {
if (item?.searchTargets) {
return Array.from(item.searchTargets).some((target) =>
searchMethod(target, query),
);
}
return searchMethod(item, query);
}
((Drupal, debounce, once) => {
/**
* Filters the table by a text input search string.
*
* The text input will have the selector.
* `.table-filter-text`.
*
* Selector for search table.
* `.table-filter-text[data-table]`
*
* Selectors for search items.
* `.table-filter-text[data-items]`
*
* Selectors for search targets.
* `.table-filter-text[data-targets]`
*
* Singular.
* `.table-filter-text[data-singular]`
*
* Plural.
* `.table-filter-text[data-plural]`
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the text filtering.
*/
Drupal.behaviors.navigationBlockFilterByText = {
attach(context) {
once('drupal-filter-text', '.table-filter-text', context).forEach(
(input) => {
const { table, items, singular, plural } = input.dataset;
const ALL_PHRASE = `All available ${plural || 'items'} are listed.`;
const SINGULAR_PHRASE = `1 ${
singular || 'item'
} is available in the modified list.`;
const PLURAL_PHRASE = `@count ${
plural || 'items'
} are available in the modified list.`;
const makeAnnounce = (matches) => {
Drupal.announce(
Drupal.formatPlural(matches, SINGULAR_PHRASE, PLURAL_PHRASE),
);
};
// Table can be in another context so we have to search in document.
const tableElement = document.querySelector(table);
const initTable = (tableElement) => {
const filterItems = tableElement.querySelectorAll(items);
const labels = tableElement.querySelectorAll('[data-filter-label]');
labels.forEach((label) => {
label.labelledItems = tableElement.querySelectorAll(
`[data-filter-labelledby="${label.dataset.filterLabel}"]`,
);
});
const checkLabels = (reset = false) => {
labels.forEach((label) => {
if (
Array.from(label.labelledItems).some(
(element) => !element.hasAttribute('hidden'),
)
) {
if (label.nodeName === 'DETAILS') {
if (!reset && !label.hasAttribute('open')) {
label.setAttribute('open', true);
label.setAttribute('opened-by-filter', true);
} else if (
reset &&
label.hasAttribute('opened-by-filter')
) {
label.removeAttribute('open');
label.removeAttribute('opened-by-filter');
}
}
showElement(label);
} else {
hideElement(label);
}
});
};
const filterTableList = (e) => {
const query = e.target.value.toLowerCase();
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
let matches = 0;
filterItems.forEach((item) => {
if (!foundInItem(query, item)) {
hideElement(item);
} else {
showElement(item);
matches += 1;
}
});
makeAnnounce(matches);
checkLabels();
} else {
Drupal.announce(ALL_PHRASE);
filterItems.forEach((item) => {
showElement(item);
});
checkLabels(true);
}
};
tableElement.addEventListener(FILTER_EVENT, (e) =>
filterTableList(e.detail.event),
);
};
initTable(tableElement);
input.addEventListener(
'input',
Drupal.debounce((event) => {
tableElement.dispatchEvent(
new CustomEvent(FILTER_EVENT, {
detail: {
event,
},
}),
);
}, 200),
);
input.addEventListener('keydown', (e) => {
if (e.which === 13) {
e.preventDefault();
e.stopPropagation();
}
});
},
);
},
};
})(Drupal, Drupal.debounce, once);
......@@ -3,77 +3,7 @@
* Navigation block admin behaviors.
*/
(function ($, Drupal, debounce, once) {
/**
* Filters the navigation block list by a text input search string.
*
* The text input will have the selector
* `input.navigation-block-filter-text`.
*
* The target element to do searching in will be in the selector
* `input.navigation-block-filter-text[data-element]`
*
* The text source where the text should be found will have the selector
* `.navigation-block-filter-text-source`
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the navigation block filtering.
*/
Drupal.behaviors.navigationBlockFilterByText = {
attach(context, settings) {
const $input = $(once('navigation-block-filter-text', 'input.navigation-block-filter-text'));
const $table = $($input.attr('data-element'));
let $filterRows;
/**
* Filters the navigation block list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
function filterNavigationBlockList(e) {
const query = e.target.value.toLowerCase();
/**
* Shows or hides the navigation block entry based on the query.
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} label
* The label of the navigation block.
*/
function toggleNavigationBlockEntry(index, label) {
const $row = $(label).parent().parent();
const textMatch = label.textContent.toLowerCase().includes(query);
$row.toggle(textMatch);
}
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
$filterRows.each(toggleNavigationBlockEntry);
Drupal.announce(
Drupal.formatPlural(
$table.find('tr:visible').length - 1,
'1 navigation block is available in the modified list.',
'@count navigation blocks are available in the modified list.',
),
);
} else {
$filterRows.each(function (index) {
$(this).parent().parent().show();
});
}
}
if ($table.length) {
$filterRows = $table.find('div.navigation-block-filter-text-source');
$input.on('keyup', debounce(filterNavigationBlockList, 200));
}
},
};
((Drupal, once) => {
/**
* Highlights a navigation block.
*
......@@ -89,26 +19,15 @@
attach(context, settings) {
// Ensure that the navigation block we are attempting to scroll to
// actually exists.
if (settings.navigationBlockPlacement && $('.js-navigation-block-placed').length) {
if (settings.navigationBlockPlacement) {
once(
'navigation-block-highlight',
'[data-drupal-selector="edit-navigation-blocks"]',
'.js-navigation-block-placed',
context,
).forEach((container) => {
const $container = $(container);
// Just scrolling the document.body will not work in Firefox. The html
// element is needed as well.
$('html, body').animate(
{
scrollTop:
$('.js-navigation-block-placed').offset().top -
$container.offset().top +
$container.scrollTop(),
},
500,
);
container.scrollIntoView({ behavior: 'smooth' });
});
}
},
};
})(jQuery, Drupal, Drupal.debounce, once);
})(Drupal, once);
......@@ -3,7 +3,7 @@
* Navigation block behaviors.
*/
(function ($, window, Drupal, once) {
((Drupal, once) => {
/**
* Provide the summary info for the navigation block settings vertical tabs.
*
......@@ -14,53 +14,82 @@
*/
Drupal.behaviors.navigationBlockSettingsSummary = {
attach() {
// The drupalSetSummary method required for this behavior is not available
// on the navigation block administration page, so we need to make sure
// this behavior is processed only if drupalSetSummary is defined.
if (typeof $.fn.drupalSetSummary === 'undefined') {
return;
/**
* Sets the summary for all matched elements.
*
* @param {array} elements
* The DOM nodes to operate on.
* @param {function} callback
* Either a function that will be called each time the summary is
* retrieved or a string (which is returned each time).
*
* @return {void}
*
* @fires event:summaryUpdated
*
* @listens event:formUpdated
*/
function drupalSetSummary(elements, callback) {
// To facilitate things, the callback should always be a function.
// If it's not, we wrap it into an anonymous function which just
// returns the value.
if (typeof callback !== 'function') {
const val = callback;
callback = function summaryCallback() {
return val;
};
}
const summaryUpdatedEvent = new CustomEvent('summaryUpdated');
elements.forEach((element) => {
element.addEventListener('formUpdated.summary', callback, {
passive: true,
});
element.dispatchEvent(summaryUpdatedEvent);
});
}
/**
* Create a summary for checkboxes in the provided context.
*
* @param {HTMLDocument|HTMLElement} context
* A context where one would find checkboxes to summarize.
*
* @return {string}
* A string with the summary.
*/
function checkboxesSummary(context) {
const values = [];
const $checkboxes = $(context).find(
'input[type="checkbox"]:checked + label',
function checkboxesSummary() {
const context = this.querySelectorAll ? this : document;
const checkboxes = Array.from(
context.querySelectorAll('input[type="checkbox"]:checked + label'),
);
const il = $checkboxes.length;
for (let i = 0; i < il; i++) {
values.push($($checkboxes[i]).html());
}
const values = checkboxes ? checkboxes.map((i) => i.innerHTML) : [];
if (!values.length) {
values.push(Drupal.t('Not restricted'));
}
return values.join(', ');
}
$(
'[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-entity-bundlenode"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"], [data-drupal-selector="edit-visibility-response-status"]',
).drupalSetSummary(checkboxesSummary);
drupalSetSummary(
document.querySelectorAll(
'[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-entity-bundlenode"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"], [data-drupal-selector="edit-visibility-response-status"]',
),
checkboxesSummary,
);
$(
'[data-drupal-selector="edit-visibility-request-path"]',
).drupalSetSummary((context) => {
const $pages = $(context).find(
'textarea[name="visibility[request_path][pages]"]',
);
if (!$pages.length || !$pages[0].value) {
return Drupal.t('Not restricted');
}
drupalSetSummary(
document.querySelectorAll(
'[data-drupal-selector="edit-visibility-request-path"]',
),
function requestCallback() {
const context = this.querySelectorAll ? this : document;
const pages = context.querySelectorAll(
'textarea[name="visibility[request_path][pages]"]',
);
if (!pages.length || !pages[0].value) {
return Drupal.t('Not restricted');
}
return Drupal.t('Restricted to certain pages');
});
return Drupal.t('Restricted to certain pages');
},
);
},
};
......@@ -77,7 +106,7 @@
* block administration.
*/
Drupal.behaviors.navigationBlockDrag = {
attach(context, settings) {
attach(context) {
// tableDrag is required, and we should be on the navigation block admin
// page.
if (
......@@ -90,102 +119,124 @@
/**
* Function to check empty regions and toggle classes based on this.
*
* @param {jQuery} table
* The jQuery object representing the table to inspect.
* @param {Element} table
* The DOM object representing the table to inspect.
* @param {Drupal.tableDrag.row} rowObject
* Drupal table drag row dropped.
*/
function checkEmptyRegions(table, rowObject) {
table.find('tr.region-message').each(function () {
const $this = $(this);
// If the dragged row is in this region, but above the message row,
// swap it down one space.
if ($this.prev('tr').get(0) === rowObject.element) {
// Prevent a recursion problem when using the keyboard to move rows
// up.
table
.querySelectorAll('tr.region-message')
.forEach(function each(item) {
// If the dragged row is in this region, but above the message row,
// swap it down one space.
if (
rowObject.method !== 'keyboard' ||
rowObject.direction === 'down'
item.previousElementSibling === rowObject.element &&
item.previousElementSibling.matches('tr')
) {
rowObject.swap('after', this);
// Prevent a recursion problem when using the keyboard to move rows
// up.
if (
rowObject.method !== 'keyboard' ||
rowObject.direction === 'down'
) {
rowObject.swap('after', item);
}
}
}
// This region has become empty.
if (
$this.next('tr').length === 0 ||
!$this.next('tr')[0].matches('.draggable')
) {
$this.removeClass('region-populated').addClass('region-empty');
}
// This region has become populated.
else if (this.matches('.region-empty')) {
$this.removeClass('region-empty').addClass('region-populated');
}
});
// item region has become empty.
if (
!item.nextElementSibling ||
!item.nextElementSibling.matches('.draggable')
) {
item.classList.remove('region-populated');
item.classList.add('region-empty');
}
// item region has become populated.
else if (item.matches('.region-empty')) {
item.classList.remove('region-empty');
item.classList.add('region-populated');
}
});
}
/**
* Function to update the last placed row with the correct classes.
*
* @param {jQuery} table
* The jQuery object representing the table to inspect.
* @param {Element} table
* The DOM object representing the table to inspect.
* @param {Drupal.tableDrag.row} rowObject
* Drupal table drag row dropped.
*/
function updateLastPlaced(table, rowObject) {
// Remove the color-success class from new navigation block if
// applicable.
table.find('.color-success').removeClass('color-success');
const $rowObject = $(rowObject);
table
.querySelectorAll('.color-success')
.forEach((item) => item.classList.remove('color-success'));
if (!rowObject.element.matches('.drag-previous')) {
table.find('.drag-previous').removeClass('drag-previous');
$rowObject.addClass('drag-previous');
table
.querySelectorAll('.drag-previous')
.forEach((item) => item.classList.remove('drag-previous'));
rowObject.element.classList.add('drag-previous');
}
}
/**
* Update navigation block weights in the given region.
*
* @param {jQuery} table
* @param {Element} table
* Table with draggable items.
* @param {string} region
* Machine name of region containing navigation blocks to update.
*/
function updateNavigationBlockWeights(table, region) {
// Calculate minimum weight.
let weight = -Math.round(table.find('.draggable').length / 2);
let weight = -Math.round(
table.querySelectorAll('.draggable').length / 2,
);
// Update the navigation block weights.
table
.find(`.region-${region}-message`)
.nextUntil('.region-title')
.find('select.navigation-block-weight')
.each(function () {
// Increment the weight before assigning it to prevent using the
// absolute minimum available weight. This way we always have an
// unused upper and lower bound, which makes manually setting the
// weights easier for users who prefer to do it that way.
this.value = ++weight;
.querySelectorAll(`.region-${region}-message`)
.forEach((messageRegion) => {
if (messageRegion.classList.contains('region-title')) return;
messageRegion
.querySelectorAll('select.navigation-block-weight')
// Increment the weight before assigning it to prevent using the
// absolute minimum available weight. This way we always have an
// unused upper and lower bound, which makes manually setting the
// weights easier for users who prefer to do it that way.
.forEach((select) => {
weight = 1 + weight;
select.value = weight;
});
});
}
const table = $('#navigation-blocks');
const table = document.getElementById('navigation-blocks');
// Get the navigation blocks tableDrag object.
const tableDrag = Drupal.tableDrag['navigation-blocks'];
// Add a handler for when a row is swapped, update empty regions.
tableDrag.row.prototype.onSwap = function (swappedRow) {
tableDrag.row.prototype.onSwap = Drupal.debounce(function swapRow() {
checkEmptyRegions(table, this);
updateLastPlaced(table, this);
};
}, 200);
// Add a handler so when a row is dropped, update fields dropped into
// new regions.
tableDrag.onDrop = function () {
const dragObject = this;
const $rowElement = $(dragObject.rowObject.element);
tableDrag.onDrop = function tableDrop() {
// Use "region-message" row instead of "region" row because
// "region-{region_name}-message" is less prone to regexp match errors.
const regionRow = $rowElement.prevAll('tr.region-message').get(0);
const regionName = regionRow.className.replace(
const regionRow = Array.from(
document.querySelectorAll('tr.region-message'),
).filter((row) => {
let passed = false;
if (passed || row === this.rowObject.element) {
passed = true;
return;
}
return row;
})[0];
const regionName = regionRow.classList.value.replace(
/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/,
'$2',
);
......@@ -196,45 +247,60 @@
};
// Add the behavior to each region select list.
$(once('navigation-block-region-select', 'select.navigation-block-region-select', context)).on(
'change',
function (event) {
const navSelect = once(
'navigation-block-region-select',
'select.navigation-block-region-select',
context,
);
if (navSelect.length) {
navSelect.shift().addEventListener('change', function regionChange() {
// Make our new row and select field.
const row = $(this).closest('tr');
const select = $(this);
const row = this.closest('tr');
// Find the correct region and insert the row as the last in the
// region.
// eslint-disable-next-line new-cap
tableDrag.rowObject = new tableDrag.row(row[0]);
const regionMessage = table.find(
`.region-${select[0].value}-message`,
);
const regionItems = regionMessage.nextUntil(
'.region-message, .region-title',
const regionMessage = table.querySelector(
`.region-${this.value}-message`,
);
const regionItems = Array.from(
document.querySelectorAll('.region-message, .region-title'),
).filter((item) => {
let passed = false;
if (passed || item === regionMessage) {
passed = true;
return;
}
return item;
});
if (regionItems.length) {
regionItems.last().after(row);
regionItems[regionItems.length - 1].after(row);
}
// We found that regionMessage is the last row.
else {
regionMessage.after(row);
}
updateNavigationBlockWeights(table, select[0].value);
updateNavigationBlockWeights(table, this.value);
// Modify empty regions with added or removed fields.
checkEmptyRegions(table, tableDrag.rowObject);
// Update last placed navigation block indication.
updateLastPlaced(table, tableDrag.rowObject);
// Show unsaved changes warning.
if (!tableDrag.changed) {
$(Drupal.theme('tableDragChangedWarning'))
.insertBefore(tableDrag.table)
.hide()
.fadeIn('slow');
const newElement = Drupal.theme('tableDragChangedWarning');
newElement.classList.add('navigation-block--animated');
newElement.classList.add('navigation-block--hide');
tableDrag.table.before(newElement);
newElement.classList.replace(
'navigation-block--hide',
'navigation-block--show',
);
tableDrag.changed = true;
}
// Remove focus from selectbox.
select.trigger('blur');
},
);
this.dispatchEvent('blur');
});
}
},
};
})(jQuery, window, Drupal, once);
})(Drupal, once);
......@@ -28,24 +28,27 @@ navigation.escapeAdmin:
js:
js/escapeAdmin.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/once
- core/drupal.displace
admin-reset-styles:
version: VERSION
css:
base:
css/base/admin-reset-styles.css: {}
body-scroll-lock:
version: VERSION
css:
base:
css/components/body-scroll-lock.css: {}
drupalFilter:
version: VERSION
js:
js/drupal-filter.js: {}
dependencies:
- core/drupal
- core/drupal.announce
- core/drupal.debounce
- core/once
floating-ui:
js:
assets/vendor/floating-ui/floating-ui.core.umd.js: { minified: true }
......@@ -86,7 +89,6 @@ navigation_block:
js:
js/navigation-block.js: {}
dependencies:
- core/jquery
- core/drupal
- core/once
......@@ -98,9 +100,7 @@ navigation_block.admin:
theme:
css/navigation-block.admin.css: {}
dependencies:
- core/jquery
- core/drupal
- navigation/drupalFilter
- core/drupal.announce
- core/drupal.debounce
- core/drupal.dialog.ajax
- core/once
......@@ -20,6 +20,8 @@ class SystemMenuNavigationBlockCacheTagInvalidator implements CacheTagsInvalidat
*
* @param \Drupal\navigation\NavigationBlockManagerInterface $navigationBlockManager
* The navigation block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(protected NavigationBlockManagerInterface $navigationBlockManager, protected EntityTypeManagerInterface $entityTypeManager) {
}
......
......@@ -153,8 +153,11 @@ class NavigationBlockLibraryController extends ControllerBase {
'#size' => 30,
'#placeholder' => $this->t('Filter by navigation block name'),
'#attributes' => [
'class' => ['navigation-block-filter-text'],
'data-element' => '.navigation-block-add-table',
'class' => ['table-filter-text'],
'data-table' => '.navigation-block-add-table',
'data-items' => 'tbody tr',
'data-singular' => 'navigation block',
'data-plural' => 'navigation blocks',
'title' => $this->t('Enter a part of the navigation block name to filter by.'),
],
];
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation\Entity;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation\Form;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation\Form;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation;
......
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace Drupal\navigation;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment