Add theme template suggestions for block typesApril 1, 2022
Create a pre-process function to add theme template suggestions for custom block types at block-level.
/**
* Implements hook_theme_suggestions_HOOK_alter() for form templates.
* @param array $suggestions
* @param array $variables
*/
function THEMENAME_theme_suggestions_block_alter(array &$suggestions, array $variables) {
// Block suggestions for custom block bundles.
if (isset($variables['elements']['content']['#block_content'])) {
array_splice($suggestions, 1, 0, 'block__bundle__' . $variables['elements']['content']['#block_content']->bundle());
}
}
This snippet was extracted from drupal.org forum:
Create a full width divFebruary 27, 2022
This tip will help you to create a full width element. It does not matter if the element is inside other element.
.w-full-container {
left: 50%;
margin-left: -50vw;
margin-right: -50vw;
position: relative;
right: 50%;
width: 100vw;
}
<section class="w-full-container">
This is my full width element
</section>
Convert links entities into objectFebruary 27, 2022
Create a pre-process to intercept a paragraph and transform a list of LinkItem into an object to use it in the twig template.
The following code uses
<my-theme-name>
, replace with your theme name.
function <my-theme-name>_preprocess_paragraph(&$variables) {
$paragraph = $variables['elements']['#paragraph'];
$paragraph_type = $paragraph->getParagraphType();
switch ($paragraph_type->id) {
case 'links_list':
$menu = $paragraph->field_menu;
$menu_items = [];
if (count($menu)) {
foreach ($menu as $menu_item) {
/* @var $menu_item DrupallinkPluginFieldFieldTypeLinkItem */
$values = $menu_item->getValue();
$link_title = isset($values['title']) ? $values['title'] : '';
$item = [
'url' => $menu_item->getUrl()->toString(),
'text' => $link_title,
'isExternal' => $menu_item->getUrl()->isExternal(),
];
$menu_items[] = $item;
}
}
$variables['menu_items'] = $menu_items;
break;
}
}
}
And in your twig template you can do something like:
{% if menu_items %}
{% set menu_markup %}
<ul class="menu list-unstyled">
{% set link_attributes = create_attribute().setAttribute('rel', 'nofollow noopener') %}
{% for link in menu_items %}
<li>
<a class="d-flex" href="{{ link.url }}"{{ link.isExternal ? link_attributes: '' }}>
{{ link.text }}
{{ arrow_icon }}
</a>
</li>
{% endfor %}
</ul>
{% endset %}
{% endif %}
# Print menu list
{{menu_markup}}
Get the parent node by using VanillaJSFebruary 27, 2022
Some utilities to select elements by using some short-hand and safe functions.
/**
* Get the node's parent element.
* @param element HTMLElement
* @param classOrId
* @returns {null|HTMLElement}
*/
const getParentNode = (element, classOrId) => {
try {
let parent = element.parentNode;
if (!classOrId) {
return parent;
}
while (parent) {
if (parent.classList.contains(classOrId) || parent.id === classOrId) {
return parent;
}
parent = parent.parentNode;
}
} catch (e) {
return null;
}
return null;
};
This snippet was extracted from StackOverflow answers:
Is a touch device?February 27, 2022
Verify if the current user's device is a touchscreen
/**
* Verify if the current browser is a touch device
* @returns {boolean}
*/
const isTouchDevice = () => (('ontouchstart' in window)
|| (navigator.maxTouchPoints > 0)
|| (navigator.msMaxTouchPoints > 0));
This snippet was extracted from StackOverflow answers:
Select elements by using VanillaJSFebruary 27, 2022
Some utilities to select elements by using some short-hand and safe functions.
/**
* Get Element by using vanillaJS
* @param selector selector like string. e.g: ".my-element", "#my-element"
* @param parent Get the element inside another element
* @returns {null|HTMLElement}
*/
const qs = (selector, parent) => {
try {
return parent
? parent.querySelector(selector)
: document.querySelector(selector);
} catch (e) {
return null;
}
};
/**
* Get Elements by using vanillaJS
* @param selector selector like string. e.g: ".my-element", "#my-element"
* @param parent Get the element inside another element
* @returns {null|*|NodeListOf<*>}
*/
const qsa = (selector, parent) => {
try {
return parent
? parent.querySelectorAll(selector)
: document.querySelectorAll(selector);
} catch (e) {
return null;
}
};
/**
* getElementById short-hand function
* @param id
* @returns {HTMLElement}
*/
const byId = (id) => document.getElementById(id);
This snippet was extracted from StackOverflow answers:
usePagination hookFebruary 27, 2022
With this React hook you can manipulate items pagination.
import { Reducer, useEffect, useReducer } from 'react';
export const DEFAULT_ITEMS_PER_PAGE = 8;
export interface PaginationState<ItemsType = any> {
totalPages: number;
currentPage: number;
items: ItemsType[];
itemsPerPage: number;
}
export enum PaginationStateActionType {
NEXT_PAGE = 'NEXT_PAGE',
PREV_PAGE = 'PREV_PAGE',
GOTO_PAGE = 'GOTO_PAGE',
SET_ITEMS = 'SET_ITEMS',
}
export interface PaginationStateActionPayload<ItemsType = any> {
pageNumber?: number;
newItems?: ItemsType[];
}
export interface PaginationStateAction<ItemsType = any> {
type: PaginationStateActionType;
payload?: PaginationStateActionPayload<ItemsType>;
}
const reducer = (state: PaginationState, action: PaginationStateAction) => {
const {
currentPage,
itemsPerPage,
totalPages,
} = state;
const {
type,
payload: {
pageNumber,
newItems = [],
} = {},
} = action;
const nextPage = type === PaginationStateActionType.NEXT_PAGE
? currentPage + 1
: currentPage - 1;
switch (type) {
case PaginationStateActionType.NEXT_PAGE: {
if (nextPage <= totalPages) {
return { ...state, currentPage: nextPage };
}
return state;
}
case PaginationStateActionType.PREV_PAGE: {
if (nextPage > 0) {
return { ...state, currentPage: nextPage };
}
return state;
}
case PaginationStateActionType.GOTO_PAGE: {
if (pageNumber && pageNumber > 0 && pageNumber <= totalPages) {
return { ...state, currentPage: pageNumber };
}
return state;
}
case PaginationStateActionType.SET_ITEMS:
return {
...state,
items: newItems,
totalPages: newItems && Math.ceil(newItems.length / itemsPerPage),
currentPage: 1,
};
default:
return state;
}
};
const initialPaginationState: PaginationState = {
currentPage: 1,
items: [],
totalPages: 0,
itemsPerPage: DEFAULT_ITEMS_PER_PAGE,
};
const usePagination = <ItemsType>(
elements: ItemsType[],
itemsPerPage: number = DEFAULT_ITEMS_PER_PAGE,
) => {
const totalPages = elements && Math.ceil(elements.length / itemsPerPage);
const [state, dispatch] = useReducer<Reducer<
PaginationState<ItemsType>,
PaginationStateAction<ItemsType>
>>(
reducer,
{
...initialPaginationState,
totalPages,
items: elements,
itemsPerPage,
},
);
const {
currentPage,
items,
} = state;
const currentPageIndex = currentPage - 1;
const pageElements = items.slice(currentPageIndex * itemsPerPage, currentPage * itemsPerPage);
const nextPage = () => dispatch({ type: PaginationStateActionType.NEXT_PAGE });
const prevPage = () => dispatch({ type: PaginationStateActionType.PREV_PAGE });
const gotToPage = (pageNumber: number) => dispatch({
type: PaginationStateActionType.GOTO_PAGE,
payload: {
pageNumber,
},
});
useEffect(() => {
dispatch({
type: PaginationStateActionType.SET_ITEMS,
payload: {
newItems: elements,
},
});
}, [elements]);
return {
currentPage,
currentPageIndex,
pageElements,
totalPages,
isFirstPage: currentPage === 1,
isLastPage: currentPage === totalPages,
gotToPage,
nextPage,
prevPage,
};
};
export default usePagination;
useBreakpoint hookFebruary 22, 2022
Useful to know if the current window screen size match in determinate breakpoint
import { useEffect, useState } from 'react';
interface IMediaQuery {
name: string
mq: string
setStateFunction(newState:boolean): void;
currentState: boolean;
}
const useBreakpoint = () => {
const [isMobile, setIsMobile] = useState(false);
const [isTablet, setIsTablet] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
const mediaQueries:IMediaQuery[] = [
{
name: 'mobile', mq: '(max-width: 767px)', currentState: isMobile, setStateFunction: setIsMobile,
},
{
name: 'tablet', mq: '(min-width: 768px) and (max-width: 1023px)', currentState: isTablet, setStateFunction: setIsTablet,
},
{
name: 'desktop', mq: '(min-width: 1024px)', currentState: isDesktop, setStateFunction: setIsDesktop,
},
];
useEffect(() => {
const mqls:MediaQueryList[] = [];
const onChange = (event: any) => {
const { media, matches } = event?.currentTarget as MediaQueryList || event;
const currentMediaQuery = mediaQueries.find((item) => item.mq === media);
if (currentMediaQuery) {
const { currentState, setStateFunction } = currentMediaQuery;
if (typeof setStateFunction === 'function') {
if (matches !== currentState) {
setStateFunction(matches);
}
}
}
};
mediaQueries.forEach((item) => {
const { mq } = item;
if (mq) {
const mql = window.matchMedia(mq);
mql.addEventListener('change', onChange);
mqls.push(mql);
onChange(mql);
}
});
return () => {
mqls.forEach((mql) => mql.removeEventListener('change', onChange));
};
});
return {
isMobile,
isTablet,
isDesktop,
};
};
export default useBreakpoint;
CRACO config to prevent dynamic js/cssFebruary 21, 2022
Override CRA configuration in order to know the output asset name.
module.exports = {
webpack: {
configure: {
output: {
// publicPath: process.env.REACT_APP_BASE_URL || '/',
filename: 'js/my-js.js',
},
optimization: {
runtimeChunk: false,
splitChunks: {
chunks() {
return false
},
},
},
},
alias: {
"@atoms": "components/atoms/",
"@icons": "images/icons/",
"@molecules": "components/molecules/",
"@organisms": "components/organisms/",
"@pages": "pages/",
"@templates": "components/templates/",
},
},
plugins: [
{
plugin: {
overrideWebpackConfig: ({ webpackConfig }) => {
webpackConfig.plugins[5].options.filename = 'css/my-css.css';
return webpackConfig;
},
},
options: {}
}
],
}
More info
SVG ToolsFebruary 21, 2022
Compress SVGs
Make SVG Sprites
Dev Mode in DrupalFebruary 20, 2022
You should create or modify the following files:
<your-site-docroot>sites/local.services.yml
parameters:
http.response.debug_cacheability_headers: true
twig.config:
debug: true
auto_reload: true
cache: false
services:
cache.backend.null:
class: DrupalCoreCacheNullBackendFactory
<your-site-docroot>sites/default/settings/local.settings.php
$settings['kint_maxLevels'] = '4';
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
More documentation in:
Library Override in DrupalFebruary 20, 2022
<your-site-docroot>themes/custom/<your-theme>/<your-theme>.yml
libraries-override:
# Replace an entire library.
core/drupal.collapse: mytheme/collapse
# Replace an asset with another.
subtheme/library:
css:
theme:
css/layout.css: css/my-layout.css
# Remove an asset.
drupal/dialog:
css:
theme:
dialog.theme.css: false
# Remove an entire library.
core/modernizr: false
More documentation in:
Component with Children and Classname interfacesFebruary 20, 2022
import { ReactNode } from 'react';
export type ClassNameType = string | undefined;
export type Children = ReactNode | JSX.Element | JSX.Element[] | string | number | null;
export interface WithChildren {
children?: Children;
}
export interface WithClassName {
className?: ClassNameType;
}
export interface DefaultComponent extends WithChildren, WithClassName {
}
Manipulate attributes in twig templateFebruary 19, 2022
{% set my_attribute = create_attribute() %}
{%
set my_classes = [
'kittens',
'llamas',
'puppies',
]
%}
<div{{ my_attribute.addClass(my_classes).setAttribute('id', 'myUniqueId') }}>
{{ content }}
</div>
# Other useful methods
# Add single class: attributes.addClass('<className>')
# Remove Class: attributes.removeClass()
# Remove Attribute: attributes.removeAttribute('<attributeName>')
# Verify if an element has a class: attributes.hasClass('<className>')
# Access to attribute key: attributes.<key>. e.g. attribute.style, attribute['attribute-name']
More documentation in:
Browser detectorFebruary 19, 2022
How to detect the current user browser.
((window, navigator) => {
const isOpera = typeof window.opr !== 'undefined';
const isFirefox = typeof InstallTrigger !== 'undefined';
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification));
const isIE = /*@cc_on!@*/false || !!document.documentMode;
const isChromium = window.chrome;
const isEdge = navigator.userAgent.indexOf('Edg') > -1;
const vendorName = navigator.vendor;
const isChrome = isChromium !== null && typeof isChromium !== 'undefined' && vendorName === 'Google Inc.' && isOpera === false && isEdge === false;
const isBlink = (isChrome || isOpera) && !!window.CSS;
var isIOSChrome = navigator.userAgent.match("CriOS");
window.browser = {
isOpera,
isFirefox,
isSafari,
isIE,
isEdge,
isChrome,
isIOSChrome,
isBlink,
};
})(window, navigator || {});
This snippet was extracted from StackOverflow answers:
Load scripts asyncFebruary 19, 2022
const LOADING_STATE = 'loading';
const LOADED_STATE = 'loaded';
const LOADING_INTERVAL_TIME = 200;
const MAX_LOADING_RETRY = 10;
/**
* Append node to header element
*
* @param element
*/
const appendToHeader = (element) => {
const headers = document.getElementsByTagName('header');
const header = headers && headers.length && headers[0];
if (header) {
header.appendChild(element);
}
};
/**
* Verify is a library was loaded before
*
* @param id Library id
* @returns {*|null}
*/
const verifyLibraryLoaded = (id) => {
const { dataset = {} } = document.documentElement;
const state = dataset[id];
return state || null;
};
/**
* Load script in async way
*
* @param source
* @param id
* @returns {Promise<boolean>}
*/
const loadScript = (source, id) => {
const state = verifyLibraryLoaded(id);
if (state) {
if (state === LOADING_STATE) {
/* Waiting 10 times the library loads */
return new Promise((resolve, reject) => {
let counter = 0;
let intervalId;
const verify = () => {
const currentState = verifyLibraryLoaded(id);
if (currentState === LOADED_STATE) {
clearInterval(intervalId);
return resolve(true);
}
if (counter === MAX_LOADING_RETRY) {
reject(false);
}
counter++;
};
intervalId = setInterval(verify, LOADING_INTERVAL_TIME);
});
} else if (state === LOADED_STATE) {
/* return and execute */
return Promise.resolve(true);
}
}
const script = document.createElement('script');
script.src = source;
if (id) {
document.documentElement.dataset[id] = LOADING_STATE;
}
return new Promise((resolve, reject) => {
script.addEventListener('error', () => reject(false));
script.addEventListener('load', () => {
if (id) {
document.documentElement.dataset[id] = LOADED_STATE;
}
resolve(true);
});
appendToHeader(script);
});
};
Load styles asyncFebruary 19, 2022
const LOADED_STATE = 'loaded';
/**
* Append node to header element
*
* @param element
*/
const appendToHeader = (element) => {
const headers = document.getElementsByTagName('header');
const header = headers && headers.length && headers[0];
if (header) {
header.appendChild(element);
}
};
/**
* Verify is a library was loaded before
*
* @param id Library id
* @returns {*|null}
*/
const verifyLibraryLoaded = (id) => {
const { dataset = {} } = document.documentElement;
const state = dataset[id];
return state || null;
};
/**
* Load stylesheet in async way
*
* @param source stylesheet url
* @param id resource id
* @returns {Promise<boolean>}
*/
const loadStyles = (source, id) => {
const state = verifyLibraryLoaded(id);
if (state && state === LOADED_STATE) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
const resource = document.createElement('link');
resource.rel = 'stylesheet';
resource.href = source;
resource.addEventListener('load', () => {
resolve(true);
});
appendToHeader(resource);
});
};