Skip to content
Snippets Groups Projects
Commit c8cc66fb authored by Justin Toupin's avatar Justin Toupin
Browse files

wip - ready to merge back to 2.0.x

parent 10a1eb47
No related branches found
No related tags found
3 merge requests!103Issue #3295875: Add a new dedicated permission for Layout paragraphs configurations,!27Add default width and height values for modals.,!25Provide layout info in hook_preprocess_paragraph
......@@ -42,17 +42,21 @@
padding: 5px;
position: relative;
}
.dragula-enabled .lpb-component {
.lpb-component:hover,
.lpb-component:focus-within {
outline: 1px solid blue;
}
.lpb-component {
cursor: grab;
}
.lpb-component:hover .lpb-region {
.lpb-component:hover .lpb-region,
.lpb-component:focus-within .lpb-region {
outline: 1px dotted rgba(0, 0, 255, 0.5);
}
.lpb-component:hover .lpb-region:hover {
.lpb-component:hover .lpb-region:hover,
.lpb-component:focus-within .lpb-region:focus-within {
outline: 1px solid rgba(0, 0, 255, 0.5);
}
.lpb-active-item,
.lpb-active-item.lpb-region
.lpb-component:hover .js-lpb-active-item {
outline: 1px solid blue !important;
}
......@@ -91,7 +95,8 @@
top: 5px;
opacity: 0;
}
.lpb-component:hover > .lpb-controls {
.lpb-component:hover > .lpb-controls,
.lpb-component:focus-within > .lpb-controls {
opacity: 1;
}
.lpb-controls a span {
......@@ -114,7 +119,7 @@
width: 28px;
margin: 0;
padding: 0;
border: none;
border: 1px solid transparent;
border-radius: 50%;
opacity: 1;
}
......@@ -126,6 +131,12 @@
background-color: #eee;
box-shadow: 0 0 1px rgba(0, 0, 0, 1);
}
.lpb-up:focus,
.lpb-down:focus,
.lpb-edit:focus,
.lpb-delete:focus {
border-color: blue;
}
.lpb-up {
background: url(../img/icon-up.png) 0 0 no-repeat;
background-size: cover;
......@@ -134,13 +145,13 @@
background: url(../img/icon-down.png) 0 0 no-repeat;
background-size: cover;
}
.lpb-down[disabled]:hover,
.lpb-up[disabled]:hover {
.lpb-down[tabindex="-1"]:hover,
.lpb-up[tabindex="-1"]:hover {
background-color: transparent;
box-shadow: none;
}
.lpb-down[disabled],
.lpb-up[disabled] {
.lpb-down[tabindex="-1"],
.lpb-up[tabindex="-1"] {
opacity: .3;
cursor: default;
}
......@@ -152,12 +163,6 @@
background: url(../img/icon-delete.png) 0 0 no-repeat;
background-size: cover;
}
.lpb-region:hover {
outline: 1px solid blue;
}
.lpb-component:hover {
outline: 1px solid blue;
}
.lpb-btn {
position: absolute;
position: absolute;
......@@ -180,13 +185,14 @@
.lpb-component > .lpb-btn {
opacity: 0;
}
.lpb-component:hover > .lpb-btn {
.lpb-component:hover > .lpb-btn,
.lpb-component:focus-within > .lpb-btn {
opacity: 1;
}
.lpb-btn--add {
position: absolute;
display: inline-block;
border: none;
border: 1px solid transparent;
color: #333;
font-weight: normal;
line-height: 1;
......@@ -205,6 +211,9 @@
z-index: 1000;
opacity: 0;
}
.lpb-btn--add:focus {
border-color: blue;
}
.lpb-btn--add.center {
top: 50%;
transform: translate(-50%, -50%);
......@@ -216,11 +225,13 @@
.lpb-btn--add.after {
bottom: -18px;
}
.lpb-component:hover > .lpb-btn--add {
.lpb-component:hover > .lpb-btn--add,
.lpb-component:focus-within > .lpb-btn--add {
visibility: visible;
opacity: 1;
}
.lpb-region:hover > .lpb-btn--add {
.lpb-region:hover > .lpb-btn--add,
.lpb-region:focus-within > .lpb-btn--add {
visibility: visible;
opacity: 1;
}
......
......@@ -27,19 +27,157 @@
},
}).execute();
});
/**
* Returns a list of errors for the attempted move, or an empty array if there are no errors.
* @param {Element} el The element being moved.
* @param {Element} target The distination
* @param {Element} settings The builder settings.
* @param {Element} el The element being moved.
* @param {Element} target The destination
* @param {Element} source The source
* @param {Element} sibling The next sibling element
* @return {Array} An array of errors.
*/
function lpbMoveErrors(el, target, settings) {
function moveErrors(settings, el, target, source, sibling) {
return Drupal._lpbMoveErrors
.map(validator => validator.apply(null, [el, target, settings]))
.map(validator =>
validator.apply(null, [settings, el, target, source, sibling]),
)
.filter(errors => errors !== false && errors !== undefined);
}
function updateMoveButtons($element) {
$element.find('.lpb-up, .lpb-down').attr('tabindex', '0');
$element
.find(
'.lpb-component:first-of-type .lpb-up, .lpb-component:last-of-type .lpb-down',
)
.attr('tabindex', '-1');
}
function updateUi($element) {
reorderComponents($element);
updateMoveButtons($element);
}
/**
* Moves a component up or down within a simple list of components.
* @param {jQuery} $moveItem The item to move.
* @param {int} direction 1 (down) or -1 (up).
* @return {void}
*/
function move($moveItem, direction) {
const $sibling =
direction === 1
? $moveItem.nextAll('.lpb-component').first()
: $moveItem.prevAll('.lpb-component').first();
const method = direction === 1 ? 'after' : 'before';
const { scrollY } = window;
const destScroll = scrollY + $sibling.outerHeight() * direction;
const distance = Math.abs(destScroll - scrollY);
if ($sibling.length === 0) {
return false;
}
$({ translateY: 0 }).animate(
{ translateY: 100 * direction },
{
duration: Math.max(100, Math.min(distance, 500)),
easing: 'swing',
step() {
const a = $sibling.outerHeight() * (this.translateY / 100);
const b = -$moveItem.outerHeight() * (this.translateY / 100);
$moveItem.css({ transform: `translateY(${a}px)` });
$sibling.css({ transform: `translateY(${b}px)` });
},
complete() {
$moveItem.css({ transform: 'none' });
$sibling.css({ transform: 'none' });
$sibling[method]($moveItem);
updateUi($moveItem.closest(`[${idAttr}]`));
},
},
);
if (distance > 50) {
$('html, body').animate({ scrollTop: destScroll });
}
}
/**
* 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
* components to any valid position in an entire layout.
* @param {jQuery} $item The jQuery item 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');
// Add shims as target elements.
if (dir === -1) {
$(
'.lpb-region .lpb-btn--add, .lpb-layout:not(.lpb-active-item)',
$element,
).before('<div class="lpb-shim"></div>');
} else if (dir === 1) {
$('.lpb-region', $element).prepend('<div class="lpb-shim"></div>');
$('.lpb-layout:not(.lpb-active-item)', $element).after(
'<div class="lpb-shim"></div>',
);
}
// Build a list of possible targets, or move destinatons.
const targets = $('.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 pos = targets.indexOf(currentElement);
// Check to see if the next position is allowed by calling the 'accepts' callback.
while (
targets[pos + dir] !== undefined &&
moveErrors(
settings,
$item[0],
targets[pos + dir].parentNode,
null,
$item.next().length ? $item.next()[0] : 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);
}
// Remove the shims and save the order.
$('.lpb-shim', $element).remove();
updateUi($element);
$item.removeClass('lpb-active-item');
}
function attachEventListeners($element, settings) {
$element.on('click.lp-builder', '.lpb-up', e => {
move($(e.target).closest('.lpb-component'), -1);
return false;
});
$element.on('click.lp-builder', '.lpb-down', e => {
move($(e.target).closest('.lpb-component'), 1);
return false;
});
$element.on('click.lp-builder', '.lpb-component', e => {
$(e.currentTarget).focus();
return false;
});
document.addEventListener('keydown', e => {
const $item = $('.lpb-component:focus');
if ($item.length) {
if (e.code === 'ArrowDown' && $item) {
nav($item, 1, settings);
}
if (e.code === 'ArrowUp' && $item) {
nav($item, -1, settings);
}
}
});
}
Drupal._lpbMoveErrors = [];
/**
* Registers a move validation function.
......@@ -49,13 +187,13 @@
Drupal._lpbMoveErrors.push(f);
};
// Checks nesting depth.
Drupal.registerLpbMoveError((el, target, settings) => {
Drupal.registerLpbMoveError((settings, el, target) => {
if (el.className.indexOf('lpb-layout') > -1) {
return $(target).parents('.lpb-layout').length > settings.nesting_depth;
}
});
// If layout is required, prevents component from being placed outside a layout.
Drupal.registerLpbMoveError((el, target, settings) => {
Drupal.registerLpbMoveError((settings, el, target) => {
if (settings.require_layouts) {
if (
el.className.indexOf('lpb-component') > -1 &&
......@@ -65,9 +203,9 @@
}
}
});
Drupal.behaviors.layoutParagraphsBuilder = {
attach: function attach(context, settings) {
// Run only once - initialize the editor ui.
$('[data-lp-builder-id]', context)
.once('lp-builder')
.each((index, element) => {
......@@ -78,10 +216,13 @@
.find('.lpb-components, .lpb-region')
.get();
const drake = dragula(dragContainers, {
accepts(el, target) {
accepts(el, target, source, sibling) {
// Returns false if any registered validator returns a value.
// @see addMoveValidator()
return lpbMoveErrors(el, target, lpbSettings).length === 0;
return (
moveErrors(lpbSettings, el, target, source, sibling).length ===
0
);
},
moves(el, source, handle) {
const $handle = $(handle);
......@@ -95,8 +236,12 @@
return true;
},
});
drake.on('drop', () => {
reorderComponents($element);
drake.on('drop', el => {
const $el = $(el);
if ($el.prev().is('a')) {
$el.insertBefore($el.prev());
}
updateUi($element);
});
drake.on('drag', el => {
$element.addClass('is-dragging');
......@@ -119,7 +264,17 @@
$(container).removeClass('drag-target');
});
$element.data('drake', drake);
updateMoveButtons($element);
attachEventListeners($element, lpbSettings);
});
// Run every time the behavior is attached.
if (context.classList && context.classList.contains('lpb-component')) {
$(context)
.closest('[data-lp-builder-id]')
.each((index, element) => {
updateMoveButtons($(element));
});
}
},
};
})(jQuery, Drupal, Drupal.debounce, drupalSettings, dragula);
......@@ -170,6 +170,7 @@ class LayoutParagraphsBuilderController extends ControllerBase {
return $type['is_section'] === FALSE;
});
$component_menu = [
'#title' => $this->t('Choose a component'),
'#theme' => 'layout_paragraphs_builder_component_menu',
'#types' => [
'layout' => $section_components,
......
......@@ -219,7 +219,7 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP
$build['#attributes']['data-type'] = $entity->bundle();
$build['#attributes']['data-id'] = $entity->id();
$build['#attributes']['class'][] = 'lpb-component';
$build['#attributes']['tabindex'] = 0;
$build['#attributes']['tabindex'] = '0';
$url_params = [
'layout_paragraphs_layout' => $layout->id(),
......@@ -270,7 +270,6 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP
'class' => ['lpb-region'],
'data-region' => $region_name,
'data-region-uuid' => $entity->uuid() . '-' . $region_name,
'tabindex' => 0,
],
'insert_button' => $this->insertComponentButton($url_params, 10000, ['center']),
];
......
......@@ -52,7 +52,7 @@ class LayoutParagraphsAllowedTypesEvent extends Event {
* @param string $region
* The region.
*/
public function __construct(array $types, LayoutParagraphsLayout $layout, string $parent_uuid, string $region) {
public function __construct(array $types, LayoutParagraphsLayout $layout, $parent_uuid = '', $region = '') {
$this->types = $types;
$this->layout = $layout;
$this->parentUuid = $parent_uuid;
......
......@@ -126,6 +126,7 @@ abstract class ComponentFormBase extends FormBase {
$this->paragraphType = $this->paragraph->getParagraphType();
$form += [
'#title' => $this->formTitle(),
'#paragraph' => $this->paragraph,
'#display' => $display,
'#tree' => TRUE,
......@@ -191,6 +192,16 @@ abstract class ComponentFormBase extends FormBase {
return $form;
}
/**
* Create the form title.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The form title.
*/
protected function formTitle() {
return $this->t('Component form');
}
/**
* After build callback fixes issues with data-drupal-selector.
*
......
......@@ -48,6 +48,16 @@ class EditComponentForm extends ComponentFormBase {
return $form;
}
/**
* Create the form title.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The form title.
*/
protected function formTitle() {
return $this->t('Edit @type', ['@type' => $this->paragraph->getParagraphType()->label()]);
}
/**
* {@inheritDoc}
*/
......
......@@ -79,6 +79,16 @@ class InsertComponentForm extends ComponentFormBase {
return $this->buildComponentForm($form, $form_state);
}
/**
* Create the form title.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The form title.
*/
protected function formTitle() {
return $this->t('Create new @type', ['@type' => $this->paragraph->getParagraphType()->label()]);
}
/**
* {@inheritDoc}
*/
......
<div class="lpb-controls">
<span class="lpb-controls-label">{{label}}</span>
<a class="lpb-up hidden" href="#move-up"><span>{{ 'Move Up'|t }}</span></a>
<a class="lpb-down hidden" href="#move-down"><span>{{ 'Move Down'|t }}</span></a>
<a class="lpb-up" href="#move-up"><span>{{ 'Move Up'|t }}</span></a>
<a class="lpb-down" href="#move-down"><span>{{ 'Move Down'|t }}</span></a>
<a class="lpb-edit use-ajax" data-dialog-type="modal" data-dialog-options="{{ dialog_options }}" href="{{edit_url}}"><span>{{ 'Edit'|t }}</span></a>
<a class="lpb-delete use-ajax" href="{{delete_url}}" data-confirm="{{ 'Really delete? There is no undo.'|t }}"><span>{{ 'Delete'|t }}</span></a>
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment