first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
* Layout Builder UI styling.
*/
.layout-builder {
padding: 1.5em 1.5em 0.5em;
border: 3px solid #2f91da;
background-color: #fff;
}
.layout-builder__add-section {
width: 100%;
margin-bottom: 1.5em;
padding: 1.5em 0;
text-align: center;
outline: 2px dashed #979797;
background-color: #f7f7f7;
}
.layout-builder__link--add {
padding-inline-start: 1.3em;
color: #686868;
border-bottom: none;
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath fill='%23787878' d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e") transparent center left / 1em no-repeat; /* LTR */
}
[dir="rtl"] .layout-builder__link--add {
background-position-x: right;
}
.layout-builder__link--add:hover,
.layout-builder__link--add:active {
color: #000;
border-bottom-style: none;
}
.layout-builder__section {
margin-bottom: 1.5em;
}
.layout-builder__section .ui-sortable-helper {
outline: 2px solid #f7f7f7;
background-color: #fff;
}
.layout-builder__section .ui-state-drop {
margin: 1.25rem;
padding: 1.875rem;
outline: 2px dashed #fedb60;
background-color: #ffd;
}
.layout-builder__region {
outline: 2px dashed #2f91da;
}
.layout-builder__add-block {
padding: 1.5em 0;
text-align: center;
background-color: #eff6fc;
}
.layout-builder__link--remove {
position: relative;
z-index: 2;
display: inline-block;
box-sizing: border-box;
width: 1.625rem;
height: 1.625rem;
margin-inline: -0.625rem 0.375rem;
padding: 0;
white-space: nowrap;
text-indent: -624.9375rem;
border: 1px solid #ccc;
border-radius: 1.625rem;
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M3.51 13.925c.194.194.512.195.706.001l3.432-3.431c.194-.194.514-.194.708 0l3.432 3.431c.192.194.514.193.707-.001l1.405-1.417c.191-.195.189-.514-.002-.709l-3.397-3.4c-.192-.193-.192-.514-.002-.708l3.401-3.43c.189-.195.189-.515 0-.709l-1.407-1.418c-.195-.195-.513-.195-.707-.001l-3.43 3.431c-.195.194-.516.194-.708 0l-3.432-3.431c-.195-.195-.512-.194-.706.001l-1.407 1.417c-.194.195-.194.515 0 .71l3.403 3.429c.193.195.193.514-.001.708l-3.4 3.399c-.194.195-.195.516-.001.709l1.406 1.419z'/%3e%3c/svg%3e") #fff center center / 1rem 1rem no-repeat;
font-size: 1rem;
}
.layout-builder__link--remove:hover {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M3.51 13.925c.194.194.512.195.706.001l3.432-3.431c.194-.194.514-.194.708 0l3.432 3.431c.192.194.514.193.707-.001l1.405-1.417c.191-.195.189-.514-.002-.709l-3.397-3.4c-.192-.193-.192-.514-.002-.708l3.401-3.43c.189-.195.189-.515 0-.709l-1.407-1.418c-.195-.195-.513-.195-.707-.001l-3.43 3.431c-.195.194-.516.194-.708 0l-3.432-3.431c-.195-.195-.512-.194-.706.001l-1.407 1.417c-.194.195-.194.515 0 .71l3.403 3.429c.193.195.193.514-.001.708l-3.4 3.399c-.194.195-.195.516-.001.709l1.406 1.419z'/%3e%3c/svg%3e");
}
.layout-builder-block {
padding: 1.5em;
cursor: move;
background-color: #fff;
}
.layout-builder-block [tabindex="-1"] {
pointer-events: none;
}
.layout-builder--content-preview-disabled .layout-builder-block {
margin: 0;
border-bottom: 2px dashed #979797;
}
/*
* Layout Builder messages.
*/
.layout-builder__message .messages {
background-repeat: no-repeat;
}
.layout-builder__message--defaults .messages {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2373b355'%3e%3cpath d='M8,0.0309333333 C12.4181818,0.0309333333 16,3.5328 16,7.85315556 C16,12.1731556 12.4181818,15.6750222 8,15.6750222 C3.58181818,15.6750222 0,12.1731556 0,7.85315556 C0,3.5328 3.58181818,0.0309333333 8,0.0309333333 Z M12.7236364,4.69653333 C12.5116364,4.95288889 12.1872727,4.50133333 11.9,4.3552 C11.6130909,4.20871111 11.5505455,4.69653333 11.2138182,5.25795556 C10.8767273,5.81937778 10.6770909,5.56302222 10.3774545,5.25795556 C10.0781818,4.95288889 10.2650909,4.68444444 10.5774545,4.56248889 C10.8890909,4.44053333 10.9141818,4.39182222 10.6894545,4.14755556 C10.4650909,3.90364444 10.3650909,4.00106667 10.5898182,3.80586667 C10.8141818,3.61066667 10.9516364,3.6352 11.4385455,3.34222222 C11.9250909,3.04924444 11.3636364,2.7808 11.0389091,2.99484444 C10.7145455,3.20853333 10.3025455,2.56142222 10.2152727,2.01671111 C10.128,1.47164444 10.3901818,1.64622222 10.6894545,1.45066667 C10.7610909,1.40444444 10.7192727,1.29386667 10.6170909,1.15377778 C9.80363636,0.849066667 8.92181818,0.680177778 8,0.680177778 C7.08945455,0.680177778 6.21745455,0.8448 5.41236364,1.14275556 C5.26763636,1.33795556 5.23709091,1.49475556 5.53527273,1.4752 C6.284,1.42648889 6.29672727,1.26791111 6.55890909,1.6096 C6.82072727,1.95128889 6.696,2.65884444 6.22181818,2.46364444 C5.74763636,2.26844444 4.96145455,1.93884444 4.76181818,2.3296 C4.56181818,2.72 4.84909091,2.81742222 5.236,2.5856 C5.62254545,2.35377778 5.93490909,2.46364444 6.00945455,2.84195556 C6.08436364,3.22026667 6.05963636,3.95235556 5.63527273,3.81831111 C5.21090909,3.68391111 5.07345455,3.8912 5.32327273,4.09884444 C5.57272727,4.30613333 5.29818182,4.42844444 4.91127273,4.26951111 C4.52436364,4.11093333 4.53709091,4.59911111 4.01272727,4.57457778 C3.48872727,4.5504 3.45127273,5.11146667 3.28909091,5.31911111 C3.12690909,5.5264 3.10181818,6.19768889 3.06436364,6.35626667 C3.02690909,6.51484444 2.86472727,6.6368 2.77745455,6.13653333 C2.68981818,5.63626667 2.80218182,5.41653333 2.34072727,5.38026667 C1.87890909,5.34328889 1.41709091,6.29191111 1.65418182,6.49493333 C1.89127273,6.69795556 2.21563636,6.30755556 2.19090909,6.60017778 C2.16581818,6.89315556 2.00363636,8.22328889 2.49018182,8.28408889 C2.97709091,8.34524444 3.30145455,8.29653333 3.70072727,8.55288889 C4.1,8.80888889 4.84909091,8.83342222 5.38545455,9.66328889 C5.92218182,10.4928 6.54618182,10.5905778 7.14545455,10.7249778 C7.74436364,10.8590222 7.68181818,11.1886222 7.36981818,11.6277333 C7.05781818,12.0672 7.23272727,12.5184 6.18436364,12.8600889 C5.136,13.2017778 5.06109091,14.0561778 5.09854545,14.2147556 C5.10618182,14.2471111 5.092,14.3239111 5.06545455,14.4248889 C5.96472727,14.8103111 6.95709091,15.0257778 8,15.0257778 C10.0716364,15.0257778 11.944,14.1802667 13.2792727,12.8256 C13.1043636,12.2965333 12.9294545,11.6920889 12.9236364,11.3713778 C12.9109091,10.7249778 13.0981818,10.6759111 12.8610909,10.2488889 C12.624,9.82186667 12.1370909,10.2368 11.1887273,10.1144889 C10.2403636,9.99253333 10.3901818,9.40693333 9.72872727,8.6016 C9.06727273,7.79626667 10.34,7.13706667 10.6894545,6.30755556 C11.0389091,5.47768889 12.5614545,5.79484444 12.8985455,5.856 C13.2352727,5.91715556 13.1105455,5.69742222 13.8221818,5.18471111 C14.5334545,4.67235556 13.4727273,4.66026667 13.1854545,4.46506667 C12.8985455,4.26951111 12.936,4.44053333 12.7236364,4.69653333 Z M3.53854545,12.4088889 C3.47636364,11.8108444 3.02690909,11.4812444 2.50290909,10.3708444 C1.97854545,9.26044444 2.328,9.13848889 2.41527273,9.05315556 C2.50290909,8.96746667 1.82872727,8.61368889 1.85381818,7.75964444 C1.87890909,6.90524444 1.14254545,7.1008 1.27963636,6.19768889 C1.39745455,5.42328889 1.55018182,4.88426667 1.47381818,4.58417778 C0.957818182,5.56515556 0.664363636,6.67591111 0.664363636,7.85315556 C0.664363636,10.4938667 2.13272727,12.8046222 4.31236364,14.0494222 L4.27490909,13.8851556 C4.27490909,13.8851556 3.60109091,13.0065778 3.53854545,12.4088889 Z M8.48072727,1.69493333 C8.28109091,2.31715556 8.26109091,2.13333333 8.03127273,2.26844444 C7.78254545,2.41457778 7.65709091,2.51235556 7.44472727,2.80533333 C7.23272727,3.09831111 6.74581818,3.03715556 7.01381818,2.72 C7.28145455,2.40248889 7.18290909,1.54844444 7.03309091,1.31662222 C7.03309091,1.31662222 6.83345455,1.13351111 7.28254545,1.06026667 C7.732,0.987022222 8.68036364,1.07271111 8.48072727,1.69493333 Z'%3e%3c/path%3e%3c/svg%3e");
}
.layout-builder__message--overrides .messages {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2373b355'%3e%3cpath d='M5.4749999,0 C2.43935876,0 0,2.45021982 0,5.50153207 C0,8.5518841 5.4749999,16.0038459 5.4749999,16.0038459 C5.4749999,16.0038459 10.9499998,8.5518841 10.9499998,5.50153207 C10.9499998,2.45021982 8.51064105,0 5.4749999,0 Z M5.89615374,8.00192294 C4.48158136,8.00192294 3.36923071,6.89054251 3.36923071,5.4749999 C3.36923071,4.06042752 4.48061114,2.94807687 5.89615374,2.94807687 C7.31072613,2.94807687 8.42307678,4.0594573 8.42307678,5.4749999 C8.42307678,6.89051825 7.31075039,8.00192294 5.89615374,8.00192294 Z'%3e%3c/path%3e%3c/svg%3e");
}
/* Label when "content preview" is disabled. */
.layout-builder-block__content-preview-placeholder-label {
margin: 0;
text-align: center;
font-size: 1.429em;
line-height: 1.4;
}
.layout-builder__add-section.is-layout-builder-highlighted {
margin-bottom: calc(1.5em - 0.5rem);
outline: none;
}
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted,
.layout-builder__add-block.is-layout-builder-highlighted {
position: relative;
z-index: 1;
margin: -0.25rem -2px;
}
.layout-builder__add-block.is-layout-builder-highlighted,
.layout-builder__add-section.is-layout-builder-highlighted,
.layout-builder__layout.is-layout-builder-highlighted::before,
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted {
border: 4px solid #000;
}
.layout-builder__region-label,
.layout-builder__section-label {
display: none;
}
.layout-builder--move-blocks-active .layout-builder__region-label {
display: block;
}
.layout-builder--move-blocks-active .layout-builder__section-label {
display: inline;
}
.layout__region-info {
padding: 0.5em;
text-align: center;
border-bottom: 2px dashed #979797;
}
/**
* Remove "You have unsaved changes" warning because Layout Builder always has
* unsaved changes until "Save layout" is submitted.
* @todo create issue for todo.
*/
.layout-builder-components-table .tabledrag-changed-warning {
display: none !important;
}

View File

@@ -0,0 +1,168 @@
/**
* @file
* Layout Builder UI styling.
*/
.layout-builder {
padding: 1.5em 1.5em 0.5em;
border: 3px solid #2f91da;
background-color: #fff;
}
.layout-builder__add-section {
width: 100%;
margin-bottom: 1.5em;
padding: 1.5em 0;
text-align: center;
outline: 2px dashed #979797;
background-color: #f7f7f7;
}
.layout-builder__link--add {
padding-inline-start: 1.3em;
color: #686868;
border-bottom: none;
background: url(../../../misc/icons/787878/plus.svg) transparent center left / 1em no-repeat; /* LTR */
&:dir(rtl) {
background-position-x: right;
}
&:hover,
&:active {
color: #000;
border-bottom-style: none;
}
}
.layout-builder__section {
margin-bottom: 1.5em;
& .ui-sortable-helper {
outline: 2px solid #f7f7f7;
background-color: #fff;
}
& .ui-state-drop {
margin: 20px;
padding: 30px;
outline: 2px dashed #fedb60;
background-color: #ffd;
}
}
.layout-builder__region {
outline: 2px dashed #2f91da;
}
.layout-builder__add-block {
padding: 1.5em 0;
text-align: center;
background-color: #eff6fc;
}
.layout-builder__link--remove {
position: relative;
z-index: 2;
display: inline-block;
box-sizing: border-box;
width: 26px;
height: 26px;
margin-inline: -10px 6px;
padding: 0;
white-space: nowrap;
text-indent: -9999px;
border: 1px solid #ccc;
border-radius: 26px;
background: url(../../../misc/icons/bebebe/ex.svg) #fff center center / 16px 16px no-repeat;
font-size: 1rem;
&:hover {
background-image: url(../../../misc/icons/787878/ex.svg);
}
}
.layout-builder-block {
padding: 1.5em;
cursor: move;
background-color: #fff;
& [tabindex="-1"] {
pointer-events: none;
}
@nest .layout-builder--content-preview-disabled & {
margin: 0;
border-bottom: 2px dashed #979797;
}
}
/*
* Layout Builder messages.
*/
.layout-builder__message .messages {
background-repeat: no-repeat;
}
.layout-builder__message--defaults .messages {
background-image: url("../../../misc/icons/73b355/globe.svg");
}
.layout-builder__message--overrides .messages {
background-image: url("../../../misc/icons/73b355/location.svg");
}
/* Label when "content preview" is disabled. */
.layout-builder-block__content-preview-placeholder-label {
margin: 0;
text-align: center;
font-size: 1.429em;
line-height: 1.4;
}
.layout-builder__add-section.is-layout-builder-highlighted {
margin-bottom: calc(1.5em - 8px);
outline: none;
}
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted,
.layout-builder__add-block.is-layout-builder-highlighted {
position: relative;
z-index: 1;
margin: -4px -2px;
}
.layout-builder__add-block.is-layout-builder-highlighted,
.layout-builder__add-section.is-layout-builder-highlighted,
.layout-builder__layout.is-layout-builder-highlighted::before,
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted {
border: 4px solid #000;
}
.layout-builder__region-label,
.layout-builder__section-label {
display: none;
}
.layout-builder--move-blocks-active .layout-builder__region-label {
display: block;
}
.layout-builder--move-blocks-active .layout-builder__section-label {
display: inline;
}
.layout__region-info {
padding: 0.5em;
text-align: center;
border-bottom: 2px dashed #979797;
}
/**
* Remove "You have unsaved changes" warning because Layout Builder always has
* unsaved changes until "Save layout" is submitted.
* @todo create issue for todo.
*/
.layout-builder-components-table .tabledrag-changed-warning {
display: none !important;
}

View File

@@ -0,0 +1,140 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
* Layout Builder styling for off-canvas UI.
*/
#drupal-off-canvas-wrapper .layout-selection {
margin: 0;
padding: 0;
list-style: none;
}
#drupal-off-canvas-wrapper .layout-selection li {
position: relative; /* Anchor throbber. */
padding: calc(0.25 * var(--off-canvas-vertical-spacing-unit));
border-bottom: 1px solid var(--off-canvas-border-color);
}
#drupal-off-canvas-wrapper .layout-selection li:last-child {
padding-bottom: 0;
border-bottom: none;
}
/* Horizontally align icon and text. */
#drupal-off-canvas-wrapper .layout-selection a {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.625rem;
padding: 0.625rem;
}
/*
* This is the styling of the SVG within the layout selection list.
*/
#drupal-off-canvas-wrapper .layout-icon__region {
fill: var(--off-canvas-text-color);
stroke: transparent;
}
@media (forced-colors: active) {
#drupal-off-canvas-wrapper .layout-icon__region {
fill: canvastext;
}
}
#drupal-off-canvas-wrapper .inline-block-create-button {
--icon-size: 1rem;
position: relative; /* Anchor pseudo-element. */
display: block;
padding: 1.5rem;
padding-inline-start: calc(2 * var(--off-canvas-padding) + var(--icon-size) / 2); /* Room for icon */
border-bottom: 1px solid #333;
font-size: 1rem;
/* Plus icon. */
}
#drupal-off-canvas-wrapper .inline-block-create-button::before {
position: absolute;
top: 50%;
left: var(--off-canvas-padding);
width: var(--icon-size);
height: var(--icon-size);
content: "";
transform: translateY(-50%);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath fill='%23bebebe' d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-size: contain;
}
@media (forced-colors: active) {
#drupal-off-canvas-wrapper .inline-block-create-button::before {
background: linktext;
-webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath fill='%23bebebe' d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e");
mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath fill='%23bebebe' d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-size: contain;
}
}
#drupal-off-canvas-wrapper .inline-block-create-button,
#drupal-off-canvas-wrapper .inline-block-list__item {
margin: 0 calc(-1 * var(--off-canvas-padding));
color: var(--off-canvas-text-color);
}
#drupal-off-canvas-wrapper .inline-block-create-button:hover,
#drupal-off-canvas-wrapper .inline-block-list__item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
#drupal-off-canvas-wrapper .inline-block-create-button:focus,
#drupal-off-canvas-wrapper .inline-block-list__item:focus {
outline-offset: -4px; /* Prevent outline from being cut off. */
}
#drupal-off-canvas-wrapper .inline-block-list {
margin: 0 0 calc(2 * var(--off-canvas-vertical-spacing-unit));
padding: 0;
list-style: none;
}
#drupal-off-canvas-wrapper .inline-block-list li {
position: relative; /* Anchor throbber. */
margin: 0;
padding: calc(0.25 * var(--off-canvas-vertical-spacing-unit)) 0;
}
#drupal-off-canvas-wrapper .inline-block-list li:last-child {
padding-bottom: 0;
border-bottom: none;
}
/* This is the <a> tag. */
#drupal-off-canvas-wrapper .inline-block-list__item {
display: block;
flex-grow: 1;
padding: calc(2 * var(--off-canvas-vertical-spacing-unit)) var(--off-canvas-padding);
border-bottom: 1px solid var(--off-canvas-border-color);
}
/* Highlight the active block in the Move Block dialog. */
#drupal-off-canvas-wrapper .layout-builder-components-table__block-label--current {
padding-left: 1.0625rem;
border-left: solid 5px;
}

View File

@@ -0,0 +1,121 @@
/**
* @file
* Layout Builder styling for off-canvas UI.
*/
#drupal-off-canvas-wrapper {
& .layout-selection {
margin: 0;
padding: 0;
list-style: none;
& li {
position: relative; /* Anchor throbber. */
padding: calc(0.25 * var(--off-canvas-vertical-spacing-unit));
border-bottom: 1px solid var(--off-canvas-border-color);
&:last-child {
padding-bottom: 0;
border-bottom: none;
}
}
/* Horizontally align icon and text. */
& a {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px;
}
}
/*
* This is the styling of the SVG within the layout selection list.
*/
& .layout-icon__region {
fill: var(--off-canvas-text-color);
stroke: transparent;
@media (forced-colors: active) {
fill: canvastext;
}
}
& .inline-block-create-button {
--icon-size: 16px;
position: relative; /* Anchor pseudo-element. */
display: block;
padding: 24px;
padding-inline-start: calc(2 * var(--off-canvas-padding) + var(--icon-size) / 2); /* Room for icon */
border-bottom: 1px solid #333;
font-size: 16px;
/* Plus icon. */
&::before {
position: absolute;
top: 50%;
left: var(--off-canvas-padding);
width: var(--icon-size);
height: var(--icon-size);
content: "";
transform: translateY(-50%);
background-image: url(../../../misc/icons/bebebe/plus.svg);
background-repeat: no-repeat;
background-size: contain;
@media (forced-colors: active) {
background: linktext;
mask-image: url(../../../misc/icons/bebebe/plus.svg);
mask-repeat: no-repeat;
mask-size: contain;
}
}
}
& .inline-block-create-button,
& .inline-block-list__item {
margin: 0 calc(-1 * var(--off-canvas-padding));
color: var(--off-canvas-text-color);
&:hover {
background-color: rgba(255, 255, 255, 0.05);
}
&:focus {
outline-offset: -4px; /* Prevent outline from being cut off. */
}
}
& .inline-block-list {
margin: 0 0 calc(2 * var(--off-canvas-vertical-spacing-unit));
padding: 0;
list-style: none;
& li {
position: relative; /* Anchor throbber. */
margin: 0;
padding: calc(0.25 * var(--off-canvas-vertical-spacing-unit)) 0;
&:last-child {
padding-bottom: 0;
border-bottom: none;
}
}
}
/* This is the <a> tag. */
& .inline-block-list__item {
display: block;
flex-grow: 1;
padding: calc(2 * var(--off-canvas-vertical-spacing-unit)) var(--off-canvas-padding);
border-bottom: 1px solid var(--off-canvas-border-color);
}
/* Highlight the active block in the Move Block dialog. */
& .layout-builder-components-table__block-label--current {
padding-left: 17px;
border-left: solid 5px;
}
}

View File

@@ -0,0 +1,36 @@
---
label: 'Changing the layout for an entity'
related:
- core.appearance
- core.content_structure
- field_ui.manage_display
- block.overview
---
{% set content_types_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_text, 'entity.node_type.collection')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
{% set block_overview_topic = render_var(help_topic_link('block.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure an entity sub-type to have its fields displayed using a layout (see {{ content_structure_topic }} for more on entities and fields).{% endtrans %}</p>
<h2>{% trans %}What are the parts of a layout?{% endtrans %}</h2>
<p>{% trans %}A layout consists of one or more <em>sections</em>. Each section can have from one to four <em>columns</em>. You can place blocks, including special blocks for the fields on the entity sub-type, in each column of each section (see {{ block_overview_topic }} for more on blocks).{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; {{ content_types_link }}.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to create a layout for, and click <em>Manage display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Under <em>Layout options</em>, check <em>Use Layout Builder</em>. You can also check the box below to allow each entity item to have its layout individually customized (if it is left unchecked, the site will use the same layout for all items of this entity sub-type).{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be returned to the <em>Manage display</em> page, but you will no longer see the table of fields of the classic display manager.{% endtrans %}</li>
<li>{% trans %}Click <em>Manage layout</em> to enter layout management view. A default layout will be set up for you, with a single one-column section containing the fields on your entity sub-type.{% endtrans %}</li>
<li>{% trans %}To remove the default section and start from an empty layout, find and click the <em>Remove</em> button for the default section, which looks like an X. Confirm by clicking <em>Remove</em> in the pop-up dialog.{% endtrans %}</li>
<li>{% trans %}Add new sections, each with one to four columns, to your layout. For instance, you might want a one-column section at the top, a two-column section in the middle, and then a one-column section at the bottom. To add a section, click <em>Add section</em> and click the desired number of columns. For multi-column sections, set the column width percentages and click <em>Add section</em> in the pop-up dialog.{% endtrans %}</li>
<li>{% trans %}In each section, click <em>Add block</em> to add a block. You will see a list of the blocks available on your site, plus a section called <em>Content fields</em> with a block for each field on your content item. Each block can be configured, if desired, with a <em>Title</em>, and for content field blocks, you can also configure the field formatter. Continue to add blocks to your sections until all the desired blocks and fields are displayed.{% endtrans %}</li>
<li>{% trans %}Verify your layout. You can check <em>Show content preview</em> to show a preview of what your layout will look like, or uncheck it to see the names of the fields and blocks in each section.{% endtrans %}</li>
<li>{% trans %}If needed, reorder the blocks by dragging them to new locations. If you hover over a block, a contextual menu will appear that will let you change the configuration of the block, remove the block, or <em>Move</em> blocks within the section using a more compact interface.{% endtrans %}</li>
<li>{% trans %}When you are satisfied with your layout, click <em>Save layout</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/layout-builder/creating-layout-defaults">{% trans %}Creating layout defaults{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/8/core/modules/layout-builder/building-layouts-using-the-layout-builder-ui">{% trans %}Building Layouts Using the Layout Builder UI{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,470 @@
/**
* @file
* Attaches the behaviors for the Layout Builder module.
*/
(($, Drupal, Sortable) => {
const { ajax, behaviors, debounce, announce, formatPlural } = Drupal;
/*
* Boolean that tracks if block listing is currently being filtered. Declared
* outside of behaviors so value is retained on rebuild.
*/
let layoutBuilderBlocksFiltered = false;
/**
* Provides the ability to filter the block listing in "Add block" dialog.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach block filtering behavior to "Add block" dialog.
*/
behaviors.layoutBuilderBlockFilter = {
attach(context) {
const $categories = $('.js-layout-builder-categories', context);
const $filterLinks = $categories.find('.js-layout-builder-block-link');
/**
* Filters the block list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
const filterBlockList = (e) => {
const query = e.target.value.toLowerCase();
/**
* Shows or hides the block entry based on the query.
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} link
* The link to add the block.
*/
const toggleBlockEntry = (index, link) => {
const $link = $(link);
const textMatch = link.textContent.toLowerCase().includes(query);
// Checks if a category is currently hidden.
// Toggles the category on if so.
if (
Drupal.elementIsHidden(
$link.closest('.js-layout-builder-category')[0],
)
) {
$link.closest('.js-layout-builder-category').show();
}
// Toggle the li tag of the matching link.
$link.parent().toggle(textMatch);
};
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
// Attribute to note which categories are closed before opening all.
$categories
.find('.js-layout-builder-category:not([open])')
.attr('remember-closed', '');
// Open all categories so every block is available to filtering.
$categories.find('.js-layout-builder-category').attr('open', '');
// Toggle visibility of links based on query.
$filterLinks.each(toggleBlockEntry);
// Only display categories containing visible links.
$categories
.find(
'.js-layout-builder-category:not(:has(.js-layout-builder-block-link:visible))',
)
.hide();
announce(
formatPlural(
$categories.find('.js-layout-builder-block-link:visible').length,
'1 block is available in the modified list.',
'@count blocks are available in the modified list.',
),
);
layoutBuilderBlocksFiltered = true;
} else if (layoutBuilderBlocksFiltered) {
layoutBuilderBlocksFiltered = false;
// Remove "open" attr from categories that were closed pre-filtering.
$categories
.find('.js-layout-builder-category[remember-closed]')
.removeAttr('open')
.removeAttr('remember-closed');
// Show all categories since filter is turned off.
$categories.find('.js-layout-builder-category').show();
// Show all li tags since filter is turned off.
$filterLinks.parent().show();
announce(Drupal.t('All available blocks are listed.'));
}
};
$(
once('block-filter-text', 'input.js-layout-builder-filter', context),
).on('input', debounce(filterBlockList, 200));
},
};
/**
* Callback used in {@link Drupal.behaviors.layoutBuilderBlockDrag}.
*
* @param {HTMLElement} item
* The HTML element representing the repositioned block.
* @param {HTMLElement} from
* The HTML element representing the previous parent of item
* @param {HTMLElement} to
* The HTML element representing the current parent of item
*
* @internal This method is a callback for layoutBuilderBlockDrag and is used
* in FunctionalJavascript tests. It may be renamed if the test changes.
* @see https://www.drupal.org/node/3084730
*/
Drupal.layoutBuilderBlockUpdate = function (item, from, to) {
const $item = $(item);
const $from = $(from);
// Check if the region from the event and region for the item match.
const itemRegion = $item.closest('.js-layout-builder-region');
if (to === itemRegion[0]) {
// Find the destination delta.
const deltaTo = $item.closest('[data-layout-delta]').data('layout-delta');
// If the block didn't leave the original delta use the destination.
const deltaFrom = $from
? $from.closest('[data-layout-delta]').data('layout-delta')
: deltaTo;
ajax({
url: [
$item.closest('[data-layout-update-url]').data('layout-update-url'),
deltaFrom,
deltaTo,
itemRegion.data('region'),
$item.data('layout-block-uuid'),
$item.prev('[data-layout-block-uuid]').data('layout-block-uuid'),
]
.filter((element) => element !== undefined)
.join('/'),
}).execute();
}
};
/**
* Provides the ability to drag blocks to new positions in the layout.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach block drag behavior to the Layout Builder UI.
*/
behaviors.layoutBuilderBlockDrag = {
attach(context) {
const regionSelector = '.js-layout-builder-region';
Array.prototype.forEach.call(
context.querySelectorAll(regionSelector),
(region) => {
Sortable.create(region, {
draggable: '.js-layout-builder-block',
ghostClass: 'ui-state-drop',
group: 'builder-region',
filter: '.contextual',
onEnd: (event) =>
Drupal.layoutBuilderBlockUpdate(event.item, event.from, event.to),
});
},
);
},
};
/**
* Disables interactive elements in previewed blocks.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach disabling interactive elements behavior to the Layout Builder UI.
*/
behaviors.layoutBuilderDisableInteractiveElements = {
attach() {
// Disable interactive elements inside preview blocks.
const $blocks = $('#layout-builder [data-layout-block-uuid]');
$blocks.find('input, textarea, select').prop('disabled', true);
$blocks
.find('a')
// Don't disable contextual links.
// @see \Drupal\contextual\Element\ContextualLinksPlaceholder
.not(
(index, element) =>
$(element).closest('[data-contextual-id]').length > 0,
)
.on('click mouseup touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
});
/*
* In preview blocks, remove from the tabbing order all input elements
* and elements specifically assigned a tab index, other than those
* related to contextual links.
*/
$blocks
.find(
'button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"]):not(.tabbable)',
)
.not(
(index, element) =>
$(element).closest('[data-contextual-id]').length > 0,
)
.attr('tabindex', -1);
},
};
// After a dialog opens, highlight element that the dialog is acting on.
window.addEventListener('dialog:aftercreate', (e) => {
const $element = $(e.target);
if (Drupal.offCanvas.isOffCanvas($element)) {
// Start by removing any existing highlighted elements.
$('.is-layout-builder-highlighted').removeClass(
'is-layout-builder-highlighted',
);
/*
* Every dialog has a single 'data-layout-builder-target-highlight-id'
* attribute. Every dialog-opening element has a unique
* 'data-layout-builder-highlight-id' attribute.
*
* When the value of data-layout-builder-target-highlight-id matches
* an element's value of data-layout-builder-highlight-id, the class
* 'is-layout-builder-highlighted' is added to element.
*/
const id = $element
.find('[data-layout-builder-target-highlight-id]')
.attr('data-layout-builder-target-highlight-id');
if (id) {
$(`[data-layout-builder-highlight-id="${id}"]`).addClass(
'is-layout-builder-highlighted',
);
}
// Remove wrapper class added by move block form.
$('#layout-builder').removeClass('layout-builder--move-blocks-active');
/**
* If dialog has a data-add-layout-builder-wrapper attribute, get the
* value and add it as a class to the Layout Builder UI wrapper.
*
* Currently, only the move block form uses
* data-add-layout-builder-wrapper, but any dialog can use this attribute
* to add a class to the Layout Builder UI while opened.
*/
const layoutBuilderWrapperValue = $element
.find('[data-add-layout-builder-wrapper]')
.attr('data-add-layout-builder-wrapper');
if (layoutBuilderWrapperValue) {
$('#layout-builder').addClass(layoutBuilderWrapperValue);
}
}
});
/*
* When a Layout Builder dialog is triggered, the main canvas resizes. After
* the resize transition is complete, see if the target element is still
* visible in viewport. If not, scroll page so the target element is again
* visible.
*
* @todo Replace this custom solution when a general solution is made
* available with https://www.drupal.org/node/3033410
*/
if (document.querySelector('[data-off-canvas-main-canvas]')) {
const mainCanvas = document.querySelector('[data-off-canvas-main-canvas]');
// This event fires when canvas CSS transitions are complete.
mainCanvas.addEventListener('transitionend', () => {
const $target = $('.is-layout-builder-highlighted');
if ($target.length > 0) {
// These four variables are used to determine if the element is in the
// viewport.
const targetTop = $target.offset().top;
const targetBottom = targetTop + $target.outerHeight();
const viewportTop = $(window).scrollTop();
const viewportBottom = viewportTop + $(window).height();
// If the element is not in the viewport, scroll it into view.
if (targetBottom < viewportTop || targetTop > viewportBottom) {
const viewportMiddle = (viewportBottom + viewportTop) / 2;
const scrollAmount = targetTop - viewportMiddle;
// Check whether the browser supports scrollBy(options). If it does
// not, use scrollBy(x-coord, y-coord) instead.
if ('scrollBehavior' in document.documentElement.style) {
window.scrollBy({
top: scrollAmount,
left: 0,
behavior: 'smooth',
});
} else {
window.scrollBy(0, scrollAmount);
}
}
}
});
}
window.addEventListener('dialog:afterclose', (e) => {
const $element = $(e.target);
if (Drupal.offCanvas.isOffCanvas($element)) {
// Remove the highlight from all elements.
$('.is-layout-builder-highlighted').removeClass(
'is-layout-builder-highlighted',
);
// Remove wrapper class added by move block form.
$('#layout-builder').removeClass('layout-builder--move-blocks-active');
}
});
/**
* Toggles content preview in the Layout Builder UI.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach content preview toggle to the Layout Builder UI.
*/
behaviors.layoutBuilderToggleContentPreview = {
attach(context) {
const $layoutBuilder = $('#layout-builder');
// The content preview toggle.
const $layoutBuilderContentPreview = $('#layout-builder-content-preview');
// data-content-preview-id specifies the layout being edited.
const contentPreviewId =
$layoutBuilderContentPreview.data('content-preview-id');
/**
* Tracks if content preview is enabled for this layout. Defaults to true
* if no value has previously been set.
*/
const isContentPreview =
JSON.parse(localStorage.getItem(contentPreviewId)) !== false;
/**
* Disables content preview in the Layout Builder UI.
*
* Disabling content preview hides block content. It is replaced with the
* value of the block's data-layout-content-preview-placeholder-label
* attribute.
*
* @todo Revisit in https://www.drupal.org/node/3043215, it may be
* possible to remove all but the first line of this function.
*/
const disableContentPreview = () => {
$layoutBuilder.addClass('layout-builder--content-preview-disabled');
/**
* Iterate over all Layout Builder blocks to hide their content and add
* placeholder labels.
*/
$('[data-layout-content-preview-placeholder-label]', context).each(
(i, element) => {
const $element = $(element);
// Hide everything in block that isn't contextual link related.
$element.children(':not([data-contextual-id])').hide(0);
const contentPreviewPlaceholderText = $element.attr(
'data-layout-content-preview-placeholder-label',
);
const contentPreviewPlaceholderLabel = Drupal.theme(
'layoutBuilderPrependContentPreviewPlaceholderLabel',
contentPreviewPlaceholderText,
);
$element.prepend(contentPreviewPlaceholderLabel);
},
);
};
/**
* Enables content preview in the Layout Builder UI.
*
* When content preview is enabled, the Layout Builder UI returns to its
* default experience. This is accomplished by removing placeholder
* labels and un-hiding block content.
*
* @todo Revisit in https://www.drupal.org/node/3043215, it may be
* possible to remove all but the first line of this function.
*/
const enableContentPreview = () => {
$layoutBuilder.removeClass('layout-builder--content-preview-disabled');
// Remove all placeholder labels.
$('.js-layout-builder-content-preview-placeholder-label').remove();
// Iterate over all blocks.
$('[data-layout-content-preview-placeholder-label]').each(
(i, element) => {
$(element).children().show();
},
);
};
$('#layout-builder-content-preview', context).on('change', (event) => {
const isChecked = event.currentTarget.checked;
localStorage.setItem(contentPreviewId, JSON.stringify(isChecked));
if (isChecked) {
enableContentPreview();
announce(
Drupal.t('Block previews are visible. Block labels are hidden.'),
);
} else {
disableContentPreview();
announce(
Drupal.t('Block previews are hidden. Block labels are visible.'),
);
}
});
/**
* On rebuild, see if content preview has been set to disabled. If yes,
* disable content preview in the Layout Builder UI.
*/
if (!isContentPreview) {
$layoutBuilderContentPreview.attr('checked', false);
disableContentPreview();
}
},
};
/**
* Creates content preview placeholder label markup.
*
* @param {string} contentPreviewPlaceholderText
* The text content of the placeholder label
*
* @return {string}
* A HTML string of the placeholder label.
*/
Drupal.theme.layoutBuilderPrependContentPreviewPlaceholderLabel = (
contentPreviewPlaceholderText,
) => {
const contentPreviewPlaceholderLabel = document.createElement('div');
contentPreviewPlaceholderLabel.className =
'layout-builder-block__content-preview-placeholder-label js-layout-builder-content-preview-placeholder-label';
contentPreviewPlaceholderLabel.innerHTML = contentPreviewPlaceholderText;
return `<div class="layout-builder-block__content-preview-placeholder-label js-layout-builder-content-preview-placeholder-label">${contentPreviewPlaceholderText}</div>`;
};
// Remove all contextual links outside the layout.
$(window).on('drupalContextualLinkAdded', (event, data) => {
const element = data.$el;
const contextualId = element.attr('data-contextual-id');
if (contextualId && !contextualId.startsWith('layout_builder_block:')) {
element.remove();
}
});
})(jQuery, Drupal, Sortable);

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Hooks provided by the Layout Builder module.
*/
/**
* @defgroup layout_builder_access Layout Builder access
* @{
* In determining access rights for the Layout Builder UI,
* \Drupal\layout_builder\Access\LayoutBuilderAccessCheck checks if the
* specified section storage plugin (an implementation of
* \Drupal\layout_builder\SectionStorageInterface) grants access.
*
* By default, the Layout Builder access check requires the 'configure any
* layout' permission. Individual section storage plugins may override this by
* setting the 'handles_permission_check' attribute key to TRUE. Any section
* storage plugin that uses 'handles_permission_check' must provide its own
* complete routing access checking to avoid any access bypasses.
*
* This access checking is only enforced on the routing level (not on the entity
* or field level) with additional form access restrictions. All HTTP API access
* to Layout Builder data is currently forbidden.
*
* @see https://www.drupal.org/project/drupal/issues/2942975
*/
/**
* @} End of "defgroup layout_builder_access".
*/

View File

@@ -0,0 +1,15 @@
name: 'Layout Builder'
type: module
description: 'Allows users to add and arrange blocks and content fields directly on the content.'
package: Core
# version: VERSION
dependencies:
- drupal:layout_discovery
- drupal:contextual
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/3003610.
- drupal:block
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains install and update functions for Layout Builder.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Section;
/**
* Implements hook_install().
*/
function layout_builder_install() {
$display_changed = FALSE;
$displays = LayoutBuilderEntityViewDisplay::loadMultiple();
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
foreach ($displays as $display) {
// Create the first section from any existing Field Layout settings.
$field_layout = $display->getThirdPartySettings('field_layout');
if (isset($field_layout['id'])) {
$field_layout += ['settings' => []];
$display
->enableLayoutBuilder()
->appendSection(new Section($field_layout['id'], $field_layout['settings']))
->save();
$display_changed = TRUE;
}
}
// Clear the rendered cache to ensure the new layout builder flow is used.
// While in many cases the above change will not affect the rendered output,
// the cacheability metadata will have changed and should be processed to
// prepare for future changes.
if ($display_changed) {
Cache::invalidateTags(['rendered']);
}
}
/**
* Implements hook_schema().
*/
function layout_builder_schema() {
$schema['inline_block_usage'] = [
'description' => 'Track where a block_content entity is used.',
'fields' => [
'block_content_id' => [
'description' => 'The block_content entity ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'layout_entity_type' => [
'description' => 'The entity type of the parent entity.',
'type' => 'varchar_ascii',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'not null' => FALSE,
'default' => '',
],
'layout_entity_id' => [
'description' => 'The ID of the parent entity.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => FALSE,
'default' => 0,
],
],
'primary key' => ['block_content_id'],
'indexes' => [
'type_id' => ['layout_entity_type', 'layout_entity_id'],
],
];
return $schema;
}
/**
* Implements hook_update_last_removed().
*/
function layout_builder_update_last_removed() {
return 8602;
}

View File

@@ -0,0 +1,52 @@
layout_twocol_section:
label: 'Two column'
path: layouts/twocol_section
template: layout--twocol-section
library: layout_builder/twocol_section
class: '\Drupal\layout_builder\Plugin\Layout\TwoColumnLayout'
category: 'Columns: 2'
default_region: first
icon_map:
- [first, second]
regions:
first:
label: First
second:
label: Second
layout_threecol_section:
label: 'Three column'
path: layouts/threecol_section
template: layout--threecol-section
library: layout_builder/threecol_section
class: '\Drupal\layout_builder\Plugin\Layout\ThreeColumnLayout'
category: 'Columns: 3'
default_region: second
icon_map:
- [first, second, third]
regions:
first:
label: First
second:
label: Second
third:
label: Third
layout_fourcol_section:
label: 'Four column'
path: layouts/fourcol_section
template: layout--fourcol-section
library: layout_builder/fourcol_section
category: 'Columns: 4'
default_region: first
icon_map:
- [first, second, third, fourth]
regions:
first:
label: First
second:
label: Second
third:
label: Third
fourth:
label: Fourth

View File

@@ -0,0 +1,30 @@
drupal.layout_builder:
version: VERSION
css:
theme:
css/layout-builder.css: {}
css/off-canvas.css: {}
js:
js/layout-builder.js: {}
dependencies:
- core/sortable
- core/drupal.dialog.off_canvas
- core/drupal.announce
- core/drupal.debounce
# CSS libraries for Layout plugins.
twocol_section:
version: VERSION
css:
theme:
layouts/twocol_section/twocol_section.css: {}
threecol_section:
version: VERSION
css:
theme:
layouts/threecol_section/threecol_section.css: {}
fourcol_section:
version: VERSION
css:
theme:
layouts/fourcol_section/fourcol_section.css: {}

View File

@@ -0,0 +1,29 @@
layout_builder_block_update:
title: 'Configure'
route_name: 'layout_builder.update_block'
group: 'layout_builder_block'
options:
attributes:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas
layout_builder_block_move:
title: 'Move'
route_name: 'layout_builder.move_block_form'
group: 'layout_builder_block'
options:
attributes:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas
layout_builder_block_remove:
title: 'Remove block'
route_name: 'layout_builder.remove_block'
group: 'layout_builder_block'
options:
attributes:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas

View File

@@ -0,0 +1,2 @@
layout_builder_ui:
deriver: '\Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver'

View File

@@ -0,0 +1,457 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder.
*/
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampFormatter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Form\DefaultsEntityForm;
use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
use Drupal\layout_builder\Form\OverridesEntityForm;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Implements hook_help().
*/
function layout_builder_help($route_name, RouteMatchInterface $route_match) {
// Add help text to the Layout Builder UI.
if ($route_match->getRouteObject()->getOption('_layout_builder')) {
$output = '<p>' . t('This layout builder tool allows you to configure the layout of the main content area.') . '</p>';
if (\Drupal::currentUser()->hasPermission('administer blocks')) {
$output .= '<p>' . t('To manage other areas of the page, use the <a href="@block-ui">block administration page</a>.', ['@block-ui' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
}
else {
$output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
}
$output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
return $output;
}
switch ($route_name) {
case 'help.page.layout_builder':
$output = '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('Layout Builder allows you to use layouts to customize how content, content blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a> are displayed.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>';
$output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout-builder']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Default layouts') . '</dt>';
$output .= '<dd>' . t('Layout Builder can be selectively enabled on the "Manage Display" page in the <a href=":field_ui">Field UI</a>. This allows you to control the output of each type of display individually. For example, a "Basic page" might have view modes such as Full and Teaser, with each view mode having different layouts selected.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>';
$output .= '<dt>' . t('Overridden layouts') . '</dt>';
$output .= '<dd>' . t('If enabled, each individual content item can have a custom layout. Once the layout for an individual content item is overridden, changes to the Default layout will no longer affect it. Overridden layouts may be reverted to return to matching and being synchronized to their Default layout.') . '</dd>';
$output .= '<dt>' . t('User permissions') . '</dt>';
$output .= '<dd>' . t('The Layout Builder module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>. For more information, see the <a href=":layout-builder-permissions">Configuring Layout Builder permissions</a> online documentation.', [
':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'layout_builder'])->toString(),
':layout-builder-permissions' => 'https://www.drupal.org/docs/8/core/modules/layout-builder/configuring-layout-builder-permissions',
]) . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_entity_type_alter().
*/
function layout_builder_entity_type_alter(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
$entity_types['entity_view_display']
->setClass(LayoutBuilderEntityViewDisplay::class)
->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
->setFormClass('layout_builder', DefaultsEntityForm::class)
->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
// Ensure every fieldable entity type has a layout form.
foreach ($entity_types as $entity_type) {
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$entity_type->setFormClass('layout_builder', OverridesEntityForm::class);
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
*/
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
// Hides the Layout Builder field. It is rendered directly in
// \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
if ($key !== FALSE) {
unset($form['#fields'][$key]);
}
}
/**
* Implements hook_field_config_insert().
*/
function layout_builder_field_config_insert(FieldConfigInterface $field_config) {
// Clear the sample entity for this entity type and bundle.
$sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
$sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
/**
* Implements hook_field_config_delete().
*/
function layout_builder_field_config_delete(FieldConfigInterface $field_config) {
// Clear the sample entity for this entity type and bundle.
$sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
$sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
/**
* Implements hook_entity_view_alter().
*
* ExtraFieldBlock block plugins add placeholders for each extra field which is
* configured to be displayed. Those placeholders are replaced by this hook.
* Modules that implement hook_entity_extra_field_info() use their
* implementations of hook_entity_view_alter() to add the rendered output of
* the extra fields they provide, so we cannot get the rendered output of extra
* fields before this point in the view process.
* layout_builder_module_implements_alter() moves this implementation of
* hook_entity_view_alter() to the end of the list.
*
* @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build()
* @see layout_builder_module_implements_alter()
*/
function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
// Only replace extra fields when Layout Builder has been used to alter the
// build. See \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
if (isset($build['_layout_builder']) && !Element::isEmpty($build['_layout_builder'])) {
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
$field_manager = \Drupal::service('entity_field.manager');
$extra_fields = $field_manager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
if (!empty($extra_fields['display'])) {
foreach ($extra_fields['display'] as $field_name => $extra_field) {
// If the extra field is not set replace with an empty array to avoid
// the placeholder text from being rendered.
$replacement = $build[$field_name] ?? [];
ExtraFieldBlock::replaceFieldPlaceholder($build, $replacement, $field_name);
// After the rendered field in $build has been copied over to the
// ExtraFieldBlock block we must remove it from its original location or
// else it will be rendered twice.
unset($build[$field_name]);
}
}
}
$route_name = \Drupal::routeMatch()->getRouteName();
// If the entity is displayed within a Layout Builder block and the current
// route is in the Layout Builder UI, then remove all contextual link
// placeholders.
if ($route_name && $display instanceof LayoutBuilderEntityViewDisplay && str_starts_with($route_name, 'layout_builder.')) {
unset($build['#contextual_links']);
}
}
/**
* Implements hook_entity_build_defaults_alter().
*/
function layout_builder_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode) {
// Contextual links are removed for entities viewed in Layout Builder's UI.
// The route.name.is_layout_builder_ui cache context accounts for this
// difference.
// @see layout_builder_entity_view_alter()
// @see \Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext
$build['#cache']['contexts'][] = 'route.name.is_layout_builder_ui';
}
/**
* Implements hook_module_implements_alter().
*/
function layout_builder_module_implements_alter(&$implementations, $hook) {
if ($hook === 'entity_view_alter') {
// Ensure that this module's implementation of hook_entity_view_alter() runs
// last so that other modules that use this hook to render extra fields will
// run before it.
$group = $implementations['layout_builder'];
unset($implementations['layout_builder']);
$implementations['layout_builder'] = $group;
}
}
/**
* Implements hook_entity_presave().
*/
function layout_builder_entity_presave(EntityInterface $entity) {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->handlePreSave($entity);
}
}
/**
* Implements hook_entity_delete().
*/
function layout_builder_entity_delete(EntityInterface $entity) {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->handleEntityDelete($entity);
}
}
/**
* Implements hook_cron().
*/
function layout_builder_cron() {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->removeUnused();
}
}
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
// Remove blocks that are not useful within Layout Builder.
unset($definitions['system_messages_block']);
unset($definitions['help_block']);
unset($definitions['local_tasks_block']);
unset($definitions['local_actions_block']);
// Remove blocks that are non-functional within Layout Builder.
unset($definitions['system_main_block']);
// @todo Restore the page title block in https://www.drupal.org/node/2938129.
unset($definitions['page_title_block']);
}
/**
* Implements hook_plugin_filter_TYPE_alter().
*/
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
// @todo Determine the 'inline_block' blocks should be allowed outside
// of layout_builder https://www.drupal.org/node/2979142.
if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
foreach ($definitions as $id => $definition) {
if ($definition['id'] === 'inline_block') {
unset($definitions[$id]);
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\block_content\BlockContentInterface $entity */
if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
// If the operation is 'view' or this is reusable block or if this is
// non-reusable that isn't used by this module then don't alter the access.
return AccessResult::neutral();
}
if ($account->hasPermission('create and edit custom blocks')) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
}
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
foreach ($definitions as $id => $definition) {
// Filter out any layout_builder-provided block that has required context
// definitions.
if ($definition['provider'] === 'layout_builder' && !empty($definition['context_definitions'])) {
/** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition */
foreach ($definition['context_definitions'] as $context_definition) {
if ($context_definition->isRequired()) {
unset($definitions[$id]);
break;
}
}
}
}
}
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_plugin_filter_layout__layout_builder_alter(array &$definitions, array $extra) {
// Remove layouts provide by layout discovery that are not needed because of
// layouts provided by this module.
$duplicate_layouts = [
'layout_twocol',
'layout_twocol_bricks',
'layout_threecol_25_50_25',
'layout_threecol_33_34_33',
];
foreach ($duplicate_layouts as $duplicate_layout) {
/** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
if (isset($definitions[$duplicate_layout])) {
if ($definitions[$duplicate_layout]->getProvider() === 'layout_discovery') {
unset($definitions[$duplicate_layout]);
}
}
}
// Move the one column layout to the top.
if (isset($definitions['layout_onecol']) && $definitions['layout_onecol']->getProvider() === 'layout_discovery') {
$one_col = $definitions['layout_onecol'];
unset($definitions['layout_onecol']);
$definitions = [
'layout_onecol' => $one_col,
] + $definitions;
}
}
/**
* Implements hook_plugin_filter_TYPE_alter().
*/
function layout_builder_plugin_filter_layout_alter(array &$definitions, array $extra, $consumer) {
// Hide the blank layout plugin from listings.
unset($definitions['layout_builder_blank']);
}
/**
* Implements hook_system_breadcrumb_alter().
*/
function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
// Remove the extra 'Manage display' breadcrumb for Layout Builder defaults.
if ($route_match->getRouteObject() && $route_match->getRouteObject()->hasOption('_layout_builder') && $route_match->getParameter('section_storage_type') === 'defaults') {
$links = array_filter($breadcrumb->getLinks(), function (Link $link) use ($route_match) {
$entity_type_id = $route_match->getParameter('entity_type_id');
if (!$link->getUrl()->isRouted()) {
return TRUE;
}
return $link->getUrl()->getRouteName() !== "entity.entity_view_display.$entity_type_id.default";
});
// Links cannot be removed from an existing breadcrumb object. Create a new
// object but carry over the cacheable metadata.
$cacheability = CacheableMetadata::createFromObject($breadcrumb);
$breadcrumb = new Breadcrumb();
$breadcrumb->setLinks($links);
$breadcrumb->addCacheableDependency($cacheability);
}
}
/**
* Implements hook_entity_translation_create().
*/
function layout_builder_entity_translation_create(EntityInterface $translation) {
/** @var \Drupal\Core\Entity\FieldableEntityInterface $translation */
if ($translation->hasField(OverridesSectionStorage::FIELD_NAME) && $translation->getFieldDefinition(OverridesSectionStorage::FIELD_NAME)->isTranslatable()) {
// When creating a new translation do not copy untranslated sections because
// per-language layouts are not supported.
$translation->set(OverridesSectionStorage::FIELD_NAME, []);
}
}
/**
* Implements hook_theme_registry_alter().
*/
function layout_builder_theme_registry_alter(&$theme_registry) {
// Move our preprocess to run after
// content_translation_preprocess_language_content_settings_table().
if (!empty($theme_registry['language_content_settings_table']['preprocess functions'])) {
$preprocess_functions = &$theme_registry['language_content_settings_table']['preprocess functions'];
$index = array_search('layout_builder_preprocess_language_content_settings_table', $preprocess_functions);
if ($index !== FALSE) {
unset($preprocess_functions[$index]);
$preprocess_functions[] = 'layout_builder_preprocess_language_content_settings_table';
}
}
}
/**
* Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig.
*/
function layout_builder_preprocess_language_content_settings_table(&$variables) {
foreach ($variables['build']['#rows'] as &$row) {
if (isset($row['#field_name']) && $row['#field_name'] === OverridesSectionStorage::FIELD_NAME) {
// Rebuild the label to include a warning about using translations with
// layouts.
$row['data'][1]['data']['field'] = [
'label' => $row['data'][1]['data']['field'],
'description' => [
'#type' => 'container',
'#markup' => t('<strong>Warning</strong>: Layout Builder does not support translating layouts. (<a href="https://www.drupal.org/docs/8/core/modules/layout-builder/layout-builder-and-content-translation">online documentation</a>)'),
'#attributes' => [
'class' => ['layout-builder-translation-warning'],
],
],
];
}
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
function layout_builder_theme_suggestions_field_alter(&$suggestions, array $variables) {
$element = $variables['element'];
if (isset($element['#third_party_settings']['layout_builder']['view_mode'])) {
// See system_theme_suggestions_field().
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'] . '__' . $element['#third_party_settings']['layout_builder']['view_mode'];
}
return $suggestions;
}
/**
* Implements hook_ENTITY_TYPE_presave() for entity_view_display entities.
*
* Provides a BC layer for modules providing old configurations.
*
* @see https://www.drupal.org/node/2993639
*
* @todo Remove this BC layer in drupal:11.0.0.
*/
function layout_builder_entity_view_display_presave(EntityViewDisplayInterface $entity_view_display): void {
if (!$entity_view_display instanceof LayoutEntityDisplayInterface || !$entity_view_display->isLayoutBuilderEnabled()) {
return;
}
/** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */
$field_formatter_manager = \Drupal::service('plugin.manager.field.formatter');
foreach ($entity_view_display->getSections() as $section) {
foreach ($section->getComponents() as $component) {
if (str_starts_with($component->getPluginId(), 'field_block:')) {
$configuration = $component->get('configuration');
$formatter =& $configuration['formatter'];
if ($formatter && isset($formatter['type'])) {
$plugin_definition = $field_formatter_manager->getDefinition($formatter['type'], FALSE);
// Check also potential plugins extending TimestampFormatter.
if (!$plugin_definition || !is_a($plugin_definition['class'], TimestampFormatter::class, TRUE)) {
continue;
}
if (!isset($formatter['settings']['tooltip']) || !isset($formatter['settings']['time_diff'])) {
@trigger_error("Using the 'timestamp' formatter plugin without the 'tooltip' and 'time_diff' settings is deprecated in drupal:10.1.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/2993639", E_USER_DEPRECATED);
$formatter['settings'] += $plugin_definition['class']::defaultSettings();
// Existing timestamp formatters don't have tooltip.
$formatter['settings']['tooltip']['date_format'] = '';
$component->set('configuration', $configuration);
}
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
configure any layout:
title: 'Configure any layout'
restrict access: true
create and edit custom blocks:
title: 'Create and edit content blocks'
description: 'Manage the single-use blocks within the Layout Builder'
permission_callbacks:
- \Drupal\layout_builder\LayoutBuilderOverridesPermissions::permissions

View File

@@ -0,0 +1,78 @@
<?php
/**
* @file
* Post update functions for Layout Builder.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampFormatter;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Implements hook_removed_post_updates().
*/
function layout_builder_removed_post_updates() {
return [
'layout_builder_post_update_rebuild_plugin_dependencies' => '9.0.0',
'layout_builder_post_update_add_extra_fields' => '9.0.0',
'layout_builder_post_update_section_storage_context_definitions' => '9.0.0',
'layout_builder_post_update_overrides_view_mode_annotation' => '9.0.0',
'layout_builder_post_update_cancel_link_to_discard_changes_form' => '9.0.0',
'layout_builder_post_update_remove_layout_is_rebuilding' => '9.0.0',
'layout_builder_post_update_routing_entity_form' => '9.0.0',
'layout_builder_post_update_discover_blank_layout_plugin' => '9.0.0',
'layout_builder_post_update_routing_defaults' => '9.0.0',
'layout_builder_post_update_discover_new_contextual_links' => '9.0.0',
'layout_builder_post_update_fix_tempstore_keys' => '9.0.0',
'layout_builder_post_update_section_third_party_settings_schema' => '9.0.0',
'layout_builder_post_update_layout_builder_dependency_change' => '9.0.0',
'layout_builder_post_update_update_permissions' => '9.0.0',
'layout_builder_post_update_make_layout_untranslatable' => '9.0.0',
'layout_builder_post_update_override_entity_form_controller' => '10.0.0',
'layout_builder_post_update_section_storage_context_mapping' => '10.0.0',
'layout_builder_post_update_tempstore_route_enhancer' => '10.0.0',
];
}
/**
* Update timestamp formatter settings for Layout Builder fields.
*/
function layout_builder_post_update_timestamp_formatter(?array &$sandbox = NULL): void {
/** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */
$field_formatter_manager = \Drupal::service('plugin.manager.field.formatter');
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (EntityViewDisplayInterface $entity_view_display) use ($field_formatter_manager): bool {
$update = FALSE;
if ($entity_view_display instanceof LayoutEntityDisplayInterface && $entity_view_display->isLayoutBuilderEnabled()) {
foreach ($entity_view_display->getSections() as $section) {
foreach ($section->getComponents() as $component) {
if (str_starts_with($component->getPluginId(), 'field_block:')) {
$configuration = $component->get('configuration');
$formatter =& $configuration['formatter'];
if ($formatter && isset($formatter['type'])) {
$plugin_definition = $field_formatter_manager->getDefinition($formatter['type'], FALSE);
// Check also potential plugins extending TimestampFormatter.
if ($plugin_definition && is_a($plugin_definition['class'], TimestampFormatter::class, TRUE)) {
if (!isset($formatter['settings']['tooltip']) || !isset($formatter['settings']['time_diff'])) {
$update = TRUE;
// No need to check the rest of components.
break 2;
}
}
}
}
}
}
}
return $update;
});
}
/**
* Enable the expose all fields feature flag module.
*/
function layout_builder_post_update_enable_expose_field_block_feature_flag(): void {
\Drupal::service('module_installer')->install(['layout_builder_expose_all_field_blocks']);
}

View File

@@ -0,0 +1,147 @@
layout_builder.choose_section:
path: '/layout_builder/choose/section/{section_storage_type}/{section_storage}/{delta}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
_title: 'Choose a layout for this section'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.add_section:
path: '/layout_builder/add/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
defaults:
_controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.configure_section:
path: '/layout_builder/configure/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
defaults:
_title: 'Configure section'
_form: '\Drupal\layout_builder\Form\ConfigureSectionForm'
# Adding a new section requires a plugin_id, while configuring an existing
# section does not.
plugin_id: null
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.remove_section:
path: '/layout_builder/remove/section/{section_storage_type}/{section_storage}/{delta}'
defaults:
_form: '\Drupal\layout_builder\Form\RemoveSectionForm'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.choose_block:
path: '/layout_builder/choose/block/{section_storage_type}/{section_storage}/{delta}/{region}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build'
_title: 'Choose a block'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.add_block:
path: '/layout_builder/add/block/{section_storage_type}/{section_storage}/{delta}/{region}/{plugin_id}'
defaults:
_form: '\Drupal\layout_builder\Form\AddBlockForm'
_title: 'Configure block'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.choose_inline_block:
path: '/layout_builder/choose/inline-block/{section_storage_type}/{section_storage}/{delta}/{region}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
_title: 'Add a new content block'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.update_block:
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:
_form: '\Drupal\layout_builder\Form\UpdateBlockForm'
_title: 'Configure block'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.move_block_form:
path: '/layout_builder/move/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:
_title_callback: '\Drupal\layout_builder\Form\MoveBlockForm::title'
_form: '\Drupal\layout_builder\Form\MoveBlockForm'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.remove_block:
path: '/layout_builder/remove/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:
_form: '\Drupal\layout_builder\Form\RemoveBlockForm'
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.move_block:
path: '/layout_builder/move/block/{section_storage_type}/{section_storage}/{delta_from}/{delta_to}/{region_to}/{block_uuid}/{preceding_block_uuid}'
defaults:
_controller: '\Drupal\layout_builder\Controller\MoveBlockController::build'
delta_from: null
delta_to: null
region_from: null
region_to: null
block_uuid: null
preceding_block_uuid: null
requirements:
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE

View File

@@ -0,0 +1,70 @@
services:
_defaults:
autoconfigure: true
layout_builder.tempstore_repository:
class: Drupal\layout_builder\LayoutTempstoreRepository
arguments: ['@tempstore.shared']
Drupal\layout_builder\LayoutTempstoreRepositoryInterface: '@layout_builder.tempstore_repository'
access_check.entity.layout_builder_access:
class: Drupal\layout_builder\Access\LayoutBuilderAccessCheck
tags:
- { name: access_check, applies_to: _layout_builder_access }
plugin.manager.layout_builder.section_storage:
class: Drupal\layout_builder\SectionStorage\SectionStorageManager
parent: default_plugin_manager
arguments: ['@context.handler']
Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface: '@plugin.manager.layout_builder.section_storage'
layout_builder.routes:
class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
arguments: ['@plugin.manager.layout_builder.section_storage']
layout_builder.tempstore.route_enhancer:
class: Drupal\layout_builder\Routing\LayoutTempstoreRouteEnhancer
arguments: ['@layout_builder.tempstore_repository']
tags:
- { name: route_enhancer }
layout_builder.param_converter:
class: Drupal\layout_builder\Routing\LayoutSectionStorageParamConverter
arguments: ['@plugin.manager.layout_builder.section_storage']
tags:
- { name: paramconverter, priority: 10 }
cache_context.layout_builder_is_active:
class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context}
cache_context.route.name.is_layout_builder_ui:
class: Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context }
layout_builder.extra_fields.invalidator:
class: Drupal\layout_builder\Cache\ExtraFieldBlockCacheTagInvalidator
arguments: ['@plugin.manager.block']
public: false
tags:
- { name: cache_tags_invalidator }
layout_builder.sample_entity_generator:
class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator
arguments: ['@tempstore.shared', '@entity_type.manager']
Drupal\layout_builder\Entity\SampleEntityGeneratorInterface: '@layout_builder.sample_entity_generator'
layout_builder.render_block_component_subscriber:
class: Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray
arguments: ['@current_user']
logger.channel.layout_builder:
parent: logger.channel_base
arguments: ['layout_builder']
inline_block.usage:
class: Drupal\layout_builder\InlineBlockUsage
arguments: ['@database']
Drupal\layout_builder\InlineBlockUsageInterface: '@inline_block.usage'
layout_builder.controller.entity_form:
# Override the entity form controller to handle the entity layout_builder
# operation.
decorates: controller.entity_form
class: Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
public: false
arguments: ['@layout_builder.controller.entity_form.inner']
Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController: '@layout_builder.controller.entity_form'
layout_builder.element.prepare_layout:
class: Drupal\layout_builder\EventSubscriber\PrepareLayout
arguments: ['@layout_builder.tempstore_repository', '@messenger']

View File

@@ -0,0 +1,19 @@
/*
* @file
* Provides the layout styles for four-column layout section.
*/
.layout--fourcol-section {
display: flex;
flex-wrap: wrap;
}
.layout--fourcol-section > .layout__region {
flex: 0 1 100%;
}
@media screen and (min-width: 40em) {
.layout--fourcol-section > .layout__region {
flex: 0 1 25%;
}
}

View File

@@ -0,0 +1,48 @@
{#
/**
* @file
* Default theme implementation for a four-column 25%-25%-25%-25% layout.
*
* Available variables:
* - in_preview: Whether the plugin is being rendered in preview mode.
* - content: The content for this layout.
* - attributes: HTML attributes for the layout <div>.
*
* @ingroup themeable
*/
#}
{%
set classes = [
'layout',
'layout--fourcol-section',
]
%}
{% if content %}
<div{{ attributes.addClass(classes) }}>
{% if content.first %}
<div {{ region_attributes.first.addClass('layout__region', 'layout__region--first') }}>
{{ content.first }}
</div>
{% endif %}
{% if content.second %}
<div {{ region_attributes.second.addClass('layout__region', 'layout__region--second') }}>
{{ content.second }}
</div>
{% endif %}
{% if content.third %}
<div {{ region_attributes.third.addClass('layout__region', 'layout__region--third') }}>
{{ content.third }}
</div>
{% endif %}
{% if content.fourth %}
<div {{ region_attributes.fourth.addClass('layout__region', 'layout__region--fourth') }}>
{{ content.fourth }}
</div>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,36 @@
{#
/**
* @file
* Default theme implementation for a three-column layout.
*
* Available variables:
* - in_preview: Whether the plugin is being rendered in preview mode.
* - content: The content for this layout.
* - attributes: HTML attributes for the layout <div>.
*
* @ingroup themeable
*/
#}
{% if content %}
<div{{ attributes.addClass(classes) }}>
{% if content.first %}
<div {{ region_attributes.first.addClass('layout__region', 'layout__region--first') }}>
{{ content.first }}
</div>
{% endif %}
{% if content.second %}
<div {{ region_attributes.second.addClass('layout__region', 'layout__region--second') }}>
{{ content.second }}
</div>
{% endif %}
{% if content.third %}
<div {{ region_attributes.third.addClass('layout__region', 'layout__region--third') }}>
{{ content.third }}
</div>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,36 @@
/*
* @file
* Provides the layout styles for three-column layout section.
*/
.layout--threecol-section {
display: flex;
flex-wrap: wrap;
}
.layout--threecol-section > .layout__region {
flex: 0 1 100%;
}
@media screen and (min-width: 40em) {
.layout--threecol-section--25-50-25 > .layout__region--first,
.layout--threecol-section--25-50-25 > .layout__region--third,
.layout--threecol-section--25-25-50 > .layout__region--first,
.layout--threecol-section--25-25-50 > .layout__region--second,
.layout--threecol-section--50-25-25 > .layout__region--second,
.layout--threecol-section--50-25-25 > .layout__region--third {
flex: 0 1 25%;
}
.layout--threecol-section--25-50-25 > .layout__region--second,
.layout--threecol-section--25-25-50 > .layout__region--third,
.layout--threecol-section--50-25-25 > .layout__region--first {
flex: 0 1 50%;
}
.layout--threecol-section--33-34-33 > .layout__region--first,
.layout--threecol-section--33-34-33 > .layout__region--third {
flex: 0 1 33%;
}
.layout--threecol-section--33-34-33 > .layout__region--second {
flex: 0 1 34%;
}
}

View File

@@ -0,0 +1,30 @@
{#
/**
* @file
* Default theme implementation to display a two-column layout.
*
* Available variables:
* - in_preview: Whether the plugin is being rendered in preview mode.
* - content: The content for this layout.
* - attributes: HTML attributes for the layout <div>.
*
* @ingroup themeable
*/
#}
{% if content %}
<div{{ attributes }}>
{% if content.first %}
<div {{ region_attributes.first.addClass('layout__region', 'layout__region--first') }}>
{{ content.first }}
</div>
{% endif %}
{% if content.second %}
<div {{ region_attributes.second.addClass('layout__region', 'layout__region--second') }}>
{{ content.second }}
</div>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,40 @@
/*
* @file
* Provides the layout styles for two-column layout section.
*/
.layout--twocol-section {
display: flex;
flex-wrap: wrap;
}
.layout--twocol-section > .layout__region {
flex: 0 1 100%;
}
@media screen and (min-width: 40em) {
.layout--twocol-section.layout--twocol-section--50-50 > .layout__region--first,
.layout--twocol-section.layout--twocol-section--50-50 > .layout__region--second {
flex: 0 1 50%;
}
.layout--twocol-section.layout--twocol-section--33-67 > .layout__region--first,
.layout--twocol-section.layout--twocol-section--67-33 > .layout__region--second {
flex: 0 1 33%;
}
.layout--twocol-section.layout--twocol-section--33-67 > .layout__region--second,
.layout--twocol-section.layout--twocol-section--67-33 > .layout__region--first {
flex: 0 1 67%;
}
.layout--twocol-section.layout--twocol-section--25-75 > .layout__region--first,
.layout--twocol-section.layout--twocol-section--75-25 > .layout__region--second {
flex: 0 1 25%;
}
.layout--twocol-section.layout--twocol-section--25-75 > .layout__region--second,
.layout--twocol-section.layout--twocol-section--75-25 > .layout__region--first {
flex: 0 1 75%;
}
}

View File

@@ -0,0 +1,14 @@
name: 'Layout Builder Expose All Field Blocks'
type: module
description: 'When enabled, this module exposes all fields for all entity view displays. When disabled, only entity type bundles that have layout builder enabled will have their fields exposed. Enabling this module could significantly decrease performance on sites with a large number of entity types and bundles.'
package: Core
# version: VERSION
dependencies:
- drupal:layout_builder
lifecycle: deprecated
lifecycle_link: "https://www.drupal.org/node/3223395#s-layout-builder-expose-all-field-blocks"
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Module implementation file.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function layout_builder_expose_all_field_blocks_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.layout_builder_expose_all_field_blocks':
$output = '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Layout Builder Expose All Field Blocks module is a Feature Flag module which, when enabled, exposes all fields on all bundles as field blocks for use in Layout Builder.') . '</p>';
$output .= '<p>' . t('Leaving this module enabled can significantly affect the performance of medium to large sites due to the number of Field Block plugins that will be created. It is recommended to turn it off if possible.') . '</p>';
$output .= '<p>' . t('While it is recommended to turn this module off, note that this may remove blocks that are already being used in existing site configurations.') . '</p>';
$output .= '<p>' . t("For example, if you have Layout Builder enabled on a Node bundle (Content type), and that bundle's display is using field blocks from the User entity (e.g the Author's name), but Layout Builder is not enabled for the User bundle, then that field block would no longer exist after disabling this module.") . '</p>';
$output .= '<p>' . t('For more information, see the <a href=":href">online documentation for the Layout Builder Expose All Field Blocks module</a>.', [':href' => 'https://www.drupal.org/node/3223395#s-layout-builder-expose-all-field-blocks']) . '</p>';
return $output;
}
return NULL;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder_expose_all_field_blocks\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for layout_builder_expose_all_field_blocks.
*
* @group layout_builder_expose_all_field_blocks
* @group legacy
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access check for the Layout Builder defaults.
*
* @ingroup layout_builder_access
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderAccessCheck implements AccessInterface {
/**
* Checks routing access to the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(SectionStorageInterface $section_storage, AccountInterface $account, Route $route) {
$operation = $route->getRequirement('_layout_builder_access');
$access = $section_storage->access($operation, $account, TRUE);
// Check for the global permission unless the section storage checks
// permissions itself.
if (!$section_storage->getPluginDefinition()->get('handles_permission_check')) {
$access = $access->andIf(AccessResult::allowedIfHasPermission($account, 'configure any layout'));
}
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($section_storage);
}
return $access;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Accessible class to allow access for inline blocks in the Layout Builder.
*
* @internal
* Tagged services are internal.
*/
class LayoutPreviewAccessAllowed implements AccessibleInterface {
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'view') {
return $return_as_object ? AccessResult::allowed() : TRUE;
}
// The layout builder preview should only need 'view' access.
return $return_as_object ? AccessResult::forbidden() : FALSE;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\layout_builder\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
/**
* Defines a Section Storage type annotation object.
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManager
* @see plugin_api
*
* @Annotation
*/
class SectionStorage extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin weight, optional (defaults to 0).
*
* When an entity with layout is rendered, section storage plugins are
* checked, in order of their weight, to determine which one should be used
* to render the layout.
*
* @var int
*/
public $weight = 0;
/**
* Any required context definitions, optional.
*
* When an entity with layout is rendered, all section storage plugins which
* match a particular set of contexts are checked, in order of their weight,
* to determine which plugin should be used to render the layout.
*
* @var array
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
*/
public $context_definitions = [];
/**
* Indicates that this section storage handles its own permission checking.
*
* If FALSE, the 'configure any layout' permission will be required during
* routing access. If TRUE, Layout Builder will not enforce any access
* restrictions for the storage, so the section storage's implementation of
* access() must perform the access checking itself. Defaults to FALSE.
*
* @var bool
*
* @see \Drupal\layout_builder\Access\LayoutBuilderAccessCheck
*/
public $handles_permission_check = FALSE;
/**
* {@inheritdoc}
*/
public function get() {
return new SectionStorageDefinition($this->definition);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\layout_builder\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
/**
* Defines a SectionStorage attribute.
*
* Plugin Namespace: Plugin\SectionStorage
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManager
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class SectionStorage extends Plugin {
/**
* Constructs a SectionStorage attribute.
*
* @param string $id
* The plugin ID.
* @param int $weight
* (optional) The plugin weight.
* When an entity with layout is rendered, section storage plugins are
* checked, in order of their weight, to determine which one should be used
* to render the layout.
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface[] $context_definitions
* (optional) Any required context definitions.
* When an entity with layout is rendered, all section storage plugins which
* match a particular set of contexts are checked, in order of their weight,
* to determine which plugin should be used to render the layout.
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
* @param bool $handles_permission_check
* (optional) Indicates that this section storage handles its own
* permission checking. If FALSE, the 'configure any layout' permission
* will be required during routing access. If TRUE, Layout Builder will
* not enforce any access restrictions for the storage, so the section
* storage's implementation of access() must perform the access checking itself.
* @param string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly int $weight = 0,
public readonly array $context_definitions = [],
public readonly bool $handles_permission_check = FALSE,
public readonly ?string $deriver = NULL,
) {}
/**
* {@inheritdoc}
*/
public function get(): SectionStorageDefinition {
return new SectionStorageDefinition([
'id' => $this->id,
'class' => $this->class,
'weight' => $this->weight,
'context_definitions' => $this->context_definitions,
'handles_permission_check' => $this->handles_permission_check,
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
/**
* Provides a cache tag invalidator that clears the block cache.
*
* @internal
* Tagged services are internal.
*/
class ExtraFieldBlockCacheTagInvalidator implements CacheTagsInvalidatorInterface {
/**
* Constructs a new ExtraFieldBlockCacheTagInvalidator.
*
* @param \Drupal\Core\Block\BlockManagerInterface $blockManager
* The block manager.
*/
public function __construct(protected BlockManagerInterface $blockManager) {
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
if (in_array('entity_field_info', $tags, TRUE)) {
if ($this->blockManager instanceof CachedDiscoveryInterface) {
$this->blockManager->clearCachedDefinitions();
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Determines whether Layout Builder is active for a given entity type or not.
*
* Cache context ID: 'layout_builder_is_active:%entity_type_id', e.g.
* 'layout_builder_is_active:node' (to vary by whether custom layout overrides
* are allowed for the Node entity specified by the route parameter).
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* LayoutBuilderCacheContext constructor.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Layout Builder');
}
/**
* {@inheritdoc}
*/
public function getContext($entity_type_id = NULL) {
if (!$entity_type_id) {
throw new \LogicException('Missing entity type ID');
}
$display = $this->getDisplay($entity_type_id);
return ($display && $display->isOverridable()) ? '1' : '0';
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($entity_type_id = NULL) {
if (!$entity_type_id) {
throw new \LogicException('Missing entity type ID');
}
$cacheable_metadata = new CacheableMetadata();
if ($display = $this->getDisplay($entity_type_id)) {
$cacheable_metadata->addCacheableDependency($display);
}
return $cacheable_metadata;
}
/**
* Returns the entity view display for a given entity type and view mode.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface|null
* The entity view display, if it exists.
*/
protected function getDisplay($entity_type_id) {
if ($entity = $this->routeMatch->getParameter($entity_type_id)) {
if ($entity instanceof FieldableEntityInterface) {
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
$view_mode = 'full';
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
if ($display instanceof LayoutEntityDisplayInterface) {
return $display;
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\layout_builder\Cache;
use Drupal\Core\Cache\Context\RouteNameCacheContext;
/**
* Determines if an entity is being viewed in the Layout Builder UI.
*
* Cache context ID: 'route.name.is_layout_builder_ui'.
*
* @internal
* Tagged services are internal.
*/
class LayoutBuilderUiCacheContext extends RouteNameCacheContext {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Layout Builder user interface');
}
/**
* {@inheritdoc}
*/
public function getContext() {
$route_name = $this->routeMatch->getRouteName();
if ($route_name && str_starts_with($route_name, 'layout_builder.')) {
return 'is_layout_builder_ui.0';
}
return 'is_layout_builder_ui.1';
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\layout_builder\Context;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a wrapper around getting contexts from a section storage object.
*/
trait LayoutBuilderContextTrait {
/**
* The context repository.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* Gets the context repository service.
*
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
* The context repository service.
*/
protected function contextRepository() {
if (!$this->contextRepository) {
$this->contextRepository = \Drupal::service('context.repository');
}
return $this->contextRepository;
}
/**
* Returns all populated contexts, both global and section-storage-specific.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The array of context objects.
*/
protected function getPopulatedContexts(SectionStorageInterface $section_storage): array {
// Get all known globally available contexts IDs.
$available_context_ids = array_keys($this->contextRepository()->getAvailableContexts());
// Filter to those that are populated.
$contexts = array_filter($this->contextRepository()->getRuntimeContexts($available_context_ids), function (ContextInterface $context) {
return $context->hasContextValue();
});
// Add in the per-section_storage contexts.
$contexts += $section_storage->getContextsDuringPreview();
return $contexts;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Defines a controller to add a new section.
*
* @internal
* Controller classes are internal.
*/
class AddSectionController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutRebuildTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* AddSectionController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* Adds the new section.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $plugin_id
* The plugin ID of the layout to add.
*
* @return \Symfony\Component\HttpFoundation\Response
* The controller response.
*/
public function build(SectionStorageInterface $section_storage, int $delta, $plugin_id) {
$section_storage->insertSection($delta, new Section($plugin_id));
$this->layoutTempstoreRepository->set($section_storage);
if ($this->isAjax()) {
return $this->rebuildAndClose($section_storage);
}
else {
$url = $section_storage->getLayoutBuilderUrl();
return new RedirectResponse($url->setAbsolute()->toString());
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to choose a new block.
*
* @internal
* Controller classes are internal.
*/
class ChooseBlockController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* ChooseBlockController constructor.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block'),
$container->get('entity_type.manager'),
$container->get('current_user')
);
}
/**
* Provides the UI for choosing a new block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function build(SectionStorageInterface $section_storage, int $delta, $region) {
if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
if (count($types) === 1) {
$type = reset($types);
$plugin_id = 'inline_block:' . $type->id();
if ($this->blockManager->hasDefinition($plugin_id)) {
$url = Url::fromRoute('layout_builder.add_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $plugin_id,
]);
}
}
else {
$url = Url::fromRoute('layout_builder.choose_inline_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]);
}
if (isset($url)) {
$build['add_block'] = [
'#type' => 'link',
'#url' => $url,
'#title' => $this->t('Create @entity_type', [
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
]),
'#attributes' => $this->getAjaxAttributes(),
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
}
}
$build['filter'] = [
'#type' => 'search',
'#title' => $this->t('Filter by block name'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => $this->t('Filter by block name'),
'#attributes' => [
'class' => ['js-layout-builder-filter'],
'title' => $this->t('Enter a part of the block name to filter by.'),
],
];
$block_categories['#type'] = 'container';
$block_categories['#attributes']['class'][] = 'block-categories';
$block_categories['#attributes']['class'][] = 'js-layout-builder-categories';
$block_categories['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), [
'section_storage' => $section_storage,
'delta' => $delta,
'region' => $region,
]);
$grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
foreach ($grouped_definitions as $category => $blocks) {
$block_categories[$category]['#type'] = 'details';
$block_categories[$category]['#attributes']['class'][] = 'js-layout-builder-category';
$block_categories[$category]['#open'] = TRUE;
$block_categories[$category]['#title'] = $category;
$block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
}
$build['block_categories'] = $block_categories;
return $build;
}
/**
* Provides the UI for choosing a new inline block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function inlineBlockList(SectionStorageInterface $section_storage, int $delta, $region) {
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), [
'section_storage' => $section_storage,
'region' => $region,
'list' => 'inline_blocks',
]);
$blocks = $this->blockManager->getGroupedDefinitions($definitions);
$build = [];
$inline_blocks_category = (string) $this->t('Inline blocks');
if (isset($blocks[$inline_blocks_category])) {
$build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks[$inline_blocks_category]);
$build['links']['#attributes']['class'][] = 'inline-block-list';
foreach ($build['links']['#links'] as &$link) {
$link['attributes']['class'][] = 'inline-block-list__item';
}
$build['back_button'] = [
'#type' => 'link',
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]
),
'#title' => $this->t('Back'),
'#attributes' => $this->getAjaxAttributes(),
];
}
$build['links']['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $build;
}
/**
* Gets a render array of block links.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
* @param array $blocks
* The information for each block.
*
* @return array
* The block links render array.
*/
protected function getBlockLinks(SectionStorageInterface $section_storage, int $delta, $region, array $blocks) {
$links = [];
foreach ($blocks as $block_id => $block) {
$attributes = $this->getAjaxAttributes();
$attributes['class'][] = 'js-layout-builder-block-link';
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
'attributes' => $attributes,
];
$links[] = $link;
}
return [
'#theme' => 'links',
'#links' => $links,
];
}
/**
* Get dialog attributes if an ajax request.
*
* @return array
* The attributes array.
*/
protected function getAjaxAttributes() {
if ($this->isAjax()) {
return [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
];
}
return [];
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to choose a new section.
*
* @internal
* Controller classes are internal.
*/
class ChooseSectionController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
* The layout manager.
*
* @var \Drupal\Core\Layout\LayoutPluginManagerInterface
*/
protected $layoutManager;
/**
* ChooseSectionController constructor.
*
* @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
* The layout manager.
*/
public function __construct(LayoutPluginManagerInterface $layout_manager) {
$this->layoutManager = $layout_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.core.layout')
);
}
/**
* Choose a layout plugin to add as a section.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
*
* @return array
* The render array.
*/
public function build(SectionStorageInterface $section_storage, int $delta) {
$items = [];
$definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', $this->getPopulatedContexts($section_storage), ['section_storage' => $section_storage]);
foreach ($definitions as $plugin_id => $definition) {
$layout = $this->layoutManager->createInstance($plugin_id);
$item = [
'#type' => 'link',
'#title' => [
'icon' => $definition->getIcon(60, 80, 1, 3),
'label' => [
'#type' => 'container',
'#children' => $definition->getLabel(),
],
],
'#url' => Url::fromRoute(
$layout instanceof PluginFormInterface ? 'layout_builder.configure_section' : 'layout_builder.add_section',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'plugin_id' => $plugin_id,
]
),
];
if ($this->isAjax()) {
$item['#attributes']['class'][] = 'use-ajax';
$item['#attributes']['data-dialog-type'][] = 'dialog';
$item['#attributes']['data-dialog-renderer'][] = 'off_canvas';
}
$items[$plugin_id] = $item;
}
$output['layouts'] = [
'#theme' => 'item_list__layouts',
'#items' => $items,
'#attributes' => [
'class' => [
'layout-selection',
],
'data-layout-builder-target-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
return $output;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Component\Assertion\Inspector;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Defines a controller to provide the Layout Builder admin UI.
*
* @internal
* Controller classes are internal.
*/
class LayoutBuilderController {
use StringTranslationTrait;
/**
* Provides a title callback.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return string
* The title for the layout page.
*/
public function title(SectionStorageInterface $section_storage) {
assert(Inspector::assertStringable($section_storage->label()), 'Section storage label is expected to be a string.');
return $this->t('Edit layout for %label', ['%label' => $section_storage->label() ?? $section_storage->getStorageType() . ' ' . $section_storage->getStorageId()]);
}
/**
* Renders the Layout UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return array
* A render array.
*/
public function layout(SectionStorageInterface $section_storage) {
return [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Controller\FormController;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Overrides the entity form controller service for layout builder operations.
*/
class LayoutBuilderHtmlEntityFormController extends FormController {
use DependencySerializationTrait;
/**
* The entity form controller being decorated.
*
* @var \Drupal\Core\Controller\FormController
*/
protected $entityFormController;
/**
* Constructs a LayoutBuilderHtmlEntityFormController object.
*
* @param \Drupal\Core\Controller\FormController $entity_form_controller
* The entity form controller being decorated.
*/
public function __construct(FormController $entity_form_controller) {
$this->entityFormController = $entity_form_controller;
}
/**
* {@inheritdoc}
*/
public function getContentResult(Request $request, RouteMatchInterface $route_match) {
$form = $this->entityFormController->getContentResult($request, $route_match);
// If the form render element has a #layout_builder_element_keys property,
// first set the form element as a child of the root render array. Use the
// keys to get the layout builder element from the form render array and
// copy it to a separate child element of the root element to prevent any
// forms within the layout builder element from being nested.
if (isset($form['#layout_builder_element_keys'])) {
$build['form'] = &$form;
$layout_builder_element = &NestedArray::getValue($form, $form['#layout_builder_element_keys']);
$build['layout_builder'] = $layout_builder_element;
// Remove the layout builder element within the form.
$layout_builder_element = [];
return $build;
}
// If no #layout_builder_element_keys property, return form as is.
return $form;
}
/**
* {@inheritdoc}
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
return $this->entityFormController->getFormArgument($route_match);
}
/**
* {@inheritdoc}
*/
protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
return $this->entityFormController->getFormObject($route_match, $form_arg);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides AJAX responses to rebuild the Layout Builder.
*/
trait LayoutRebuildTrait {
/**
* Rebuilds the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to either rebuild the layout and close the dialog, or
* reload the page.
*/
protected function rebuildAndClose(SectionStorageInterface $section_storage) {
$response = $this->rebuildLayout($section_storage);
$response->addCommand(new CloseDialogCommand('#drupal-off-canvas'));
return $response;
}
/**
* Rebuilds the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to either rebuild the layout and close the dialog, or
* reload the page.
*/
protected function rebuildLayout(SectionStorageInterface $section_storage) {
$response = new AjaxResponse();
$layout = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
$response->addCommand(new ReplaceCommand('#layout-builder', $layout));
return $response;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a controller to move a block.
*
* @internal
* Controller classes are internal.
*/
class MoveBlockController implements ContainerInjectionInterface {
use LayoutRebuildTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* LayoutController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* Moves a block to another region.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta_from
* The delta of the original section.
* @param int $delta_to
* The delta of the destination section.
* @param string $region_to
* The new region for this block.
* @param string $block_uuid
* The UUID for this block.
* @param string|null $preceding_block_uuid
* (optional) If provided, the UUID of the block to insert this block after.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response.
*/
public function build(SectionStorageInterface $section_storage, int $delta_from, int $delta_to, $region_to, $block_uuid, $preceding_block_uuid = NULL) {
$section = $section_storage->getSection($delta_from);
$component = $section->getComponent($block_uuid);
$section->removeComponent($block_uuid);
// If the block is moving from one section to another, update the original
// section and load the new one.
if ($delta_from !== $delta_to) {
$section = $section_storage->getSection($delta_to);
}
// If a preceding block was specified, insert after that. Otherwise add the
// block to the front.
$component->setRegion($region_to);
if (isset($preceding_block_uuid)) {
$section->insertAfterComponent($preceding_block_uuid, $component);
}
else {
$section->insertComponent(0, $component);
}
$this->layoutTempstoreRepository->set($section_storage);
return $this->rebuildLayout($section_storage);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
/**
* Defines an interface for an object that stores layout sections for defaults.
*/
interface DefaultsSectionStorageInterface extends SectionStorageInterface, ThirdPartySettingsInterface, LayoutBuilderEnabledInterface, LayoutBuilderOverridableInterface {}

View File

@@ -0,0 +1,382 @@
<?php
namespace Drupal\layout_builder\Element;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines a render element for building the Layout Builder UI.
*
* @internal
* Plugin classes are internal.
*/
#[RenderElement('layout_builder')]
class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs a new LayoutBuilder.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('event_dispatcher')
);
}
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#section_storage' => NULL,
'#pre_render' => [
[$this, 'preRender'],
],
];
}
/**
* Pre-render callback: Renders the Layout Builder UI.
*/
public function preRender($element) {
if ($element['#section_storage'] instanceof SectionStorageInterface) {
$element['layout_builder'] = $this->layout($element['#section_storage']);
}
return $element;
}
/**
* Renders the Layout UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return array
* A render array.
*/
protected function layout(SectionStorageInterface $section_storage) {
$this->prepareLayout($section_storage);
$output = [];
if ($this->isAjax()) {
$output['status_messages'] = [
'#type' => 'status_messages',
];
}
$count = 0;
for ($i = 0; $i < $section_storage->count(); $i++) {
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output[] = $this->buildAdministrativeSection($section_storage, $count);
$count++;
}
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
// As the Layout Builder UI is typically displayed using the frontend theme,
// it is not marked as an administrative page at the route level even though
// it performs an administrative task. Mark this as an administrative page
// for JavaScript.
$output['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
$output['#type'] = 'container';
$output['#attributes']['id'] = 'layout-builder';
$output['#attributes']['class'][] = 'layout-builder';
// Mark this UI as uncacheable.
$output['#cache']['max-age'] = 0;
return $output;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*/
protected function prepareLayout(SectionStorageInterface $section_storage) {
$event = new PrepareLayoutEvent($section_storage);
$this->eventDispatcher->dispatch($event, LayoutBuilderEvents::PREPARE_LAYOUT);
}
/**
* Builds a link to add a new section at a given delta.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
*
* @return array
* A render array for a link.
*/
protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
// If the delta and the count are the same, it is either the end of the
// layout or an empty layout.
if ($delta === count($section_storage)) {
if ($delta === 0) {
$title = $this->t('Add section');
}
else {
$title = $this->t('Add section <span class="visually-hidden">at end of layout</span>');
}
}
// If the delta and the count are different, it is either the beginning of
// the layout or in between two sections.
else {
if ($delta === 0) {
$title = $this->t('Add section <span class="visually-hidden">at start of layout</span>');
}
else {
$title = $this->t('Add section <span class="visually-hidden">between @first and @second</span>', ['@first' => $delta, '@second' => $delta + 1]);
}
}
return [
'link' => [
'#type' => 'link',
'#title' => $title,
'#url' => Url::fromRoute('layout_builder.choose_section',
[
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
],
[
'attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--add',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
]
),
],
'#type' => 'container',
'#attributes' => [
'class' => ['layout-builder__add-section'],
'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
}
/**
* Builds the render array for the layout section while editing.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section.
*
* @return array
* The render array for a given section.
*/
protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
$section = $section_storage->getSection($delta);
$layout = $section->getLayout($this->getPopulatedContexts($section_storage));
$layout_settings = $section->getLayoutSettings();
$section_label = !empty($layout_settings['label']) ? $layout_settings['label'] : $this->t('Section @section', ['@section' => $delta + 1]);
$build = $section->toRenderArray($this->getPopulatedContexts($section_storage), TRUE);
$layout_definition = $layout->getPluginDefinition();
$region_labels = $layout_definition->getRegionLabels();
foreach ($layout_definition->getRegions() as $region => $info) {
if (!empty($build[$region])) {
foreach (Element::children($build[$region]) as $uuid) {
$build[$region][$uuid]['#attributes']['class'][] = 'js-layout-builder-block';
$build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block';
$build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
$build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid);
$build[$region][$uuid]['#contextual_links'] = [
'layout_builder_block' => [
'route_parameters' => [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
'uuid' => $uuid,
],
// Add metadata about the current operations available in
// contextual links. This will invalidate the client-side cache of
// links that were cached before the 'move' link was added.
// @see layout_builder.links.contextual.yml
'metadata' => [
'operations' => 'move:update:remove',
],
],
];
}
}
$build[$region]['layout_builder_add_block']['link'] = [
'#type' => 'link',
// Add one to the current delta since it is zero-indexed.
'#title' => $this->t('Add block <span class="visually-hidden">in @section, @region region</span>', ['@section' => $section_label, '@region' => $region_labels[$region]]),
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
],
[
'attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--add',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
]
),
];
$build[$region]['layout_builder_add_block']['#type'] = 'container';
$build[$region]['layout_builder_add_block']['#attributes'] = [
'class' => ['layout-builder__add-block'],
'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region),
];
$build[$region]['layout_builder_add_block']['#weight'] = 1000;
$build[$region]['#attributes']['data-region'] = $region;
$build[$region]['#attributes']['class'][] = 'layout-builder__region';
$build[$region]['#attributes']['class'][] = 'js-layout-builder-region';
$build[$region]['#attributes']['role'] = 'group';
$build[$region]['#attributes']['aria-label'] = $this->t('@region region in @section', [
'@region' => $info['label'],
'@section' => $section_label,
]);
// Get weights of all children for use by the region label.
$weights = array_map(function ($a) {
return $a['#weight'] ?? 0;
}, $build[$region]);
// The region label is made visible when the move block dialog is open.
$build[$region]['region_label'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['layout__region-info', 'layout-builder__region-label'],
// A more detailed version of this information is already read by
// screen readers, so this label can be hidden from them.
'aria-hidden' => TRUE,
],
'#markup' => $this->t('Region: @region', ['@region' => $info['label']]),
// Ensures the region label is displayed first.
'#weight' => min($weights) - 1,
];
}
$build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
])->toString();
$build['#attributes']['data-layout-delta'] = $delta;
$build['#attributes']['class'][] = 'layout-builder__layout';
$build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta);
return [
'#type' => 'container',
'#attributes' => [
'class' => ['layout-builder__section'],
'role' => 'group',
'aria-label' => $section_label,
],
'remove' => [
'#type' => 'link',
'#title' => $this->t('Remove @section', ['@section' => $section_label]),
'#url' => Url::fromRoute('layout_builder.remove_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--remove',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
],
// The section label is added to sections without a "Configure section"
// link, and is only visible when the move block dialog is open.
'section_label' => [
'#markup' => $this->t('<span class="layout-builder__section-label" aria-hidden="true">@section</span>', ['@section' => $section_label]),
'#access' => !$layout instanceof PluginFormInterface,
],
'configure' => [
'#type' => 'link',
'#title' => $this->t('Configure @section', ['@section' => $section_label]),
'#access' => $layout instanceof PluginFormInterface,
'#url' => Url::fromRoute('layout_builder.configure_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
'class' => [
'use-ajax',
'layout-builder__link',
'layout-builder__link--configure',
],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
],
'layout-builder__section' => $build,
];
}
}

View File

@@ -0,0 +1,529 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionListTrait;
/**
* Provides an entity view display entity that has a layout.
*/
class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
use LayoutEntityHelperTrait;
use SectionListTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
// Set $entityFieldManager before calling the parent constructor because the
// constructor will call init() which then calls setComponent() which needs
// $entityFieldManager.
$this->entityFieldManager = \Drupal::service('entity_field.manager');
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function isOverridable() {
return $this->isLayoutBuilderEnabled() && $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
}
/**
* {@inheritdoc}
*/
public function setOverridable($overridable = TRUE) {
$this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
// Enable Layout Builder if it's not already enabled and overriding.
if ($overridable && !$this->isLayoutBuilderEnabled()) {
$this->enableLayoutBuilder();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isLayoutBuilderEnabled() {
// Layout Builder must not be enabled for the '_custom' view mode that is
// used for on-the-fly rendering of fields in isolation from the entity.
if ($this->isCustomMode()) {
return FALSE;
}
return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->setOverridable(FALSE);
$this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
return $this;
}
/**
* {@inheritdoc}
*/
public function getSections() {
return $this->getThirdPartySetting('layout_builder', 'sections', []);
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
// Third-party settings must be completely unset instead of stored as an
// empty array.
if (!$sections) {
$this->unsetThirdPartySetting('layout_builder', 'sections');
}
else {
$this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
$original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
$new_value = $this->isOverridable();
if ($original_value !== $new_value) {
$entity_type_id = $this->getTargetEntityTypeId();
$bundle = $this->getTargetBundle();
if ($new_value) {
$this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
else {
$this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
}
parent::preSave($storage);
$already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
$set_enabled = $this->isLayoutBuilderEnabled();
if ($already_enabled !== $set_enabled) {
if ($set_enabled) {
// Loop through all existing field-based components and add them as
// section-based components.
$components = $this->getComponents();
// Sort the components by weight.
uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
foreach ($components as $name => $component) {
$this->setComponent($name, $component);
}
}
else {
// When being disabled, remove all existing section data.
$this->removeAllSections();
}
}
}
/**
* {@inheritdoc}
*/
public function save(): int {
$return = parent::save();
if (!\Drupal::moduleHandler()->moduleExists('layout_builder_expose_all_field_blocks')) {
// Invalidate the block cache in order to regenerate field block
// definitions.
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
return $return;
}
/**
* Removes a layout section field if it is no longer needed.
*
* Because the field is shared across all view modes, the field will only be
* removed if no other view modes are using it.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $field_name
* The name for the layout section field.
*/
protected function removeSectionField($entity_type_id, $bundle, $field_name) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
$query = $storage->getQuery()
->condition('targetEntityType', $this->getTargetEntityTypeId())
->condition('bundle', $this->getTargetBundle())
->condition('mode', $this->getMode(), '<>')
->condition('third_party_settings.layout_builder.allow_custom', TRUE);
$enabled = (bool) $query->count()->execute();
if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
$field->delete();
}
}
/**
* Adds a layout section field to a given bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $field_name
* The name for the layout section field.
*/
protected function addSectionField($entity_type_id, $bundle, $field_name) {
$field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
if (!$field) {
$field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
if (!$field_storage) {
$field_storage = FieldStorageConfig::create([
'entity_type' => $entity_type_id,
'field_name' => $field_name,
'type' => 'layout_section',
'locked' => TRUE,
]);
$field_storage->setTranslatable(FALSE);
$field_storage->save();
}
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $bundle,
'label' => t('Layout'),
]);
$field->setTranslatable(FALSE);
$field->save();
}
}
/**
* {@inheritdoc}
*/
public function createCopy($mode) {
// Disable Layout Builder and remove any sections copied from the original.
return parent::createCopy($mode)
->setSections([])
->disableLayoutBuilder();
}
/**
* {@inheritdoc}
*/
protected function getDefaultRegion() {
if ($this->hasSection(0)) {
return $this->getSection(0)->getDefaultRegion();
}
return parent::getDefaultRegion();
}
/**
* Wraps the context repository service.
*
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
* The context repository service.
*/
protected function contextRepository() {
return \Drupal::service('context.repository');
}
/**
* Indicates if this display is using the '_custom' view mode.
*
* @return bool
* TRUE if this display is using the '_custom' view mode, FALSE otherwise.
*/
protected function isCustomMode() {
return $this->getOriginalMode() === static::CUSTOM_MODE;
}
/**
* {@inheritdoc}
*/
public function buildMultiple(array $entities) {
$build_list = parent::buildMultiple($entities);
// Layout Builder can not be enabled for the '_custom' view mode that is
// used for on-the-fly rendering of fields in isolation from the entity.
if ($this->isCustomMode()) {
return $build_list;
}
foreach ($entities as $id => $entity) {
$build_list[$id]['_layout_builder'] = $this->buildSections($entity);
// If there are any sections, remove all fields with configurable display
// from the existing build. These fields are replicated within sections as
// field blocks by ::setComponent().
if (!Element::isEmpty($build_list[$id]['_layout_builder'])) {
foreach ($build_list[$id] as $name => $build_part) {
$field_definition = $this->getFieldDefinition($name);
if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
unset($build_list[$id][$name]);
}
}
}
}
return $build_list;
}
/**
* Builds the render array for the sections of a given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
*
* @return array
* The render array representing the sections of the entity.
*/
protected function buildSections(FieldableEntityInterface $entity) {
$contexts = $this->getContextsForEntity($entity);
$label = new TranslatableMarkup('@entity being viewed', [
'@entity' => $entity->getEntityType()->getSingularLabel(),
]);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
$cacheability = new CacheableMetadata();
$storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability);
$build = [];
if ($storage) {
foreach ($storage->getSections() as $delta => $section) {
$build[$delta] = $section->toRenderArray($contexts);
}
}
// The render array is built based on decisions made by SectionStorage
// plugins and therefore it needs to depend on the accumulated
// cacheability of those decisions.
$cacheability->applyTo($build);
return $build;
}
/**
* Gets the available contexts for a given entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* An array of context objects for a given entity.
*/
protected function getContextsForEntity(FieldableEntityInterface $entity) {
$available_context_ids = array_keys($this->contextRepository()->getAvailableContexts());
return [
'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
'entity' => EntityContext::fromEntity($entity),
'display' => EntityContext::fromEntity($this),
] + $this->contextRepository()->getRuntimeContexts($available_context_ids);
}
/**
* {@inheritdoc}
*
* @todo Move this upstream in https://www.drupal.org/node/2939931.
*/
public function label() {
$bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
$bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
$target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
foreach ($this->getSections() as $section) {
$this->calculatePluginDependencies($section->getLayout());
foreach ($section->getComponents() as $component) {
$this->calculatePluginDependencies($component->getPlugin());
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// Loop through all sections and determine if the removed dependencies are
// used by their layout plugins.
foreach ($this->getSections() as $delta => $section) {
$layout_dependencies = $this->getPluginDependencies($section->getLayout());
$layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
if ($layout_removed_dependencies) {
// @todo Allow the plugins to react to their dependency removal in
// https://www.drupal.org/project/drupal/issues/2579743.
$this->removeSection($delta);
$changed = TRUE;
}
// If the section is not removed, loop through all components.
else {
foreach ($section->getComponents() as $uuid => $component) {
$plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
$component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
if ($component_removed_dependencies) {
// @todo Allow the plugins to react to their dependency removal in
// https://www.drupal.org/project/drupal/issues/2579743.
$section->removeComponent($uuid);
$changed = TRUE;
}
}
}
}
return $changed;
}
/**
* {@inheritdoc}
*/
public function setComponent($name, array $options = []) {
parent::setComponent($name, $options);
// Only continue if Layout Builder is enabled.
if (!$this->isLayoutBuilderEnabled()) {
return $this;
}
// Retrieve the updated options after the parent:: call.
$options = $this->content[$name];
// Provide backwards compatibility by converting to a section component.
$field_definition = $this->getFieldDefinition($name);
$extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
$is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
$configuration = [
'label_display' => '0',
'context_mapping' => ['entity' => 'layout_builder.entity'],
];
if ($is_view_configurable_non_extra_field) {
$configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
$keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
$configuration['formatter'] = array_intersect_key($options, $keys);
}
else {
$configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
}
$section = $this->getDefaultSection();
$region = $options['region'] ?? $section->getDefaultRegion();
$new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
$section->appendComponent($new_component);
}
return $this;
}
/**
* Gets a default section.
*
* @return \Drupal\layout_builder\Section
* The default section.
*/
protected function getDefaultSection() {
// If no section exists, append a new one.
if (!$this->hasSection(0)) {
$this->appendSection(new Section('layout_onecol'));
}
// Return the first section.
return $this->getSection(0);
}
/**
* Gets the section storage manager.
*
* @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
* The section storage manager.
*/
private function sectionStorageManager() {
return \Drupal::service('plugin.manager.layout_builder.section_storage');
}
/**
* {@inheritdoc}
*/
public function getComponent($name) {
if ($this->isLayoutBuilderEnabled() && $section_component = $this->getSectionComponentForFieldName($name)) {
$plugin = $section_component->getPlugin();
if ($plugin instanceof ConfigurableInterface) {
$configuration = $plugin->getConfiguration();
if (isset($configuration['formatter'])) {
return $configuration['formatter'];
}
}
}
return parent::getComponent($name);
}
/**
* Gets the component for a given field name if any.
*
* @param string $field_name
* The field name.
*
* @return \Drupal\layout_builder\SectionComponent|null
* The section component if it is available.
*/
private function getSectionComponentForFieldName($field_name) {
// Loop through every component until the first match is found.
foreach ($this->getSections() as $section) {
foreach ($section->getComponents() as $component) {
$plugin = $component->getPlugin();
if ($plugin instanceof DerivativeInspectionInterface && in_array($plugin->getBaseId(), ['field_block', 'extra_field_block'], TRUE)) {
// FieldBlock derivative IDs are in the format
// [entity_type]:[bundle]:[field].
[, , $field_block_field_name] = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getDerivativeId());
if ($field_block_field_name === $field_name) {
return $component;
}
}
}
}
return NULL;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\layout_builder\Section;
/**
* Provides storage for entity view display entities that have layouts.
*
* @internal
* Entity handlers are internal.
*/
class LayoutBuilderEntityViewDisplayStorage extends ConfigEntityStorage {
/**
* {@inheritdoc}
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = parent::mapToStorageRecord($entity);
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
$record['third_party_settings']['layout_builder']['sections'] = array_map(function (Section $section) {
return $section->toArray();
}, $record['third_party_settings']['layout_builder']['sections']);
}
return $record;
}
/**
* {@inheritdoc}
*/
protected function mapFromStorageRecords(array $records) {
foreach ($records as &$record) {
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
$sections = &$record['third_party_settings']['layout_builder']['sections'];
$sections = array_map([Section::class, 'fromArray'], $sections);
}
}
return parent::mapFromStorageRecords($records);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
/**
* Generates a sample entity for use by the Layout Builder.
*/
class LayoutBuilderSampleEntityGenerator implements SampleEntityGeneratorInterface {
/**
* The shared tempstore factory.
*
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* LayoutBuilderSampleEntityGenerator constructor.
*
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager) {
$this->tempStoreFactory = $temp_store_factory;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function get($entity_type_id, $bundle_id) {
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) {
return $entity;
}
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
if (!$entity_storage instanceof ContentEntityStorageInterface) {
throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id));
}
$entity = $entity_storage->createWithSampleValues($bundle_id);
// Mark the sample entity as being a preview.
$entity->in_preview = TRUE;
$tempstore->set("$entity_type_id.$bundle_id", $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function delete($entity_type_id, $bundle_id) {
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
$tempstore->delete("$entity_type_id.$bundle_id");
return $this;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\layout_builder\LayoutBuilderEnabledInterface;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\LayoutBuilderOverridableInterface;
/**
* Provides an interface for entity displays that have layout.
*/
interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface, LayoutBuilderEnabledInterface, LayoutBuilderOverridableInterface {}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\layout_builder\Entity;
/**
* Generates a sample entity.
*/
interface SampleEntityGeneratorInterface {
/**
* Gets a sample entity for a given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The bundle ID.
*
* @return \Drupal\Core\Entity\EntityInterface
* An entity.
*/
public function get($entity_type_id, $bundle_id);
/**
* Deletes a sample entity for a given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The bundle ID.
*
* @return $this
*/
public function delete($entity_type_id, $bundle_id);
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\layout_builder\Event;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Event fired in #pre_render of \Drupal\layout_builder\Element\LayoutBuilder.
*
* Subscribers to this event can prepare section storage before rendering.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::PREPARE_LAYOUT
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayoutEvent extends Event {
/**
* The section storage plugin.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new PrepareLayoutEvent.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage preparing the Layout.
*/
public function __construct(SectionStorageInterface $section_storage) {
$this->sectionStorage = $section_storage;
}
/**
* Gets the section storage.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage.
*/
public function getSectionStorage(): SectionStorageInterface {
return $this->sectionStorage;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\layout_builder\Event;
use Drupal\Core\Cache\CacheableResponseTrait;
use Drupal\Core\Plugin\PreviewAwarePluginInterface;
use Drupal\layout_builder\SectionComponent;
use Drupal\Component\EventDispatcher\Event;
/**
* Event fired when a section component's render array is being built.
*
* Subscribers to this event should manipulate the cacheability object and the
* build array in this event.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY
*/
class SectionComponentBuildRenderArrayEvent extends Event {
use CacheableResponseTrait;
/**
* The section component whose render array is being built.
*
* @var \Drupal\layout_builder\SectionComponent
*/
protected $component;
/**
* The available contexts.
*
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
*/
protected $contexts;
/**
* The plugin for the section component being built.
*
* @var \Drupal\Component\Plugin\PluginInspectionInterface
*/
protected $plugin;
/**
* Whether the component is in preview mode or not.
*
* @var bool
*/
protected $inPreview;
/**
* The render array built by the event subscribers.
*
* @var array
*/
protected $build = [];
/**
* Creates a new SectionComponentBuildRenderArrayEvent object.
*
* @param \Drupal\layout_builder\SectionComponent $component
* The section component whose render array is being built.
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The available contexts.
* @param bool $in_preview
* (optional) Whether the component is in preview mode or not.
*/
public function __construct(SectionComponent $component, array $contexts, $in_preview = FALSE) {
$this->component = $component;
$this->contexts = $contexts;
$this->plugin = $component->getPlugin($contexts);
$this->inPreview = $in_preview;
if ($this->plugin instanceof PreviewAwarePluginInterface) {
$this->plugin->setInPreview($in_preview);
}
}
/**
* Get the section component whose render array is being built.
*
* @return \Drupal\layout_builder\SectionComponent
* The section component whose render array is being built.
*/
public function getComponent() {
return $this->component;
}
/**
* Get the available contexts.
*
* @return array|\Drupal\Core\Plugin\Context\ContextInterface[]
* The available contexts.
*/
public function getContexts() {
return $this->contexts;
}
/**
* Get the plugin for the section component being built.
*
* @return \Drupal\Component\Plugin\PluginInspectionInterface
* The plugin for the section component being built.
*/
public function getPlugin() {
return $this->plugin;
}
/**
* Determine if the component is in preview mode.
*
* @return bool
* Whether the component is in preview mode or not.
*/
public function inPreview() {
return $this->inPreview;
}
/**
* Get the render array in its current state.
*
* @return array
* The render array built by the event subscribers.
*/
public function getBuild() {
return $this->build;
}
/**
* Set the render array.
*
* @param array $build
* A render array.
*/
public function setBuild(array $build) {
$this->build = $build;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\Plugin\Block\InlineBlock;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\views\Plugin\Block\ViewsBlock;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Builds render arrays and handles access for all block components.
*
* @internal
* Tagged services are internal.
*/
class BlockComponentRenderArray implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Creates a BlockComponentRenderArray object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(AccountInterface $current_user) {
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY] = ['onBuildRender', 100];
return $events;
}
/**
* Builds render arrays for block plugins and sets it on the event.
*
* @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
* The section component render event.
*/
public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
$block = $event->getPlugin();
if (!$block instanceof BlockPluginInterface) {
return;
}
// Set block access dependency even if we are not checking access on
// this level. The block itself may render another
// RefinableDependentAccessInterface object and need to pass on this value.
if ($block instanceof RefinableDependentAccessInterface) {
$contexts = $event->getContexts();
if (isset($contexts['layout_builder.entity'])) {
if ($entity = $contexts['layout_builder.entity']->getContextValue()) {
if ($event->inPreview()) {
// If previewing in Layout Builder allow access.
$block->setAccessDependency(new LayoutPreviewAccessAllowed());
}
else {
$block->setAccessDependency($entity);
}
}
}
}
// Only check access if the component is not being previewed.
if ($event->inPreview()) {
$access = AccessResult::allowed()->setCacheMaxAge(0);
}
else {
$access = $block->access($this->currentUser, TRUE);
}
$event->addCacheableDependency($access);
if ($access->isAllowed()) {
$event->addCacheableDependency($block);
// @todo Revisit after https://www.drupal.org/node/3027653, as this will
// provide a better way to remove contextual links from Views blocks.
// Currently, doing this requires setting
// \Drupal\views\ViewExecutable::$showAdminLinks() to false before the
// Views block is built.
if ($block instanceof ViewsBlock && $event->inPreview()) {
$block->getViewExecutable()->setShowAdminLinks(FALSE);
}
$content = $block->build();
// We don't output the block render data if there are no render elements
// found, but we want to capture the cache metadata from the block
// regardless.
$event->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
$is_content_empty = Element::isEmpty($content);
$is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
// If the content is empty and no placeholder is available, return.
if ($is_content_empty && !$is_placeholder_ready) {
return;
}
$build = [
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
'#theme' => 'block',
'#configuration' => $block->getConfiguration(),
'#plugin_id' => $block->getPluginId(),
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'#in_preview' => $event->inPreview(),
'#weight' => $event->getComponent()->getWeight(),
];
// Place the $content returned by the block plugin into a 'content' child
// element, as a way to allow the plugin to have complete control of its
// properties and rendering (for instance, its own #theme) without
// conflicting with the properties used above, or alternate ones used by
// alternate block rendering approaches in contributed modules. However,
// the use of a child element is an implementation detail of this
// particular block rendering approach. Semantically, the content returned
// by the block plugin, and in particular, attributes and contextual links
// are information that belong to the entire block. Therefore, we must
// move these properties from $content and merge them into the top-level
// element.
if (isset($content['#attributes'])) {
$build['#attributes'] = $content['#attributes'];
unset($content['#attributes']);
}
// Hide contextual links for inline blocks until the UX issues surrounding
// editing them directly are resolved.
// @see https://www.drupal.org/project/drupal/issues/3075308
if (!$block instanceof InlineBlock && !empty($content['#contextual_links'])) {
$build['#contextual_links'] = $content['#contextual_links'];
}
$build['content'] = $content;
if ($event->inPreview()) {
if ($block instanceof PreviewFallbackInterface) {
$preview_fallback_string = $block->getPreviewFallbackString();
}
else {
$preview_fallback_string = $this->t('"@block" block', ['@block' => $block->label()]);
}
// @todo Use new label methods so
// data-layout-content-preview-placeholder-label doesn't have to use
// preview fallback in https://www.drupal.org/node/2025649.
$build['#attributes']['data-layout-content-preview-placeholder-label'] = $preview_fallback_string;
if ($is_content_empty && $is_placeholder_ready) {
$build['content']['#markup'] = $this->t('Placeholder for the @preview_fallback', ['@preview_fallback' => $block->getPreviewFallbackString()]);
}
}
$event->setBuild($build);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber to prepare section storage.
*
* Section storage works via the
* \Drupal\layout_builder\Event\PrepareLayoutEvent.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayout implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new PrepareLayout.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onPrepareLayout', 10];
return $events;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onPrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
else {
// If the layout is an override that has not yet been overridden, copy the
// sections from the corresponding default.
if ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) {
$sections = $section_storage->getDefaultSectionStorage()->getSections();
foreach ($sections as $section) {
$section_storage->appendSection($section);
}
}
// Add storage to tempstore regardless of what the storage is.
$this->layoutTempstoreRepository->set($section_storage);
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\BlockContentEvents;
use Drupal\block_content\BlockContentInterface;
use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\layout_builder\InlineBlockUsageInterface;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber that returns an access dependency for inline blocks.
*
* When used within the layout builder the access dependency for inline blocks
* will be explicitly set but if access is evaluated outside of the layout
* builder then the dependency may not have been set.
*
* A known example of when the access dependency will not have been set is when
* determining 'view' or 'download' access to a file entity that is attached
* to a content block via a field that is using the private file system. The
* file access handler will evaluate access on the content block without setting
* the dependency.
*
* @internal
* Tagged services are internal.
*
* @see \Drupal\file\FileAccessControlHandler::checkAccess()
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
*/
class SetInlineBlockDependency implements EventSubscriberInterface {
use LayoutEntityHelperTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The inline block usage service.
*
* @var \Drupal\layout_builder\InlineBlockUsageInterface
*/
protected $usage;
/**
* Constructs SetInlineBlockDependency object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
* The inline block usage service.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->database = $database;
$this->usage = $usage;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY => 'onGetDependency',
];
}
/**
* Handles the BlockContentEvents::INLINE_BLOCK_GET_DEPENDENCY event.
*
* @param \Drupal\block_content\Event\BlockContentGetDependencyEvent $event
* The event.
*/
public function onGetDependency(BlockContentGetDependencyEvent $event) {
if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) {
$event->setAccessDependency($dependency);
}
}
/**
* Get the access dependency of an inline block.
*
* If the block is used in an entity that entity will be returned as the
* dependency.
*
* For revisionable entities the entity will only be returned if it is used in
* the latest revision of the entity. For inline blocks that are not used in
* the latest revision but are used in a previous revision the entity will not
* be returned because calling
* \Drupal\Core\Access\AccessibleInterface::access() will only check access on
* the latest revision. Therefore if the previous revision of the entity was
* returned as the dependency access would be granted to inline block
* regardless of whether the user has access to the revision in which the
* inline block was used.
*
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content entity.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* Returns the layout dependency.
*
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
protected function getInlineBlockDependency(BlockContentInterface $block_content) {
$layout_entity_info = $this->usage->getUsage($block_content->id());
if (empty($layout_entity_info)) {
// If the block does not have usage information then we cannot set a
// dependency. It may be used by another module besides layout builder.
return NULL;
}
$layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type);
$layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id);
if ($this->isLayoutCompatibleEntity($layout_entity)) {
if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) {
return $layout_entity;
}
}
return NULL;
}
/**
* Determines if a block content revision is used in an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $layout_entity
* The layout entity.
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content revision.
*
* @return bool
* TRUE if the block content revision is used as an inline block in the
* layout entity.
*/
protected function isBlockRevisionUsedInEntity(EntityInterface $layout_entity, BlockContentInterface $block_content) {
$sections_blocks_revision_ids = $this->getInlineBlockRevisionIdsInSections($this->getEntitySections($layout_entity));
return in_array($block_content->getRevisionId(), $sections_blocks_revision_ids);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Drupal\layout_builder\Field;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\SectionListTrait;
/**
* Defines an item list class for layout section fields.
*
* @internal
* Plugin classes are internal.
*
* @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
*/
class LayoutSectionItemList extends FieldItemList implements SectionListInterface {
use SectionListTrait;
/**
* Numerically indexed array of field items.
*
* @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem[]
*/
protected $list = [];
/**
* {@inheritdoc}
*/
public function getSections() {
$sections = [];
foreach ($this->list as $delta => $item) {
$sections[$delta] = $item->section;
}
return $sections;
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
$this->list = [];
$sections = array_values($sections);
/** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */
foreach ($sections as $section) {
$item = $this->appendItem();
$item->section = $section;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getEntity() {
$entity = parent::getEntity();
// Ensure the entity is updated with the latest value.
$entity->set($this->getName(), $this->getValue());
return $entity;
}
/**
* {@inheritdoc}
*/
public function preSave() {
parent::preSave();
// Loop through each section and reconstruct it to ensure that all default
// values are present.
foreach ($this->list as $item) {
$item->section = Section::fromArray($item->section->toArray());
}
}
/**
* {@inheritdoc}
*/
public function equals(FieldItemListInterface $list_to_compare) {
if (!$list_to_compare instanceof LayoutSectionItemList) {
return FALSE;
}
// Convert arrays of section objects to array values for comparison.
$convert = function (LayoutSectionItemList $list) {
return array_map(function (Section $section) {
return $section->toArray();
}, $list->getSections());
};
return $convert($this) === $convert($list_to_compare);
}
/**
* Overrides \Drupal\Core\Field\FieldItemListInterface::defaultAccess().
*
* @ingroup layout_builder_access
*/
public function defaultAccess($operation = 'view', ?AccountInterface $account = NULL) {
// @todo Allow access in https://www.drupal.org/node/2942975.
return AccessResult::forbidden();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to add a block.
*
* @internal
* Form classes are internal.
*/
class AddBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_add_block';
}
/**
* {@inheritdoc}
*/
protected function submitLabel() {
return $this->t('Add block');
}
/**
* Builds the form for the block.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param string $region
* The region of the block.
* @param string|null $plugin_id
* The plugin ID of the block to add.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL) {
// Only generate a new component once per form submission.
if (!$component = $form_state->get('layout_builder__component')) {
$component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]);
$section_storage->getSection($delta)->appendComponent($component);
$form_state->set('layout_builder__component', $component);
}
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base form for configuring a block.
*
* @internal
* Form classes are internal.
*/
abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInterface, WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use ContextAwarePluginAssignmentTrait;
use LayoutBuilderContextTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The plugin being configured.
*
* @var \Drupal\Core\Block\BlockPluginInterface
*/
protected $block;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The UUID generator.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidGenerator;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* The current region.
*
* @var string
*/
protected $region;
/**
* The UUID of the component.
*
* @var string
*/
protected $uuid;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new block form.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The context repository.
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Component\Uuid\UuidInterface $uuid
* The UUID generator.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ContextRepositoryInterface $context_repository, BlockManagerInterface $block_manager, UuidInterface $uuid, PluginFormFactoryInterface $plugin_form_manager) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->contextRepository = $context_repository;
$this->blockManager = $block_manager;
$this->uuidGenerator = $uuid;
$this->pluginFormFactory = $plugin_form_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('context.repository'),
$container->get('plugin.manager.block'),
$container->get('uuid'),
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'layout_builder_configure_block';
}
/**
* Builds the form for the block.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param \Drupal\layout_builder\SectionComponent $component
* The section component containing the block.
*
* @return array
* The form array.
*/
public function doBuildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, ?SectionComponent $component = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->uuid = $component->getUuid();
$this->block = $component->getPlugin();
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($section_storage));
$form['#tree'] = TRUE;
$form['settings'] = [];
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$form['settings'] = $this->getPluginForm($this->block)->buildConfigurationForm($form['settings'], $subform_state);
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->submitLabel(),
'#button_type' => 'primary',
];
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
// @todo static::ajaxSubmit() requires data-drupal-selector to be the same
// between the various Ajax requests. A bug in
// \Drupal\Core\Form\FormBuilder prevents that from happening unless
// $form['#id'] is also the same. Normally, #id is set to a unique HTML
// ID via Html::getUniqueId(), but here we bypass that in order to work
// around the data-drupal-selector bug. This is okay so long as we
// assume that this form only ever occurs once on a page. Remove this
// workaround in https://www.drupal.org/node/2897377.
$form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
}
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* Returns the label for the submit button.
*
* @return string
* Submit label.
*/
abstract protected function submitLabel();
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$this->getPluginForm($this->block)->validateConfigurationForm($form['settings'], $subform_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Call the plugin submit handler.
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$this->getPluginForm($this->block)->submitConfigurationForm($form, $subform_state);
// If this block is context-aware, set the context mapping.
if ($this->block instanceof ContextAwarePluginInterface) {
$this->block->setContextMapping($subform_state->getValue('context_mapping', []));
}
$configuration = $this->block->getConfiguration();
$section = $this->sectionStorage->getSection($this->delta);
$section->getComponent($this->uuid)->setConfiguration($configuration);
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Retrieves the plugin form for a given block.
*
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the block.
*/
protected function getPluginForm(BlockPluginInterface $block) {
if ($block instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($block, 'configure');
}
return $block;
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
/**
* Retrieves the current layout section being edited by the form.
*
* @return \Drupal\layout_builder\Section
* The current layout section.
*/
public function getCurrentSection() {
return $this->sectionStorage->getSection($this->delta);
}
/**
* Retrieves the current component being edited by the form.
*
* @return \Drupal\layout_builder\SectionComponent
* The current section component.
*/
public function getCurrentComponent() {
return $this->getCurrentSection()->getComponent($this->uuid);
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Layout\LayoutInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for configuring a layout section.
*
* @internal
* Form classes are internal.
*/
class ConfigureSectionForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The plugin being configured.
*
* @var \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
*/
protected $layout;
/**
* The section being configured.
*
* @var \Drupal\layout_builder\Section
*/
protected $section;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* The plugin ID.
*
* @var string
*/
protected $pluginId;
/**
* Indicates whether the section is being added or updated.
*
* @var bool
*/
protected $isUpdate;
/**
* Constructs a new ConfigureSectionForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, PluginFormFactoryInterface $plugin_form_manager) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->pluginFormFactory = $plugin_form_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_configure_section';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $plugin_id = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->isUpdate = is_null($plugin_id);
$this->pluginId = $plugin_id;
$section = $this->getCurrentSection();
if ($this->isUpdate) {
if ($label = $section->getLayoutSettings()['label']) {
$form['#title'] = $this->t('Configure @section', ['@section' => $label]);
}
}
// Passing available contexts to the layout plugin here could result in an
// exception since the layout may not have a context mapping for a required
// context slot on creation.
$this->layout = $section->getLayout();
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($this->sectionStorage));
$form['#tree'] = TRUE;
$form['layout_settings'] = [];
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state);
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'),
'#button_type' => 'primary',
];
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
// @todo static::ajaxSubmit() requires data-drupal-selector to be the same
// between the various Ajax requests. A bug in
// \Drupal\Core\Form\FormBuilder prevents that from happening unless
// $form['#id'] is also the same. Normally, #id is set to a unique HTML
// ID via Html::getUniqueId(), but here we bypass that in order to work
// around the data-drupal-selector bug. This is okay so long as we
// assume that this form only ever occurs once on a page. Remove this
// workaround in https://www.drupal.org/node/2897377.
$form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
}
$target_highlight_id = $this->isUpdate ? $this->sectionUpdateHighlightId($delta) : $this->sectionAddHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Call the plugin submit handler.
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
$this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state);
// If this layout is context-aware, set the context mapping.
if ($this->layout instanceof ContextAwarePluginInterface) {
$this->layout->setContextMapping($subform_state->getValue('context_mapping', []));
}
$configuration = $this->layout->getConfiguration();
$section = $this->getCurrentSection();
$section->setLayoutSettings($configuration);
if (!$this->isUpdate) {
$this->sectionStorage->insertSection($this->delta, $section);
}
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Retrieves the plugin form for a given layout.
*
* @param \Drupal\Core\Layout\LayoutInterface $layout
* The layout plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the layout.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
protected function getPluginForm(LayoutInterface $layout) {
if ($layout instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($layout, 'configure');
}
if ($layout instanceof PluginFormInterface) {
return $layout;
}
throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId()));
}
/**
* Retrieves the section storage property.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
/**
* Retrieves the layout being modified by the form.
*
* @return \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
* The layout for the current form.
*/
public function getCurrentLayout(): LayoutInterface {
return $this->layout;
}
/**
* Retrieves the section being modified by the form.
*
* @return \Drupal\layout_builder\Section
* The section for the current form.
*/
public function getCurrentSection(): Section {
if (!isset($this->section)) {
if ($this->isUpdate) {
$this->section = $this->sectionStorage->getSection($this->delta);
}
else {
$this->section = new Section($this->pluginId);
}
}
return $this->section;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for defaults.
*
* @internal
* Form classes are internal.
*/
class DefaultsEntityForm extends EntityForm {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new DefaultsEntityForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$form['#attributes']['class'][] = 'layout-builder-form';
$form['layout_builder'] = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
'#process' => [[static::class, 'layoutBuilderElementGetKeys']],
];
$form['layout_builder_message'] = $this->buildMessage($section_storage->getContextValue('display'));
$this->sectionStorage = $section_storage;
return parent::buildForm($form, $form_state);
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
/**
* Renders a message to display at the top of the layout builder.
*
* @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity
* The entity view display being edited.
*
* @return array
* A renderable array containing the message.
*/
protected function buildMessage(LayoutEntityDisplayInterface $entity) {
$entity_type_id = $entity->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$args = [
'@bundle' => $bundle_info[$entity->getTargetBundle()]['label'],
'@plural_label' => $entity_type->getPluralLabel(),
];
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout template for all @bundle @plural_label.', $args);
}
else {
$message = $this->t('You are editing the layout template for all @plural_label.', $args);
}
return $this->buildMessageContainer($message, 'defaults');
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// \Drupal\Core\Entity\EntityForm::buildEntity() clones the entity object.
// Keep it in sync with the one used by the section storage.
$this->setEntity($this->sectionStorage->getContextValue('display'));
$entity = parent::buildEntity($form, $form_state);
$this->sectionStorage->setContextValue('display', $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$route_parameters = $route_match->getParameters()->all();
return $this->entityTypeManager->getStorage('entity_view_display')->load($route_parameters['entity_type_id'] . '.' . $route_parameters['bundle'] . '.' . $route_parameters['view_mode_name']);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
return $this->buildActions($actions);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = $this->sectionStorage->save();
$this->saveTasks($form_state, $this->t('The layout has been saved.'));
return $return;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Discards any pending changes to the layout.
*
* @internal
* Form classes are internal.
*/
class DiscardLayoutChangesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new DiscardLayoutChangesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_discard_changes';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to discard your layout changes?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger->addMessage($this->t('The changes to the layout have been discarded.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Disables Layout Builder for a given default.
*
* @internal
* Form classes are internal.
*/
class LayoutBuilderDisableForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new RevertOverridesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->setMessenger($messenger);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_disable_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to disable Layout Builder?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('All customizations will be removed. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getRedirectUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
if (!$section_storage instanceof DefaultsSectionStorageInterface) {
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide defaults', $section_storage->getStorageType(), $section_storage->getStorageId()));
}
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->sectionStorage->disableLayoutBuilder()->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger()->addMessage($this->t('Layout Builder has been disabled.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a trait for common methods used in Layout Builder entity forms.
*/
trait LayoutBuilderEntityFormTrait {
use PreviewToggleTrait;
/**
* {@inheritdoc}
*/
public function getBaseFormId(): string {
return $this->getEntity()->getEntityTypeId() . '_layout_builder_form';
}
/**
* Build the message container.
*
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
* The message to display.
* @param string $type
* The form type this is being attached to.
*
* @return array
* The render array.
*/
protected function buildMessageContainer(TranslatableMarkup $message, string $type): array {
return [
'#type' => 'container',
'#attributes' => [
'class' => [
'layout-builder__message',
sprintf('layout-builder__message--%s', $type),
],
],
'message' => [
'#theme' => 'status_messages',
'#message_list' => ['status' => [$message]],
'#status_headings' => [
'status' => $this->t('Status message'),
],
],
'#weight' => -900,
];
}
/**
* Form submission handler.
*/
public function redirectOnSubmit(array $form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl($form_state->getTriggeringElement()['#redirect']));
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage(): SectionStorageInterface {
return $this->sectionStorage;
}
/**
* Builds the actions for the form.
*
* @param array $actions
* The actions array to modify.
*
* @return array
* The modified actions array.
*/
protected function buildActions(array $actions): array {
$actions['#attributes']['role'] = 'region';
$actions['#attributes']['aria-label'] = $this->t('Layout Builder tools');
$actions['submit']['#value'] = $this->t('Save layout');
$actions['#weight'] = -1000;
$actions['discard_changes'] = [
'#type' => 'submit',
'#value' => $this->t('Discard changes'),
'#submit' => ['::redirectOnSubmit'],
'#redirect' => 'discard_changes',
];
$actions['preview_toggle'] = $this->buildContentPreviewToggle();
return $actions;
}
/**
* Performs tasks that are needed during the save process.
*
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
* The message to display.
*/
protected function saveTasks(FormStateInterface $formState, TranslatableMarkup $message): void {
$this->layoutTempstoreRepository->delete($this->getSectionStorage());
$this->messenger()->addStatus($message);
$formState->setRedirectUrl($this->getSectionStorage()->getRedirectUrl());
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Edit form for the LayoutBuilderEntityViewDisplay entity type.
*
* @internal
* Form classes are internal.
*/
class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
/**
* The entity being used by this form.
*
* @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
*/
protected $entity;
/**
* The storage section.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
// Remove the Layout Builder field from the list.
$form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$is_enabled = $this->entity->isLayoutBuilderEnabled();
if ($is_enabled) {
// Hide the table of fields.
$form['fields']['#access'] = FALSE;
$form['#fields'] = [];
$form['#extra'] = [];
}
$form['manage_layout'] = [
'#type' => 'link',
'#title' => $this->t('Manage layout'),
'#weight' => -10,
'#attributes' => ['class' => ['button']],
'#url' => $this->sectionStorage->getLayoutBuilderUrl(),
'#access' => $is_enabled,
];
$form['layout'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Layout options'),
'#tree' => TRUE,
];
$form['layout']['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use Layout Builder'),
'#default_value' => $is_enabled,
];
$form['#entity_builders']['layout_builder'] = '::entityFormEntityBuild';
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
if ($this->isCanonicalMode($this->entity->getMode())) {
$entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
$form['layout']['allow_custom'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow each @entity to have its layout customized.', [
'@entity' => $entity_type->getSingularLabel(),
]),
'#default_value' => $this->entity->isOverridable(),
'#states' => [
'disabled' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
'invisible' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
],
];
if (!$is_enabled) {
$form['layout']['allow_custom']['#attributes']['disabled'] = 'disabled';
}
// Prevent turning off overrides while any exist.
if ($this->hasOverrides($this->entity)) {
$form['layout']['enabled']['#disabled'] = TRUE;
$form['layout']['enabled']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
$form['layout']['allow_custom']['#disabled'] = TRUE;
$form['layout']['allow_custom']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
unset($form['layout']['allow_custom']['#states']);
unset($form['#entity_builders']['layout_builder']);
}
}
// For non-canonical modes, the existing value should be preserved.
else {
$form['layout']['allow_custom'] = [
'#type' => 'value',
'#value' => $this->entity->isOverridable(),
];
}
return $form;
}
/**
* Determines if the mode is used by the canonical route.
*
* @param string $mode
* The view mode.
*
* @return bool
* TRUE if the mode is valid, FALSE otherwise.
*/
protected function isCanonicalMode($mode) {
// @todo This is a convention core uses but is not a given, nor is it easily
// introspectable. Address in https://www.drupal.org/node/2907413.
$canonical_mode = 'full';
if ($mode === $canonical_mode) {
return TRUE;
}
// The default mode is valid if the canonical mode is not enabled.
if ($mode === 'default') {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($this->entity->getEntityTypeId());
$query = $storage->getQuery()
->condition('targetEntityType', $this->entity->getTargetEntityTypeId())
->condition('bundle', $this->entity->getTargetBundle())
->condition('status', TRUE)
->condition('mode', $canonical_mode);
return !$query->count()->execute();
}
return FALSE;
}
/**
* Determines if the defaults have any overrides.
*
* @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display
* The entity display.
*
* @return bool
* TRUE if there are any overrides of this default, FALSE otherwise.
*/
protected function hasOverrides(LayoutEntityDisplayInterface $display) {
if (!$display->isOverridable()) {
return FALSE;
}
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
->accessCheck(FALSE)
->exists(OverridesSectionStorage::FIELD_NAME);
if ($bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $display->getTargetBundle());
}
return (bool) $query->count()->execute();
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// Do not process field values if Layout Builder is or will be enabled.
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity */
$already_enabled = $entity->isLayoutBuilderEnabled();
if ($already_enabled || $set_enabled) {
$form['#fields'] = [];
$form['#extra'] = [];
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
/**
* Entity builder for layout options on the entity view display form.
*/
public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) {
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
$already_enabled = $display->isLayoutBuilderEnabled();
if ($set_enabled) {
$overridable = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
$display->setOverridable($overridable);
if (!$already_enabled) {
$display->enableLayoutBuilder();
}
}
elseif ($already_enabled) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl('disable'));
}
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildFieldRow($field_definition, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildExtraFieldRow($field_id, $extra_field);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for confirmation forms that rebuild the Layout Builder.
*
* @internal
* Form classes are internal.
*/
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The field delta.
*
* @var int
*/
protected $delta;
/**
* Constructs a new RemoveSectionForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$form = parent::buildForm($form, $form_state);
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
$form['actions']['cancel']['#attributes']['class'][] = 'dialog-cancel';
$target_highlight_id = !empty($this->uuid) ? $this->blockUpdateHighlightId($this->uuid) : $this->sectionUpdateHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
// The AJAX system automatically moves focus to the first tabbable
// element after closing a dialog, sometimes scrolling to a page top.
// Disable refocus on the button.
$form['actions']['submit']['#ajax']['disable-refocus'] = TRUE;
}
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->handleSectionStorage($this->sectionStorage, $form_state);
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Performs any actions on the section storage before saving.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
abstract protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state);
}

View File

@@ -0,0 +1,345 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for moving a block.
*
* @internal
* Form classes are internal.
*/
class MoveBlockForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* The section delta.
*
* @var int
*/
protected $delta;
/**
* The region name.
*
* @var string
*/
protected $region;
/**
* The component uuid.
*
* @var string
*/
protected $uuid;
/**
* The Layout Tempstore.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstore;
/**
* Constructs a new MoveBlockForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstore = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_block_move';
}
/**
* Builds the move block form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The original delta of the section.
* @param string $region
* The original region of the block.
* @param string $uuid
* The UUID of the block being updated.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$parameters = array_slice(func_get_args(), 2);
foreach ($parameters as $parameter) {
if (is_null($parameter)) {
throw new \InvalidArgumentException('MoveBlockForm requires all parameters.');
}
}
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->uuid = $uuid;
$this->region = $region;
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);
$sections = $section_storage->getSections();
$contexts = $this->getPopulatedContexts($section_storage);
$region_options = [];
foreach ($sections as $section_delta => $section) {
$layout = $section->getLayout($contexts);
$layout_definition = $layout->getPluginDefinition();
if (!($section_label = $section->getLayoutSettings()['label'])) {
$section_label = $this->t('Section: @delta', ['@delta' => $section_delta + 1])->render();
}
foreach ($layout_definition->getRegions() as $region_name => $region_info) {
// Group regions by section.
$region_options[$section_label]["$section_delta:$region_name"] = $this->t(
'@section, Region: @region',
['@section' => $section_label, '@region' => $region_info['label']]
);
}
}
// $this->region and $this->delta are where the block is currently placed.
// $selected_region and $selected_delta are the values from this form
// specifying where the block should be moved to.
$selected_region = $this->getSelectedRegion($form_state);
$selected_delta = $this->getSelectedDelta($form_state);
$form['region'] = [
'#type' => 'select',
'#options' => $region_options,
'#title' => $this->t('Region'),
'#default_value' => "$selected_delta:$selected_region",
'#ajax' => [
'wrapper' => 'layout-builder-components-table',
'callback' => '::getComponentsWrapper',
],
];
$current_section = $sections[$selected_delta];
$aria_label = $this->t('Blocks in Section: @section, Region: @region', ['@section' => $selected_delta + 1, '@region' => $selected_region]);
$form['components_wrapper']['components'] = [
'#type' => 'table',
'#header' => [
$this->t('Block label'),
$this->t('Weight'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'table-sort-weight',
],
],
// Create a wrapping element so that the Ajax update also replaces the
// 'Show block weights' link.
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'id' => 'layout-builder-components-table',
'class' => ['layout-builder-components-table'],
'aria-label' => $aria_label,
],
],
],
];
/** @var \Drupal\layout_builder\SectionComponent[] $components */
$components = $current_section->getComponentsByRegion($selected_region);
// If the component is not in this region, add it to the listed components.
if (!isset($components[$uuid])) {
$components[$uuid] = $sections[$delta]->getComponent($uuid);
}
$state_weight_delta = round(count($components) / 2);
foreach ($components as $component_uuid => $component) {
/** @var \Drupal\Core\Block\BlockPluginInterface $plugin */
$plugin = $component->getPlugin();
$is_current_block = $component_uuid === $uuid;
$row_classes = [
'draggable',
'layout-builder-components-table__row',
];
$label['#wrapper_attributes']['class'] = ['layout-builder-components-table__block-label'];
if ($is_current_block) {
// Highlight the current block.
$label['#markup'] = $this->t('@label (current)', ['@label' => $plugin->label()]);
$label['#wrapper_attributes']['class'][] = 'layout-builder-components-table__block-label--current';
$row_classes[] = 'layout-builder-components-table__row--current';
}
else {
$label['#markup'] = $plugin->label();
}
$form['components_wrapper']['components'][$component_uuid] = [
'#attributes' => ['class' => $row_classes],
'label' => $label,
'weight' => [
'#type' => 'weight',
'#default_value' => $component->getWeight(),
'#title' => $this->t('Weight for @block block', ['@block' => $plugin->label()]),
'#title_display' => 'invisible',
'#attributes' => [
'class' => ['table-sort-weight'],
],
'#delta' => $state_weight_delta,
],
];
}
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Move'),
'#button_type' => 'primary',
];
$form['#attributes']['data-add-layout-builder-wrapper'] = 'layout-builder--move-blocks-active';
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$region = $this->getSelectedRegion($form_state);
$delta = $this->getSelectedDelta($form_state);
$original_section = $this->sectionStorage->getSection($this->delta);
$component = $original_section->getComponent($this->uuid);
$section = $this->sectionStorage->getSection($delta);
if ($delta !== $this->delta) {
// Remove component from old section and add it to the new section.
$original_section->removeComponent($this->uuid);
$section->insertComponent(0, $component);
}
$component->setRegion($region);
foreach ($form_state->getValue('components') as $uuid => $component_info) {
$section->getComponent($uuid)->setWeight($component_info['weight']);
}
$this->layoutTempstore->set($this->sectionStorage);
}
/**
* Ajax callback for the region select element.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The components wrapper render array.
*/
public function getComponentsWrapper(array $form, FormStateInterface $form_state) {
return $form['components_wrapper'];
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
return $this->rebuildAndClose($this->sectionStorage);
}
/**
* Gets the selected region.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return string
* The current region name.
*/
protected function getSelectedRegion(FormStateInterface $form_state) {
if ($form_state->hasValue('region')) {
return explode(':', $form_state->getValue('region'), 2)[1];
}
return $this->region;
}
/**
* Gets the selected delta.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return int
* The section delta.
*/
protected function getSelectedDelta(FormStateInterface $form_state) {
if ($form_state->hasValue('region')) {
return (int) explode(':', $form_state->getValue('region'))[0];
}
return (int) $this->delta;
}
/**
* Provides a title callback.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The original delta of the section.
* @param string $uuid
* The UUID of the block being updated.
*
* @return string
* The title for the move block form.
*/
public function title(SectionStorageInterface $section_storage, $delta, $uuid) {
$block_label = $section_storage
->getSection($delta)
->getComponent($uuid)
->getPlugin()
->label();
return $this->t('Move the @block_label block', ['@block_label' => $block_label]);
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for overrides.
*
* @internal
* Form classes are internal.
*/
class OverridesEntityForm extends ContentEntityForm implements WorkspaceDynamicSafeFormInterface {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
use WorkspaceSafeFormTrait;
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new OverridesEntityForm.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
protected function init(FormStateInterface $form_state) {
parent::init($form_state);
$form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation(), FALSE);
$form_display->setComponent(OverridesSectionStorage::FIELD_NAME, [
'type' => 'layout_builder_widget',
'weight' => -10,
'settings' => [],
]);
$this->setFormDisplay($form_display, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
$form = parent::buildForm($form, $form_state);
$form['#attributes']['class'][] = 'layout-builder-form';
// @todo \Drupal\layout_builder\Field\LayoutSectionItemList::defaultAccess()
// restricts all access to the field, explicitly allow access here until
// https://www.drupal.org/node/2942975 is resolved.
$form[OverridesSectionStorage::FIELD_NAME]['#access'] = TRUE;
$form['layout_builder_message'] = $this->buildMessage($section_storage->getContextValue('entity'), $section_storage);
return $form;
}
/**
* Renders a message to display at the top of the layout builder.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose layout is being edited.
* @param \Drupal\layout_builder\OverridesSectionStorageInterface $section_storage
* The current section storage.
*
* @return array
* A renderable array containing the message.
*/
protected function buildMessage(EntityInterface $entity, OverridesSectionStorageInterface $section_storage) {
$entity_type = $entity->getEntityType();
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity->getEntityTypeId());
$variables = [
'@bundle' => $bundle_info[$entity->bundle()]['label'],
'@singular_label' => $entity_type->getSingularLabel(),
'@plural_label' => $entity_type->getPluralLabel(),
];
$defaults_link = $section_storage
->getDefaultSectionStorage()
->getLayoutBuilderUrl();
if ($defaults_link->access($this->currentUser())) {
$variables[':link'] = $defaults_link->toString();
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout for this @bundle @singular_label. <a href=":link">Edit the template for all @bundle @plural_label instead.</a>', $variables);
}
else {
$message = $this->t('You are editing the layout for this @singular_label. <a href=":link">Edit the template for all @plural_label instead.</a>', $variables);
}
}
else {
if ($entity_type->hasKey('bundle')) {
$message = $this->t('You are editing the layout for this @bundle @singular_label.', $variables);
}
else {
$message = $this->t('You are editing the layout for this @singular_label.', $variables);
}
}
return $this->buildMessageContainer($message, 'overrides');
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = parent::save($form, $form_state);
$this->saveTasks($form_state, $this->t('The layout override has been saved.'));
return $return;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions = $this->buildActions($actions);
$actions['delete']['#access'] = FALSE;
$actions['discard_changes']['#limit_validation_errors'] = [];
// @todo This button should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$actions['revert'] = [
'#type' => 'submit',
'#value' => $this->t('Revert to defaults'),
'#submit' => ['::redirectOnSubmit'],
'#redirect' => 'revert',
];
return $actions;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\layout_builder\Form;
/**
* Provides a trait that provides a toggle for the content preview.
*/
trait PreviewToggleTrait {
/**
* Builds the content preview toggle input.
*
* @return array
* The render array for the content preview toggle.
*/
protected function buildContentPreviewToggle() {
return [
'#type' => 'container',
'#attributes' => [
'class' => ['js-show'],
],
'toggle_content_preview' => [
'#title' => $this->t('Show content preview'),
'#type' => 'checkbox',
'#value' => TRUE,
'#attributes' => [
// Set attribute used by local storage to get content preview status.
'data-content-preview-id' => "Drupal.layout_builder.content_preview.{$this->currentUser()->id()}",
],
'#id' => 'layout-builder-content-preview',
],
];
}
/**
* Gets the current user.
*
* @return \Drupal\Core\Session\AccountInterface
* The current user.
*/
abstract protected function currentUser();
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to confirm the removal of a block.
*
* @internal
* Form classes are internal.
*/
class RemoveBlockForm extends LayoutRebuildConfirmFormBase {
/**
* The current region.
*
* @var string
*/
protected $region;
/**
* The UUID of the block being removed.
*
* @var string
*/
protected $uuid;
/**
* {@inheritdoc}
*/
public function getQuestion() {
$label = $this->sectionStorage
->getSection($this->delta)
->getComponent($this->uuid)
->getPlugin()
->label();
return $this->t('Are you sure you want to remove the %label block?', ['%label' => $label]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Remove');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_remove_block';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$this->region = $region;
$this->uuid = $uuid;
return parent::buildForm($form, $form_state, $section_storage, $delta);
}
/**
* {@inheritdoc}
*/
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
$section_storage->getSection($this->delta)->removeComponent($this->uuid);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to confirm the removal of a section.
*
* @internal
* Form classes are internal.
*/
class RemoveSectionForm extends LayoutRebuildConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_remove_section';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
$configuration = $this->sectionStorage->getSection($this->delta)->getLayoutSettings();
// Layouts may choose to use a class that might not have a label
// configuration.
if (!empty($configuration['label'])) {
return $this->t('Are you sure you want to remove @section?', ['@section' => $configuration['label']]);
}
return $this->t('Are you sure you want to remove section @section?', ['@section' => $this->delta + 1]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Remove');
}
/**
* {@inheritdoc}
*/
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
$section_storage->removeSection($this->delta);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Reverts the overridden layout to the defaults.
*
* @internal
* Form classes are internal.
*/
class RevertOverridesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new RevertOverridesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_revert_overrides';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to revert this to defaults?');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Revert');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
if (!$section_storage instanceof OverridesSectionStorageInterface) {
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide overrides', $section_storage->getStorageType(), $section_storage->getStorageId()));
}
$this->sectionStorage = $section_storage;
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Remove all sections.
$this->sectionStorage
->removeAllSections()
->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger->addMessage($this->t('The layout has been reverted back to defaults.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to update a block.
*
* @internal
* Form classes are internal.
*/
class UpdateBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_update_block';
}
/**
* Builds the block form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage being configured.
* @param int $delta
* The delta of the section.
* @param string $region
* The region of the block.
* @param string $uuid
* The UUID of the block being updated.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$component = $section_storage->getSection($delta)->getComponent($uuid);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
/**
* {@inheritdoc}
*/
protected function submitLabel() {
return $this->t('Update');
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\Form\WorkspaceSafeFormTrait as EntityWorkspaceSafeFormTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a trait that marks Layout Builder forms as workspace-safe.
*/
trait WorkspaceSafeFormTrait {
use EntityWorkspaceSafeFormTrait;
/**
* Determines whether the current form is safe to be submitted in a workspace.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* TRUE if the form is workspace-safe, FALSE otherwise.
*/
public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool {
$section_storage = $this->sectionStorage ?: $this->getSectionStorageFromFormState($form_state);
if ($section_storage) {
$context_definitions = $section_storage->getContextDefinitions();
if (!empty($context_definitions['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $section_storage->getContextValue('entity');
return $this->isWorkspaceSafeEntity($entity);
}
}
return FALSE;
}
/**
* Retrieves the section storage from a form state object, if it exists.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage or NULL if it doesn't exist.
*/
protected function getSectionStorageFromFormState(FormStateInterface $form_state): ?SectionStorageInterface {
foreach ($form_state->getBuildInfo()['args'] as $argument) {
if ($argument instanceof SectionStorageInterface) {
return $argument;
}
}
return NULL;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\SynchronizableInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class for reacting to entity events related to Inline Blocks.
*
* @internal
* This is an internal utility class wrapping hook implementations.
*/
class InlineBlockEntityOperations implements ContainerInjectionInterface {
use LayoutEntityHelperTrait;
/**
* Inline block usage tracking service.
*
* @var \Drupal\layout_builder\InlineBlockUsageInterface
*/
protected $usage;
/**
* The block content storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityOperations object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
* Inline block usage tracking service.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entityTypeManager;
$this->blockContentStorage = $entityTypeManager->getStorage('block_content');
$this->usage = $usage;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('inline_block.usage'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
/**
* Remove all unused inline blocks on save.
*
* Entities that were used in prevision revisions will be removed if not
* saving a new revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
protected function removeUnusedForEntityOnSave(EntityInterface $entity) {
// If the entity is new or '$entity->original' is not set then there will
// not be any unused inline blocks to remove.
// If this is a revisionable entity then do not remove inline blocks. They
// could be referenced in previous revisions even if this is not a new
// revision.
if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) {
return;
}
// If the original entity used the default storage then we cannot remove
// unused inline blocks because they will still be referenced in the
// defaults.
if ($this->originalEntityUsesDefaultStorage($entity)) {
return;
}
// Delete and remove the usage for inline blocks that were removed.
if ($removed_block_ids = $this->getRemovedBlockIds($entity)) {
$this->deleteBlocksAndUsage($removed_block_ids);
}
}
/**
* Gets the IDs of the inline blocks that were removed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*
* @return int[]
* The block content IDs that were removed.
*/
protected function getRemovedBlockIds(EntityInterface $entity) {
$original_sections = $this->getEntitySections($entity->original);
$current_sections = $this->getEntitySections($entity);
// Avoid un-needed conversion from revision IDs to block content IDs by
// first determining if there are any revisions in the original that are not
// also in the current sections.
$current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections);
$original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections);
if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) {
// If there are any revisions in the original that aren't in the current
// there may some blocks that need to be removed.
$current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids);
$unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids);
return array_diff($unused_original_block_content_ids, $current_block_content_ids);
}
return [];
}
/**
* Handles entity tracking on deleting a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handleEntityDelete(EntityInterface $entity) {
// @todo In https://www.drupal.org/node/3008943 call
// \Drupal\layout_builder\LayoutEntityHelperTrait::isLayoutCompatibleEntity().
$this->usage->removeByLayoutEntity($entity);
}
/**
* Handles saving a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handlePreSave(EntityInterface $entity) {
if (($entity instanceof SynchronizableInterface && $entity->isSyncing())
|| !$this->isLayoutCompatibleEntity($entity)
) {
return;
}
$duplicate_blocks = FALSE;
if ($sections = $this->getEntitySections($entity)) {
if ($this->originalEntityUsesDefaultStorage($entity)) {
// This is a new override from a default and the blocks need to be
// duplicated.
$duplicate_blocks = TRUE;
}
// Since multiple parent entity revisions may reference common block
// revisions, when a block is modified, it must always result in the
// creation of a new block revision.
$new_revision = $entity instanceof RevisionableInterface;
foreach ($this->getInlineBlockComponents($sections) as $component) {
$this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks);
}
}
$this->removeUnusedForEntityOnSave($entity);
}
/**
* Delete the inline blocks and the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
protected function deleteBlocksAndUsage(array $block_content_ids) {
foreach ($block_content_ids as $block_content_id) {
if ($block = $this->blockContentStorage->load($block_content_id)) {
$block->delete();
}
}
$this->usage->deleteUsage($block_content_ids);
}
/**
* Removes unused inline blocks.
*
* @param int $limit
* The maximum number of inline blocks to remove.
*/
public function removeUnused($limit = 100) {
$this->deleteBlocksAndUsage($this->usage->getUnused($limit));
}
/**
* Gets blocks IDs for an array of revision IDs.
*
* @param int[] $revision_ids
* The revision IDs.
*
* @return int[]
* The block IDs.
*/
protected function getBlockIdsForRevisionIds(array $revision_ids) {
if ($revision_ids) {
$query = $this->blockContentStorage->getQuery()->accessCheck(FALSE);
$query->condition('revision_id', $revision_ids, 'IN');
$block_ids = $query->execute();
return $block_ids;
}
return [];
}
/**
* Saves an inline block component.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with the layout.
* @param \Drupal\layout_builder\SectionComponent $component
* The section component with an inline block.
* @param bool $new_revision
* Whether a new revision of the block should be created when modified.
* @param bool $duplicate_blocks
* Whether the blocks should be duplicated.
*/
protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) {
/** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */
$plugin = $component->getPlugin();
$pre_save_configuration = $plugin->getConfiguration();
$plugin->saveBlockContent($new_revision, $duplicate_blocks);
$post_save_configuration = $plugin->getConfiguration();
if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) {
$this->usage->addUsage($post_save_configuration['block_id'], $entity);
}
$component->setConfiguration($post_save_configuration);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
/**
* Service class to track inline block usage.
*/
class InlineBlockUsage implements InlineBlockUsageInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Creates an InlineBlockUsage object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function addUsage($block_content_id, EntityInterface $entity) {
$this->database->merge('inline_block_usage')
->keys([
'block_content_id' => $block_content_id,
'layout_entity_id' => $entity->id(),
'layout_entity_type' => $entity->getEntityTypeId(),
])->execute();
}
/**
* {@inheritdoc}
*/
public function getUnused($limit = 100) {
$query = $this->database->select('inline_block_usage', 't');
$query->fields('t', ['block_content_id']);
$query->isNull('layout_entity_id');
$query->isNull('layout_entity_type');
return $query->range(0, $limit)->execute()->fetchCol();
}
/**
* {@inheritdoc}
*/
public function removeByLayoutEntity(EntityInterface $entity) {
$query = $this->database->update('inline_block_usage')
->fields([
'layout_entity_type' => NULL,
'layout_entity_id' => NULL,
]);
$query->condition('layout_entity_type', $entity->getEntityTypeId());
$query->condition('layout_entity_id', $entity->id());
$query->execute();
}
/**
* {@inheritdoc}
*/
public function deleteUsage(array $block_content_ids) {
if (!empty($block_content_ids)) {
$query = $this->database->delete('inline_block_usage')->condition('block_content_id', $block_content_ids, 'IN');
$query->execute();
}
}
/**
* {@inheritdoc}
*/
public function getUsage($block_content_id) {
$query = $this->database->select('inline_block_usage');
$query->condition('block_content_id', $block_content_id);
$query->fields('inline_block_usage', ['layout_entity_id', 'layout_entity_type']);
$query->range(0, 1);
return $query->execute()->fetchObject();
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines an interface for tracking inline block usage.
*/
interface InlineBlockUsageInterface {
/**
* Adds a usage record.
*
* @param int $block_content_id
* The block content ID.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function addUsage($block_content_id, EntityInterface $entity);
/**
* Gets unused inline block IDs.
*
* @param int $limit
* The maximum number of block content entity IDs to return.
*
* @return int[]
* The entity IDs.
*/
public function getUnused($limit = 100);
/**
* Remove usage record by layout entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function removeByLayoutEntity(EntityInterface $entity);
/**
* Delete the inline blocks' the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
public function deleteUsage(array $block_content_ids);
/**
* Gets usage record for inline block by ID.
*
* @param int $block_content_id
* The block content entity ID.
*
* @return object|false
* The usage record with properties layout_entity_id and layout_entity_type
* or FALSE if there is no usage.
*/
public function getUsage($block_content_id);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides methods for enabling and disabling Layout Builder.
*/
interface LayoutBuilderEnabledInterface {
/**
* Determines if Layout Builder is enabled.
*
* @return bool
* TRUE if Layout Builder is enabled, FALSE otherwise.
*/
public function isLayoutBuilderEnabled();
/**
* Enables the Layout Builder.
*
* @return $this
*/
public function enableLayoutBuilder();
/**
* Disables the Layout Builder.
*
* @return $this
*/
public function disableLayoutBuilder();
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\layout_builder;
/**
* Defines events for the layout_builder module.
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
*/
final class LayoutBuilderEvents {
/**
* Name of the event fired when a component's render array is built.
*
* This event allows modules to collaborate on creating the render array of
* the SectionComponent object. The event listener method receives a
* \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* instance.
*
* @Event
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* @see \Drupal\layout_builder\SectionComponent::toRenderArray()
*
* @var string
*/
const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array';
/**
* Name of the event fired in when preparing a layout builder element.
*
* This event allows modules to collaborate on creating the sections used in
* \Drupal\layout_builder\Element\LayoutBuilder during #pre_render.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder
*
* @var string
*/
const PREPARE_LAYOUT = 'prepare_layout';
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\layout_builder;
/**
* A trait for generating IDs used to highlight active UI elements.
*/
trait LayoutBuilderHighlightTrait {
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The section the block is in.
* @param string $region
* The section region in which the block is placed.
*
* @return string
* The highlight ID of the block.
*/
protected function blockAddHighlightId($delta, $region) {
return "block-$delta-$region";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $uuid
* The uuid of the block.
*
* @return string
* The highlight ID of the block.
*/
protected function blockUpdateHighlightId($uuid) {
return $uuid;
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionAddHighlightId($delta) {
return "section-$delta";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionUpdateHighlightId($delta) {
return "section-update-$delta";
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides an interface for displays that could be overridable.
*/
interface LayoutBuilderOverridableInterface {
/**
* Determines if the display allows custom overrides.
*
* @return bool
* TRUE if custom overrides are allowed, FALSE otherwise.
*/
public function isOverridable();
/**
* Sets the display to allow or disallow overrides.
*
* @param bool $overridable
* TRUE if the display should allow overrides, FALSE otherwise.
*
* @return $this
*/
public function setOverridable($overridable = TRUE);
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions for Layout Builder overrides.
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::access()
*
* @internal
* Dynamic permission callbacks are internal.
*/
class LayoutBuilderOverridesPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* LayoutBuilderOverridesPermissions constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The bundle info service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* Returns an array of permissions.
*
* @return string[][]
* An array whose keys are permission names and whose corresponding values
* are defined in \Drupal\user\PermissionHandlerInterface::getPermissions().
*/
public function permissions() {
$permissions = [];
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $entity_displays */
$entity_displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties(['third_party_settings.layout_builder.allow_custom' => TRUE]);
foreach ($entity_displays as $entity_display) {
$entity_type_id = $entity_display->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $entity_display->getTargetBundle();
$args = [
'%entity_type' => $entity_type->getCollectionLabel(),
'@entity_type_singular' => $entity_type->getSingularLabel(),
'@entity_type_plural' => $entity_type->getPluralLabel(),
'%bundle' => $this->bundleInfo->getBundleInfo($entity_type_id)[$bundle]['label'],
];
// These permissions are generated on behalf of $entity_display entity
// display, therefore add this entity display as a config dependency.
$dependencies = [
$entity_display->getConfigDependencyKey() => [
$entity_display->getConfigDependencyName(),
],
];
if ($entity_type->hasKey('bundle')) {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type - %bundle: Configure layout overrides for @entity_type_plural that the user can edit', $args),
'dependencies' => $dependencies,
];
}
else {
$permissions["configure all $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure all layout overrides', $args),
'warning' => $this->t('Warning: Allows configuring the layout even if the user cannot edit the @entity_type_singular itself.', $args),
'dependencies' => $dependencies,
];
$permissions["configure editable $bundle $entity_type_id layout overrides"] = [
'title' => $this->t('%entity_type: Configure layout overrides for @entity_type_plural that the user can edit', $args),
'dependencies' => $dependencies,
];
}
}
return $permissions;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
use Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Sets the layout_builder.get_block_dependency_subscriber service definition.
*
* This service is dependent on the block_content module so it must be provided
* dynamically.
*
* @internal
* Service providers are internal.
*
* @see \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency
*/
class LayoutBuilderServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$modules = $container->getParameter('container.modules');
if (isset($modules['block_content'])) {
$definition = new Definition(SetInlineBlockDependency::class);
$definition->setArguments([
new Reference('entity_type.manager'),
new Reference('database'),
new Reference('inline_block.usage'),
new Reference('plugin.manager.layout_builder.section_storage'),
]);
$definition->addTag('event_subscriber');
$definition->setPublic(TRUE);
$container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
}
if (isset($modules['serialization'])) {
$definition = (new ChildDefinition('serializer.normalizer.config_entity'))
->setClass(LayoutEntityDisplayNormalizer::class)
// Ensure that this normalizer takes precedence for Layout Builder data
// over the generic serializer.normalizer.config_entity.
->addTag('normalizer', ['priority' => 5]);
$container->setDefinition('layout_builder.normalizer.layout_entity_display', $definition);
}
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Methods to help with entities using the layout builder.
*/
trait LayoutEntityHelperTrait {
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Determines if an entity can have a layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if the entity can have a layout otherwise FALSE.
*/
protected function isLayoutCompatibleEntity(EntityInterface $entity) {
return $this->getSectionStorageForEntity($entity) !== NULL;
}
/**
* Gets revision IDs for layout sections.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return int[]
* The revision IDs.
*/
protected function getInlineBlockRevisionIdsInSections(array $sections) {
$revision_ids = [];
foreach ($this->getInlineBlockComponents($sections) as $component) {
$configuration = $component->getPlugin()->getConfiguration();
if (!empty($configuration['block_revision_id'])) {
$revision_ids[] = $configuration['block_revision_id'];
}
}
return $revision_ids;
}
/**
* Gets the sections for an entity if any.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\layout_builder\Section[]
* The entity layout sections if available.
*/
protected function getEntitySections(EntityInterface $entity) {
$section_storage = $this->getSectionStorageForEntity($entity);
return $section_storage ? $section_storage->getSections() : [];
}
/**
* Gets components that have Inline Block plugins.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return \Drupal\layout_builder\SectionComponent[]
* The components that contain Inline Block plugins.
*/
protected function getInlineBlockComponents(array $sections) {
$inline_block_components = [];
foreach ($sections as $section) {
foreach ($section->getComponents() as $component) {
$plugin = $component->getPlugin();
if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') {
$inline_block_components[] = $component;
}
}
}
return $inline_block_components;
}
/**
* Gets the section storage for an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\layout_builder\SectionStorageInterface|null
* The section storage if found otherwise NULL.
*/
protected function getSectionStorageForEntity(EntityInterface $entity) {
// @todo Take into account other view modes in
// https://www.drupal.org/node/3008924.
$view_mode = 'full';
if ($entity instanceof LayoutEntityDisplayInterface) {
$contexts['display'] = EntityContext::fromEntity($entity);
$contexts['view_mode'] = new Context(new ContextDefinition('string'), $entity->getMode());
}
else {
$contexts['entity'] = EntityContext::fromEntity($entity);
if ($entity instanceof FieldableEntityInterface) {
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
if ($display instanceof LayoutEntityDisplayInterface) {
$contexts['display'] = EntityContext::fromEntity($display);
}
$contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode);
}
}
return $this->sectionStorageManager()->findByContext($contexts, new CacheableMetadata());
}
/**
* Determines if the original entity used the default section storage.
*
* This method can be used during the entity save process to determine whether
* $entity->original is set and used the default section storage plugin as
* determined by ::getSectionStorageForEntity().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return bool
* TRUE if the original entity used the default storage.
*/
protected function originalEntityUsesDefaultStorage(EntityInterface $entity) {
$section_storage = $this->getSectionStorageForEntity($entity);
if ($section_storage instanceof OverridesSectionStorageInterface && !$entity->isNew() && isset($entity->original)) {
$original_section_storage = $this->getSectionStorageForEntity($entity->original);
return $original_section_storage instanceof DefaultsSectionStorageInterface;
}
return FALSE;
}
/**
* Gets the section storage manager.
*
* @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
* The section storage manager.
*/
private function sectionStorageManager() {
return $this->sectionStorageManager ?: \Drupal::service('plugin.manager.layout_builder.section_storage');
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\TempStore\SharedTempStoreFactory;
/**
* Provides a mechanism for loading layouts from tempstore.
*/
class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
/**
* The shared tempstore factory.
*
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The static cache of loaded values.
*
* @var \Drupal\layout_builder\SectionStorageInterface[]
*/
protected array $cache = [];
/**
* LayoutTempstoreRepository constructor.
*
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The shared tempstore factory.
*/
public function __construct(SharedTempStoreFactory $temp_store_factory) {
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public function get(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
// Check if the storage is present in the static cache.
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
$tempstore = $this->getTempstore($section_storage)->get($key);
if (!empty($tempstore['section_storage'])) {
$storage_type = $section_storage->getStorageType();
$section_storage = $tempstore['section_storage'];
if (!($section_storage instanceof SectionStorageInterface)) {
throw new \UnexpectedValueException(sprintf('The entry with storage type "%s" and ID "%s" is invalid', $storage_type, $key));
}
// Set the storage in the static cache.
$this->cache[$key] = $section_storage;
}
return $section_storage;
}
/**
* {@inheritdoc}
*/
public function has(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
// Check if the storage is present in the static cache.
if (isset($this->cache[$key])) {
return TRUE;
}
$tempstore = $this->getTempstore($section_storage)->get($key);
return !empty($tempstore['section_storage']);
}
/**
* {@inheritdoc}
*/
public function set(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
$this->getTempstore($section_storage)->set($key, ['section_storage' => $section_storage]);
// Update the storage in the static cache.
$this->cache[$key] = $section_storage;
}
/**
* {@inheritdoc}
*/
public function delete(SectionStorageInterface $section_storage) {
$key = $this->getKey($section_storage);
$this->getTempstore($section_storage)->delete($key);
// Remove the storage from the static cache.
unset($this->cache[$key]);
}
/**
* Gets the shared tempstore.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\TempStore\SharedTempStore
* The tempstore.
*/
protected function getTempstore(SectionStorageInterface $section_storage) {
$collection = 'layout_builder.section_storage.' . $section_storage->getStorageType();
return $this->tempStoreFactory->get($collection);
}
/**
* Gets the string to use as the tempstore key.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return string
* A unique string representing the section storage. This should include as
* much identifying information as possible about this particular storage,
* including information like the current language.
*/
protected function getKey(SectionStorageInterface $section_storage) {
if ($section_storage instanceof TempStoreIdentifierInterface) {
return $section_storage->getTempstoreKey();
}
return $section_storage->getStorageId();
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides an interface for loading layouts from tempstore.
*/
interface LayoutTempstoreRepositoryInterface {
/**
* Gets the tempstore version of a section storage, if it exists.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* Either the version of this section storage from tempstore, or the passed
* section storage if none exists.
*
* @throw \UnexpectedValueException
* Thrown if a value exists, but is not a section storage.
*/
public function get(SectionStorageInterface $section_storage);
/**
* Stores this section storage in tempstore.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to set in tempstore.
*/
public function set(SectionStorageInterface $section_storage);
/**
* Checks for the existence of a tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return bool
* TRUE if there is a tempstore version of this section storage.
*/
public function has(SectionStorageInterface $section_storage);
/**
* Removes the tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to remove from tempstore.
*/
public function delete(SectionStorageInterface $section_storage);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\layout_builder\Normalizer;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\serialization\Normalizer\ConfigEntityNormalizer;
/**
* Normalizes/denormalizes LayoutEntityDisplay objects into an array structure.
*
* @internal
* Tagged services are internal.
*/
class LayoutEntityDisplayNormalizer extends ConfigEntityNormalizer {
/**
* {@inheritdoc}
*/
protected static function getDataWithoutInternals(array $data) {
$data = parent::getDataWithoutInternals($data);
// Do not expose the actual layout sections in normalization.
// @todo Determine what to expose here in
// https://www.drupal.org/node/2942975.
unset($data['third_party_settings']['layout_builder']['sections']);
return $data;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
LayoutEntityDisplayInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\layout_builder;
/**
* Defines an interface for an object that stores layout sections for overrides.
*/
interface OverridesSectionStorageInterface extends SectionStorageInterface {
/**
* Returns the corresponding defaults section storage for this override.
*
* @return \Drupal\layout_builder\DefaultsSectionStorageInterface
* The defaults section storage.
*
* @todo Determine if this method needs a parameter in
* https://www.drupal.org/project/drupal/issues/2907413.
*/
public function getDefaultSectionStorage();
/**
* Indicates if overrides are in use.
*
* @return bool
* TRUE if this overrides section storage is in use, otherwise FALSE.
*/
public function isOverridden();
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block that renders an extra field from an entity.
*
* This block handles fields that are provided by implementations of
* hook_entity_extra_field_info().
*
* @see \Drupal\layout_builder\Plugin\Block\FieldBlock
* This block plugin handles all other field entities not provided by
* hook_entity_extra_field_info().
*
* @internal
* Plugin classes are internal.
*/
#[Block(
id: "extra_field_block",
deriver: ExtraFieldBlockDeriver::class
)]
class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ExtraFieldBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
// Get field name from the plugin ID.
[, , , $field_name] = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
assert(!empty($field_name));
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
'formatter' => [
'settings' => [],
'third_party_settings' => [],
],
];
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager')
);
}
/**
* Gets the entity that has the field.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity.
*/
protected function getEntity() {
return $this->getContextValue('entity');
}
/**
* {@inheritdoc}
*/
public function build() {
$entity = $this->getEntity();
// Add a placeholder to replace after the entity view is built.
// @see layout_builder_entity_view_alter().
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
if (!isset($extra_fields['display'][$this->fieldName])) {
$build = [];
}
else {
$build = [
'#extra_field_placeholder_field_name' => $this->fieldName,
// Always provide a placeholder. The Layout Builder will NOT invoke
// hook_entity_view_alter() so extra fields will not be added to the
// render array. If the hook is invoked the placeholder will be
// replaced.
// @see ::replaceFieldPlaceholder()
'#markup' => $this->t('Placeholder for the @preview_fallback', ['@preview_fallback' => $this->getPreviewFallbackString()]),
];
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
$entity = $this->getEntity();
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
return new TranslatableMarkup('"@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
}
/**
* Replaces all placeholders for a given field.
*
* @param array $build
* The built render array for the elements.
* @param array $built_field
* The render array to replace the placeholder.
* @param string $field_name
* The field name.
*
* @see ::build()
*/
public static function replaceFieldPlaceholder(array &$build, array $built_field, $field_name) {
foreach (Element::children($build) as $child) {
if (isset($build[$child]['#extra_field_placeholder_field_name']) && $build[$child]['#extra_field_placeholder_field_name'] === $field_name) {
$placeholder_cache = CacheableMetadata::createFromRenderArray($build[$child]);
$built_cache = CacheableMetadata::createFromRenderArray($built_field);
$merged_cache = $placeholder_cache->merge($built_cache);
$build[$child] = $built_field;
$merged_cache->applyTo($build);
}
else {
static::replaceFieldPlaceholder($build[$child], $built_field, $field_name);
}
}
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return $this->getEntity()->access('view', $account, TRUE);
}
}

View File

@@ -0,0 +1,441 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityDisplayBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FormatterInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Form\FormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\field\FieldLabelOptionsTrait;
/**
* Provides a block that renders a field from an entity.
*
* @internal
* Plugin classes are internal.
*/
#[Block(
id: "field_block",
deriver: FieldBlockDeriver::class
)]
class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
use FieldLabelOptionsTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The formatter manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $formatterManager;
/**
* The entity type ID.
*
* @var string
*/
protected $entityTypeId;
/**
* The bundle ID.
*
* @var string
*/
protected $bundle;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The field definition.
*
* @var \Drupal\Core\Field\FieldDefinitionInterface
*/
protected $fieldDefinition;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new FieldBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
* The formatter manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, FormatterPluginManager $formatter_manager, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
$this->entityFieldManager = $entity_field_manager;
$this->formatterManager = $formatter_manager;
$this->moduleHandler = $module_handler;
$this->logger = $logger;
// Get the entity type and field name from the plugin ID.
[, $entity_type_id, $bundle, $field_name] = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
$this->entityTypeId = $entity_type_id;
$this->bundle = $bundle;
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.formatter'),
$container->get('module_handler'),
$container->get('logger.channel.layout_builder')
);
}
/**
* Gets the entity that has the field.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity.
*/
protected function getEntity() {
return $this->getContextValue('entity');
}
/**
* {@inheritdoc}
*/
public function build() {
$display_settings = $this->getConfiguration()['formatter'];
$display_settings['third_party_settings']['layout_builder']['view_mode'] = $this->getContextValue('view_mode');
$entity = $this->getEntity();
try {
$build = [];
$view = $entity->get($this->fieldName)->view($display_settings);
if ($view) {
$build = [$view];
}
}
// @todo Remove in https://www.drupal.org/project/drupal/issues/2367555.
catch (EnforcedResponseException $e) {
throw $e;
}
catch (\Exception $e) {
$build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
}
CacheableMetadata::createFromRenderArray($build)->addCacheableDependency($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
return new TranslatableMarkup('"@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$entity = $this->getEntity();
// First consult the entity.
$access = $entity->access('view', $account, TRUE);
if (!$access->isAllowed()) {
return $access;
}
// Check that the entity in question has this field.
if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($this->fieldName)) {
return $access->andIf(AccessResult::forbidden());
}
// Check field access.
$field = $entity->get($this->fieldName);
$access = $access->andIf($field->access('view', $account, TRUE));
if (!$access->isAllowed()) {
return $access;
}
// Check to see if the field has any values or a default value.
if ($field->isEmpty() && !$this->entityFieldHasDefaultValue()) {
return $access->andIf(AccessResult::forbidden());
}
return $access;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
'formatter' => [
'label' => 'above',
'type' => $this->pluginDefinition['default_formatter'],
'settings' => [],
'third_party_settings' => [],
],
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$config = $this->getConfiguration();
$form['formatter'] = [
'#tree' => TRUE,
'#process' => [
[$this, 'formatterSettingsProcessCallback'],
],
];
$form['formatter']['label'] = [
'#type' => 'select',
'#title' => $this->t('Label'),
'#options' => $this->getFieldLabelOptions(),
'#default_value' => $config['formatter']['label'],
];
$form['formatter']['type'] = [
'#type' => 'select',
'#title' => $this->t('Formatter'),
'#options' => $this->getApplicablePluginOptions($this->getFieldDefinition()),
'#required' => TRUE,
'#default_value' => $config['formatter']['type'],
'#ajax' => [
'callback' => [static::class, 'formatterSettingsAjaxCallback'],
'wrapper' => 'formatter-settings-wrapper',
],
];
// Add the formatter settings to the form via AJAX.
$form['formatter']['settings_wrapper'] = [
'#prefix' => '<div id="formatter-settings-wrapper">',
'#suffix' => '</div>',
];
return $form;
}
/**
* Render API callback: builds the formatter settings elements.
*/
public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) {
if ($formatter = $this->getFormatter($element['#parents'], $form_state)) {
$element['settings_wrapper']['settings'] = $formatter->settingsForm($complete_form, $form_state);
$element['settings_wrapper']['settings']['#parents'] = array_merge($element['#parents'], ['settings']);
$element['settings_wrapper']['third_party_settings'] = $this->thirdPartySettingsForm($formatter, $this->getFieldDefinition(), $complete_form, $form_state);
$element['settings_wrapper']['third_party_settings']['#parents'] = array_merge($element['#parents'], ['third_party_settings']);
FormHelper::rewriteStatesSelector($element['settings_wrapper'], "fields[$this->fieldName][settings_edit_form]", 'settings[formatter]');
// Store the array parents for our element so that we can retrieve the
// formatter settings in our AJAX callback.
$form_state->set('field_block_array_parents', $element['#array_parents']);
}
return $element;
}
/**
* Adds the formatter third party settings forms.
*
* @param \Drupal\Core\Field\FormatterInterface $plugin
* The formatter.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The formatter third party settings form.
*/
protected function thirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$settings_form = [];
// Invoke hook_field_formatter_third_party_settings_form(), keying resulting
// subforms by module name.
$this->moduleHandler->invokeAllWith(
'field_formatter_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, $plugin, $field_definition, $form, $form_state) {
$settings_form[$module] = $hook(
$plugin,
$field_definition,
EntityDisplayBase::CUSTOM_MODE,
$form,
$form_state,
);
}
);
return $settings_form;
}
/**
* Render API callback: gets the layout settings elements.
*/
public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
$formatter_array_parents = $form_state->get('field_block_array_parents');
return NestedArray::getValue($form, array_merge($formatter_array_parents, ['settings_wrapper']));
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['formatter'] = $form_state->getValue('formatter');
}
/**
* Gets the field definition.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition.
*/
protected function getFieldDefinition() {
if (empty($this->fieldDefinition)) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $this->bundle);
$this->fieldDefinition = $field_definitions[$this->fieldName];
}
return $this->fieldDefinition;
}
/**
* Returns an array of applicable formatter options for a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* An array of applicable formatter options.
*
* @see \Drupal\field_ui\Form\EntityDisplayFormBase::getApplicablePluginOptions()
*/
protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
$options = $this->formatterManager->getOptions($field_definition->getType());
$applicable_options = [];
foreach ($options as $option => $label) {
$plugin_class = DefaultFactory::getPluginClass($option, $this->formatterManager->getDefinition($option));
if ($plugin_class::isApplicable($field_definition)) {
$applicable_options[$option] = $label;
}
}
return $applicable_options;
}
/**
* Gets the formatter object.
*
* @param array $parents
* The #parents of the element representing the formatter.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Field\FormatterInterface
* The formatter object.
*/
protected function getFormatter(array $parents, FormStateInterface $form_state) {
// Use the processed values, if available.
$configuration = NestedArray::getValue($form_state->getValues(), $parents);
if (!$configuration) {
// Next check the raw user input.
$configuration = NestedArray::getValue($form_state->getUserInput(), $parents);
if (!$configuration) {
// If no user input exists, use the default values.
$configuration = $this->getConfiguration()['formatter'];
}
}
return $this->formatterManager->getInstance([
'configuration' => $configuration,
'field_definition' => $this->getFieldDefinition(),
'view_mode' => EntityDisplayBase::CUSTOM_MODE,
'prepare' => TRUE,
]);
}
/**
* Checks whether there is a default value set on the field.
*
* @return bool
* TRUE if default value set, FALSE otherwise.
*/
protected function entityFieldHasDefaultValue(): bool {
$entity = $this->getEntity();
$field = $entity->get($this->fieldName);
$definition = $field->getFieldDefinition();
if ($definition->getDefaultValue($entity)) {
return TRUE;
}
// @todo Remove special handling of image fields after
// https://www.drupal.org/project/drupal/issues/3005528.
if ($definition->getType() !== 'image') {
return FALSE;
}
$default_image = $definition->getSetting('default_image');
// If we are dealing with a configurable field, look in both instance-level
// and field-level settings.
if (empty($default_image['uuid']) && ($definition instanceof FieldConfigInterface)) {
$default_image = $definition->getFieldStorageDefinition()->getSetting('default_image');
}
return !empty($default_image['uuid']);
}
}

View File

@@ -0,0 +1,299 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\block_content\Access\RefinableDependentAccessTrait;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines an inline block plugin type.
*
* @Block(
* id = "inline_block",
* admin_label = @Translation("Inline block"),
* category = @Translation("Inline blocks"),
* deriver = "Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver",
* )
*
* @internal
* Plugin classes are internal.
*/
class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface, RefinableDependentAccessInterface {
use RefinableDependentAccessTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $blockContent;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* Whether a new block is being created.
*
* @var bool
*/
protected $isNew = TRUE;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new InlineBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityDisplayRepository = $entity_display_repository;
$this->currentUser = $current_user;
if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) {
$this->isNew = FALSE;
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_display.repository'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'view_mode' => 'full',
'block_id' => NULL,
'block_revision_id' => NULL,
'block_serialized' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$block = $this->getEntity();
// Add the entity form display in a process callback so that #parents can
// be successfully propagated to field widgets.
$form['block_form'] = [
'#type' => 'container',
'#process' => [[static::class, 'processBlockForm']],
'#block' => $block,
'#access' => $this->currentUser->hasPermission('create and edit custom blocks'),
];
$options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
$form['view_mode'] = [
'#type' => 'select',
'#options' => $options,
'#title' => $this->t('View mode'),
'#description' => $this->t('The view mode in which to render the block.'),
'#default_value' => $this->configuration['view_mode'],
'#access' => count($options) > 1,
];
return $form;
}
/**
* Process callback to insert a Content Block form.
*
* @param array $element
* The containing element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The containing element, with the Content Block form inserted.
*/
public static function processBlockForm(array $element, FormStateInterface $form_state) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $element['#block'];
EntityFormDisplay::collectRenderDisplay($block, 'edit')->buildForm($block, $element, $form_state);
$element['revision_log']['#access'] = FALSE;
$element['info']['#access'] = FALSE;
return $element;
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
$block_form = $form['block_form'];
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$form_display->validateFormValues($block, $block_form, $complete_form_state);
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$form_state->setTemporaryValue('block_form_parents', $block_form['#parents']);
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['view_mode'] = $form_state->getValue('view_mode');
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$block_form = NestedArray::getValue($form, $form_state->getTemporaryValue('block_form_parents'));
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$block->setInfo($this->configuration['label']);
$this->configuration['block_serialized'] = serialize($block);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($entity = $this->getEntity()) {
return $entity->access('view', $account, TRUE);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$block = $this->getEntity();
return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']);
}
/**
* Loads or creates the block content entity of the block.
*
* @return \Drupal\block_content\BlockContentInterface
* The block content entity.
*/
protected function getEntity() {
if (!isset($this->blockContent)) {
if (!empty($this->configuration['block_serialized'])) {
$this->blockContent = unserialize($this->configuration['block_serialized']);
}
elseif (!empty($this->configuration['block_revision_id'])) {
$entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
$this->blockContent = $entity;
}
else {
$this->blockContent = $this->entityTypeManager->getStorage('block_content')->create([
'type' => $this->getDerivativeId(),
'reusable' => FALSE,
]);
}
if ($this->blockContent instanceof RefinableDependentAccessInterface && $dependee = $this->getAccessDependency()) {
$this->blockContent->setAccessDependency($dependee);
}
}
return $this->blockContent;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
if ($this->isNew) {
// If the Content Block is new then don't provide a default label.
unset($form['label']['#default_value']);
}
$form['label']['#description'] = $this->t('The title of the block as shown to the user.');
return $form;
}
/**
* Saves the block_content entity for this plugin.
*
* @param bool $new_revision
* Whether to create new revision, if the block was modified.
* @param bool $duplicate_block
* Whether to duplicate the "block_content" entity.
*/
public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = NULL;
if (!empty($this->configuration['block_serialized'])) {
$block = unserialize($this->configuration['block_serialized']);
}
if ($duplicate_block) {
if (empty($block) && !empty($this->configuration['block_revision_id'])) {
$block = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
}
if ($block) {
$block = $block->createDuplicate();
}
}
if ($block) {
// Since the content block is only set if it was unserialized, the flag
// will only effect blocks which were modified or serialized originally.
if ($new_revision) {
$block->setNewRevision();
}
$block->save();
$this->configuration['block_id'] = $block->id();
$this->configuration['block_revision_id'] = $block->getRevisionId();
$this->configuration['block_serialized'] = NULL;
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\layout_builder\Plugin\DataType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\TypedData;
use Drupal\layout_builder\Section;
/**
* Provides a data type wrapping \Drupal\layout_builder\Section.
*
* @internal
* Plugin classes are internal.
*/
#[DataType(
id: "layout_section",
label: new TranslatableMarkup("Layout Section"),
description: new TranslatableMarkup("A layout section"),
)]
class SectionData extends TypedData {
/**
* The section object.
*
* @var \Drupal\layout_builder\Section
*/
protected $value;
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
if ($value && !$value instanceof Section) {
throw new \InvalidArgumentException(sprintf('Value assigned to "%s" is not a valid section', $this->getName()));
}
parent::setValue($value, $notify);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
/**
* Provides entity field block definitions for every field.
*
* @internal
* Plugin derivers are internal.
*/
class ExtraFieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* Constructs new FieldBlockDeriver.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
* The entity type repository.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
*/
public function __construct(
EntityFieldManagerInterface $entity_field_manager,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_type_bundle_info,
EntityTypeRepositoryInterface $entity_type_repository,
protected ModuleHandlerInterface $moduleHandler,
) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityTypeRepository = $entity_type_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.repository'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
$enabled_bundle_ids = $this->bundleIdsWithLayoutBuilderDisplays();
$expose_all_fields = $this->moduleHandler->moduleExists('layout_builder_expose_all_field_blocks');
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Only process fieldable entity types.
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
continue;
}
// If not loading everything, skip entity types that aren't included.
if (!$expose_all_fields && !isset($enabled_bundle_ids[$entity_type_id])) {
continue;
}
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle_id => $bundle) {
// If not loading everything, skip bundle types that aren't included.
if (!$expose_all_fields && !isset($enabled_bundle_ids[$entity_type_id][$bundle_id])) {
continue;
}
$extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle_id);
// Skip bundles without any extra fields.
if (empty($extra_fields['display'])) {
continue;
}
foreach ($extra_fields['display'] as $extra_field_id => $extra_field) {
$derivative = $base_plugin_definition;
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $extra_field['label'];
$context_definition = EntityContextDefinition::fromEntityType($entity_type)
->addConstraint('Bundle', [$bundle_id]);
$derivative['context_definitions'] = [
'entity' => $context_definition,
];
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle_id . PluginBase::DERIVATIVE_SEPARATOR . $extra_field_id;
$this->derivatives[$derivative_id] = $derivative;
}
}
}
return $this->derivatives;
}
/**
* Gets a list of entity type and bundle tuples that have layout builder enabled.
*
* @return array
* A structured array with entity type as first key, bundle as second.
*/
protected function bundleIdsWithLayoutBuilderDisplays(): array {
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
$displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties([
'third_party_settings.layout_builder.enabled' => TRUE,
]);
$layout_bundles = [];
foreach ($displays as $display) {
$bundle = $display->getTargetBundle();
$layout_bundles[$display->getTargetEntityTypeId()][$bundle] = $bundle;
}
return $layout_bundles;
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides entity field block definitions for every field.
*
* @internal
* Plugin derivers are internal.
*/
class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
use LoggerChannelTrait;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field type manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* The formatter manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $formatterManager;
/**
* Constructs new FieldBlockDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
* The entity type repository.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type manager.
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
* The formatter manager.
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entityViewDisplayStorage
* The entity view display storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
*/
public function __construct(
EntityTypeRepositoryInterface $entity_type_repository,
EntityFieldManagerInterface $entity_field_manager,
FieldTypePluginManagerInterface $field_type_manager,
FormatterPluginManager $formatter_manager,
protected ConfigEntityStorageInterface $entityViewDisplayStorage,
protected ModuleHandlerInterface $moduleHandler,
) {
$this->entityTypeRepository = $entity_type_repository;
$this->entityFieldManager = $entity_field_manager;
$this->fieldTypeManager = $field_type_manager;
$this->formatterManager = $formatter_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.repository'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.formatter'),
$container->get('entity_type.manager')->getStorage('entity_view_display'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
foreach ($this->getFieldMap() as $entity_type_id => $entity_field_map) {
foreach ($entity_field_map as $field_name => $field_info) {
// Skip fields without any formatters.
$options = $this->formatterManager->getOptions($field_info['type']);
if (empty($options)) {
continue;
}
foreach ($field_info['bundles'] as $bundle) {
$derivative = $base_plugin_definition;
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
if (empty($field_definitions[$field_name])) {
$this->getLogger('field')->error('Field %field_name exists but is missing a corresponding field definition and may be misconfigured.', ['%field_name' => "$entity_type_id.$bundle.$field_name"]);
continue;
}
$field_definition = $field_definitions[$field_name];
// Store the default formatter on the definition.
$derivative['default_formatter'] = '';
$field_type_definition = $this->fieldTypeManager->getDefinition($field_info['type']);
if (isset($field_type_definition['default_formatter'])) {
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
}
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $field_definition->getLabel();
// Add a dependency on the field if it is configurable.
if ($field_definition instanceof FieldConfigInterface) {
$derivative['config_dependencies'][$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName();
}
// For any field that is not display configurable, mark it as
// unavailable to place in the block UI.
$derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view');
$context_definition = EntityContextDefinition::fromEntityTypeId($entity_type_id)->setLabel($entity_type_labels[$entity_type_id]);
$context_definition->addConstraint('Bundle', [$bundle]);
$derivative['context_definitions'] = [
'entity' => $context_definition,
'view_mode' => new ContextDefinition('string'),
];
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
$this->derivatives[$derivative_id] = $derivative;
}
}
}
return $this->derivatives;
}
/**
* Returns the entity field map for deriving block definitions.
*
* @return array
* The entity field map.
*
* @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldMap()
*/
protected function getFieldMap(): array {
$field_map = $this->entityFieldManager->getFieldMap();
// If all fields are exposed as field blocks, just return the field map
// without any further processing.
if ($this->moduleHandler->moduleExists('layout_builder_expose_all_field_blocks')) {
return $field_map;
}
// Load all entity view displays which are using Layout Builder.
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
$displays = $this->entityViewDisplayStorage->loadByProperties([
'third_party_settings.layout_builder.enabled' => TRUE,
]);
$layout_bundles = [];
foreach ($displays as $display) {
$bundle = $display->getTargetBundle();
$layout_bundles[$display->getTargetEntityTypeId()][$bundle] = $bundle;
}
// Process $field_map, removing any entity types which are not using Layout
// Builder.
$field_map = array_intersect_key($field_map, $layout_bundles);
foreach ($field_map as $entity_type_id => $fields) {
foreach ($fields as $field_name => $field_info) {
$field_map[$entity_type_id][$field_name]['bundles'] = array_intersect($field_info['bundles'], $layout_bundles[$entity_type_id]);
// If no bundles are using Layout Builder, remove this field from the
// field map.
if (empty($field_info['bundles'])) {
unset($field_map[$entity_type_id][$field_name]);
}
}
}
return $field_map;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides inline block plugin definitions for all block types.
*
* @internal
* Plugin derivers are internal.
*/
class InlineBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a BlockContentDeriver object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
if ($this->entityTypeManager->hasDefinition('block_content_type')) {
$block_content_types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple();
foreach ($block_content_types as $id => $type) {
$this->derivatives[$id] = $base_plugin_definition;
$this->derivatives[$id]['admin_label'] = $type->label();
$this->derivatives[$id]['config_dependencies'][$type->getConfigDependencyKey()][] = $type->getConfigDependencyName();
}
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for the layout builder user interface.
*
* @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655.
*
* @internal
* Plugin derivers are internal.
*/
class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutBuilderLocalTaskDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->sectionStorageManager = $section_storage_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
$section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
$this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
}
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\layout_builder\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\layout_builder\Field\LayoutSectionItemList;
use Drupal\layout_builder\Section;
/**
* Plugin implementation of the 'layout_section' field type.
*
* @internal
* Plugin classes are internal.
*
* @property \Drupal\layout_builder\Section $section
*/
#[FieldType(
id: "layout_section",
label: new TranslatableMarkup("Layout Section"),
description: new TranslatableMarkup("Layout Section"),
no_ui: TRUE,
list_class: LayoutSectionItemList::class,
cardinality: FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
)]
class LayoutSectionItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['section'] = DataDefinition::create('layout_section')
->setLabel(new TranslatableMarkup('Layout Section'))
->setRequired(FALSE);
return $properties;
}
/**
* {@inheritdoc}
*/
public function __get($name) {
// @todo \Drupal\Core\Field\FieldItemBase::__get() does not return default
// values for un-instantiated properties. This will forcibly instantiate
// all properties with the side-effect of a performance hit, resolve
// properly in https://www.drupal.org/node/2413471.
$this->getProperties();
return parent::__get($name);
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'section';
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$schema = [
'columns' => [
'section' => [
'type' => 'blob',
'size' => 'normal',
'serialize' => TRUE,
],
],
];
return $schema;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// @todo Expand this in https://www.drupal.org/node/2912331.
$values['section'] = new Section('layout_onecol');
return $values;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->section);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\layout_builder\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* A widget to display the layout form.
*
* @internal
* Plugin classes are internal.
*/
#[FieldWidget(
id: 'layout_builder_widget',
label: new TranslatableMarkup('Layout Builder Widget'),
description: new TranslatableMarkup('A field widget for Layout Builder.'),
field_types: ['layout_section'],
multiple_values: TRUE,
)]
class LayoutBuilderWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element += [
'#type' => 'layout_builder',
'#section_storage' => $this->getSectionStorage($form_state),
];
$element['#process'][] = [static::class, 'layoutBuilderElementGetKeys'];
return $element;
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// @todo This isn't resilient to being set twice, during validation and
// save https://www.drupal.org/project/drupal/issues/2833682.
if (!$form_state->isValidationComplete()) {
return;
}
$items->setValue($this->getSectionStorage($form_state)->getSections());
}
/**
* Gets the section storage.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage loaded from the tempstore.
*/
private function getSectionStorage(FormStateInterface $form_state) {
return $form_state->getFormObject()->getSectionStorage();
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a layout plugin that produces no output.
*
* @see \Drupal\layout_builder\Field\LayoutSectionItemList::removeSection()
* @see \Drupal\layout_builder\SectionListTrait::addBlankSection()
* @see \Drupal\layout_builder\SectionListTrait::hasBlankSection()
*
* @internal
* This layout plugin is intended for internal use by Layout Builder only.
*/
#[Layout(
id: 'layout_builder_blank',
label: new TranslatableMarkup('Blank'),
)]
class BlankLayout extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function build(array $regions) {
// Return no output.
return [];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Base class of layouts with configurable widths.
*/
abstract class MultiWidthLayoutBase extends LayoutDefault implements PluginFormInterface {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = parent::defaultConfiguration();
return $configuration + [
'column_widths' => $this->getDefaultWidth(),
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['column_widths'] = [
'#type' => 'select',
'#title' => $this->t('Column widths'),
'#default_value' => $this->configuration['column_widths'],
'#options' => $this->getWidthOptions(),
'#description' => $this->t('Choose the column widths for this layout.'),
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['column_widths'] = $form_state->getValue('column_widths');
}
/**
* {@inheritdoc}
*/
public function build(array $regions) {
$build = parent::build($regions);
$build['#attributes']['class'] = [
'layout',
$this->getPluginDefinition()->getTemplate(),
$this->getPluginDefinition()->getTemplate() . '--' . $this->configuration['column_widths'],
];
return $build;
}
/**
* Gets the width options for the configuration form.
*
* The first option will be used as the default 'column_widths' configuration
* value.
*
* @return string[]
* The width options array where the keys are strings that will be added to
* the CSS classes and the values are the human readable labels.
*/
abstract protected function getWidthOptions();
/**
* Provides a default value for the width options.
*
* @return string
* A key from the array returned by ::getWidthOptions().
*/
protected function getDefaultWidth() {
// Return the first available key from the list of options.
$width_classes = array_keys($this->getWidthOptions());
return array_shift($width_classes);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
/**
* Configurable three column layout plugin class.
*
* @internal
* Plugin classes are internal.
*/
class ThreeColumnLayout extends MultiWidthLayoutBase {
/**
* {@inheritdoc}
*/
protected function getWidthOptions() {
return [
'25-50-25' => '25%/50%/25%',
'33-34-33' => '33%/34%/33%',
'25-25-50' => '25%/25%/50%',
'50-25-25' => '50%/25%/25%',
];
}
/**
* {@inheritdoc}
*/
protected function getDefaultWidth() {
return '33-34-33';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder\Plugin\Layout;
/**
* Configurable two column layout plugin class.
*
* @internal
* Plugin classes are internal.
*/
class TwoColumnLayout extends MultiWidthLayoutBase {
/**
* {@inheritdoc}
*/
protected function getWidthOptions() {
return [
'50-50' => '50%/50%',
'33-67' => '33%/67%',
'67-33' => '67%/33%',
'25-75' => '25%/75%',
'75-25' => '75%/25%',
];
}
/**
* {@inheritdoc}
*/
protected function getDefaultWidth() {
return '50-50';
}
}

Some files were not shown because too many files have changed in this diff Show More