diff --git a/css/builder.css b/css/builder.css index e030293ebf5d366c047205f6af02395759e52896..c629481a538f6db0a48f5dad37b97c9a8af31597 100644 --- a/css/builder.css +++ b/css/builder.css @@ -76,7 +76,9 @@ outline: none; } .lp-builder.is-dragging .js-lpb-region, -.lp-builder.is-dragging .js-lpb-component { +.lp-builder.is-navigating .js-lpb-region, +.lp-builder.is-dragging .js-lpb-component, +.lp-builder.is-navigating .js-lpb-component:not([data-focus="true"]) { outline: 1px dotted blue; } diff --git a/js/builder.js b/js/builder.js index 851ddff5adccd85b9797a936da5ceb410db45c29..cd6819525e9f5357ed65ffa31f7e65f9b12fcdc5 100644 --- a/js/builder.js +++ b/js/builder.js @@ -1,16 +1,19 @@ -(($, Drupal, debounce, Sortable, once) => { +(($, Drupal, drupalSettings, debounce, Sortable, once) => { const idAttr = 'data-lpb-id'; /** * Removes focus data attributes from all components. */ function unfocusComponents() { - document.querySelectorAll('[data-focus="true"]').forEach((element) => { + Array.from(document.querySelectorAll('[data-focus="true"]')).forEach((element) => { element.removeAttribute('data-focus'); }); - document.querySelectorAll('[data-focus-within="true"]').forEach((element) => { + Array.from(document.querySelectorAll('[data-focus-within="true"]')).forEach((element) => { element.removeAttribute('data-focus-within'); }); + Array.from(document.querySelectorAll('.is-navigating')).forEach((element) => { + element.classList.remove('is-navigating'); + }); } /** @@ -132,13 +135,33 @@ }); document.addEventListener('keydown', (event) => { - if (event.key === 'Delete' || event.key === 'Backspace') { - const focused = document.querySelector('.js-lpb-component[data-focus="true"]'); - if (focused) { + const focused = document.querySelector('.js-lpb-component[data-focus="true"]'); + if (!focused) { + return; + } + const layoutId = focused.closest(`[${idAttr}]`).getAttribute(idAttr); + const settings = drupalSettings.lpBuilder[layoutId]; + switch (event.key) { + case 'Delete': + case 'Backspace': focused.querySelector('.lpb-delete').click(); - } + break; + case 'ArrowUp': + case 'ArrowLeft': + nav(focused, -1, settings); + event.preventDefault(); + event.stopPropagation(); + break; + case 'ArrowDown': + case 'ArrowRight': + nav(focused, 1, settings); + event.preventDefault(); + event.stopPropagation(); + break; + default: + break; } - }) + }); /** * Attaches UI elements to $container. @@ -373,99 +396,73 @@ /** * Moves the focused component up or down the DOM to the next valid position - * when an arrow key is pressed. Unlike move(), nav()can fully navigate + * when an arrow key is pressed. Unlike move(), nav() can fully navigate * components to any valid position in an entire layout. - * @param {jQuery} $item The jQuery item to move. + * @param {HTMLElement} element The component to move. * @param {int} dir The direction to move (1 == down, -1 == up). * @param {Object} settings The builder ui settings. */ - function nav($item, dir, settings) { - const $element = $item.closest(`[${idAttr}]`); - $item.addClass('lpb-active-item'); + function nav(element, dir, settings) { + const layoutContainer = element.closest(`[${idAttr}]`); + layoutContainer.classList.add('is-navigating'); + + element.classList.add('lpb-active-item'); + // Add shims as target elements. - if (dir === -1) { - $( - '.js-lpb-region .lpb-btn--add.center, .lpb-layout:not(.lpb-active-item)', - $element, - ).before('<div class="lpb-shim"></div>'); - } else if (dir === 1) { - $('.js-lpb-region', $element).prepend('<div class="lpb-shim"></div>'); - $('.lpb-layout:not(.lpb-active-item)', $element).after( - '<div class="lpb-shim"></div>', - ); + if (dir < 0) { + layoutContainer.querySelectorAll('.js-lpb-region .lpb-btn--add.center, .lpb-layout:not(.lpb-active-item)').forEach(el => { + const shim = document.createElement('div'); + shim.className = 'lpb-shim'; + el.parentNode.insertBefore(shim, el); + }); + } else if (dir > 0 ) { + layoutContainer.querySelectorAll('.js-lpb-region').forEach(region => { + const shim = document.createElement('div'); + shim.className = 'lpb-shim'; + region.prepend(shim); + }); + layoutContainer.querySelectorAll('.lpb-layout:not(.lpb-active-item)').forEach(layout => { + const shim = document.createElement('div'); + shim.className = 'lpb-shim'; + layout.parentNode.insertBefore(shim, layout.nextSibling); + }); } + // Build a list of possible targets, or move destinations. - const targets = $('.js-lpb-component, .lpb-shim', $element) - .toArray() - // Remove child components from possible targets. - .filter((i) => !$.contains($item[0], i)) - // Remove layout elements that are not self from possible targets. - .filter( - (i) => i.className.indexOf('lpb-layout') === -1 || i === $item[0], - ); - const currentElement = $item[0]; + let targets = Array.from(layoutContainer.querySelectorAll('.js-lpb-component, .lpb-shim')) + .filter(i => !(element !== i && element.contains(i))) + .filter(i => !(i.classList.contains('lpb-layout') && i !== element)); + + const currentElement = element; let pos = targets.indexOf(currentElement); + // Check to see if the next position is allowed by calling the 'accepts' callback. while ( targets[pos + dir] !== undefined && acceptsErrors( settings, - $item[0], + element, targets[pos + dir].parentNode, null, - $item.next().length ? $item.next()[0] : null, + element.nextElementSibling || null ).length > 0 ) { pos += dir; } + if (targets[pos + dir] !== undefined) { - // Move after or before the target based on direction. - $(targets[pos + dir])[dir === 1 ? 'after' : 'before']($item); + const target = targets[pos + dir]; + target.insertAdjacentElement(dir === 1 ? 'afterend' : 'beforebegin', element); } - // Remove the shims and save the order. - $('.lpb-shim', $element).remove(); - $item.removeClass('lpb-active-item').focus(); - $item - .closest(`[${idAttr}]`) - .trigger('lpb-component:move', [$item.attr('data-uuid')]); - } - function startNav($item) { - const $msg = $( - `<div id="lpb-navigating-msg" class="lpb-tooltiptext lpb-tooltiptext--visible js-lpb-tooltiptext">${Drupal.t( - 'Use arrow keys to move. Press Return or Tab when finished.', - )}</div>`, - ); - $item - .closest('.lp-builder') - .addClass('is-navigating') - .find('.is-navigating') - .removeClass('is-navigating'); - $item - .attr('aria-describedby', 'lpb-navigating-msg') - .addClass('is-navigating') - .prepend($msg); - $item.before('<div class="lpb-navigating-placeholder"></div>'); - } - - function stopNav($item) { - $item - .removeClass('is-navigating') - .attr('aria-describedby', '') - .find('.js-lpb-tooltiptext') - .remove(); - $item - .closest(`[${idAttr}]`) - .removeClass('is-navigating') - .find('.lpb-navigating-placeholder') - .remove(); - } + // Remove the shims and save the order. + layoutContainer.querySelectorAll('.lpb-shim').forEach(shim => shim.remove()); + element.classList.remove('lpb-active-item'); + element.focus(); - function cancelNav($item) { - const $builder = $item.closest(`[${idAttr}]`); - $builder.find('.lpb-navigating-placeholder').replaceWith($item); - updateUi($builder[0]); - stopNav($item); + layoutContainer.dispatchEvent(new CustomEvent('lpb-component:move', { + detail: element.getAttribute('data-uuid') + })); } /** @@ -519,30 +516,6 @@ startNav($btn.closest('.js-lpb-component')); }); $(document).off('keydown'); - $(document).on('keydown', (e) => { - const $item = $('.js-lpb-component.is-navigating'); - if ($item.length) { - switch (e.code) { - case 'ArrowUp': - case 'ArrowLeft': - nav($item, -1, settings); - break; - case 'ArrowDown': - case 'ArrowRight': - nav($item, 1, settings); - break; - case 'Enter': - case 'Tab': - stopNav($item); - break; - case 'Escape': - cancelNav($item); - break; - default: - break; - } - } - }); } /** @@ -901,4 +874,4 @@ } else { window.addEventListener('dialog:aftercreate', handleAfterDialogCreate); } -})(jQuery, Drupal, Drupal.debounce, Sortable, once); +})(jQuery, Drupal, drupalSettings, Drupal.debounce, Sortable, once);