Skip to content

Instantly share code, notes, and snippets.

@ahwayakchih
Last active October 2, 2017 13:44
Show Gist options
  • Save ahwayakchih/b8131fb788a0f05a7fd2e5b4ef68065d to your computer and use it in GitHub Desktop.
Save ahwayakchih/b8131fb788a0f05a7fd2e5b4ef68065d to your computer and use it in GitHub Desktop.
Drupal - add "add", "edit" and "remove" buttons next to the autocomplete inputs of entityreference fields.
<!-- EXAMPLE of type information that will be used by "Add" buttons, if available:
<script type="application/json" id="entityreference-types">
{
"types": {
"blockquote": "Blockquote",
"custom-page": "Page",
"product": "Product",
"simple-picture": "Simple Picture"
},
"fields": {
"field-404-background-image": [
"simple-picture"
],
"field-background-image": [
"simple-picture"
],
"field-button-link": [
"custom-page",
"product"
],
"field-featured-strip": [
"blockquote",
"custom-page",
"simple-picture"
],
"field-gallery": [
"simple-picture"
]
}
}
</script>
-->
<script>
(function () {
// fieldTypes data must be added to HTML elsewhere.
// It should be in a `script` tag, with type `application/json` and `id` set to "entityreference-types".
// See example above.
const fieldTypes = (function(id) {
const data = document.querySelector('script[id="entityreference-types"][type="application/json"]');
if (!data) {
return {};
}
// Strip any comments that Drupal might have added
const source = data.textContent.replace(/^[^{]*/, '').replace(/[^}]*$/, '');
try {
const info = JSON.parse(source);
return Object.keys(info.fields).reduce((result, field) => {
result[field.replace(/_/g, '-')] = info.fields[field].reduce((acc, type) => {
const label = info.types[type];
if (label) {
acc[type.replace(/_/g, '-')] = label;
}
return acc;
}, {});
return result;
}, {});
}
catch (e) {
console.warn('Could not parse entityreference-types data', e);
return {};
}
})('entityreference-types');
const fieldObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes) {
initializeAutocompleteButtons();
}
});
});
const fieldObserverConfig = { attributes: true, childList: true, characterData: true };
const locale = (function() {
// Get region if available
// Then node/ID/edit
// Then language code, if available. Since Drupal may add some other stuff there,
// like 'add' or 'add/en/ar-SA', we have to be strict about what we accept as a language code,
// and select only from the last `/whatever` and only `2-letter(optional: -[2-letter])`.
const match = document.location.pathname.match(/(?:([^/]+)\/)?node\/\d+\/edit(?:[\w\W]*\/([^/]{2}(?:-[^/]{2})?)(?:\/|$))?/);
return {
region: (match && match[1]) || '',
lang: (match && match[2]) || '',
};
})();
function throttle (f, delay) {
var timeout = null;
return function () {
if (timeout) {
return;
}
timeout = setTimeout(() => {
timeout = null;
f();
}, delay);
}
}
function onWindowEveryLoad (targetWindow, callback) {
let interval;
const onload = function () {
this.onpageshow = null;
this.onbeforeunload = onunload;
callback(this);
}
const onunload = function () {
if (!this.closed) {
watcher(this);
}
}
const watcher = function (w) {
const href = w.location.href;
if (interval) {
clearInterval(interval);
}
interval = setInterval(() => {
if (!w.closed && w.location.href === href) {
return;
}
if (w.closed) {
w = null;
}
else {
w.onpageshow = onload;
if (w.document && w.document.readyState !== 'loading') {
// Dispatch early to make sure our `onload` will be called,
// just in case we're too late with setting up `onpageshow`.
w.dispatchEvent(new Event('pageshow'));
}
}
clearInterval(interval);
interval = null;
}, 60);
}
targetWindow.onpageshow = onload;
}
function getSanitizedURL (url) {
return url && url.replace(/\/+/g, '/');
}
function getNIDFromAutocompleteValue (value) {
const match = value && value.match(/\((\d+)\)\s*("|$)/);
return match && match[1];
}
function getTypesReferencedByField (field) {
const fieldName = field.id.replace(/^edit-/, '');
const types = fieldTypes[fieldName];
if (!types) {
return [];
}
return Object.keys(types).map(type => ({
name: type,
label: types[type],
}));
}
function updateAddButtonHrefWithValue (field, button, value) {
if (value || !field.classList.contains('can-add-new')) {
button.classList.add('disabled');
}
else {
button.classList.remove('disabled');
}
}
function createAddButton (field, input) {
const allowedTypes = getTypesReferencedByField(field);
if (!allowedTypes || allowedTypes.length < 1) {
return null;
}
const button = document.createElement('span');
button.classList.add('form-edit-entityreference', 'form-submit', 'add-submit');
button.appendChild(document.createTextNode('Add'));
if (allowedTypes.length > 0) {
const ul = document.createElement('ul');
ul.classList.add('select-content-type');
allowedTypes.forEach(type => {
const li = document.createElement('li');
li.classList.add('option');
const a = document.createElement('a');
a.appendChild(document.createTextNode(type.label));
a.classList.add('value');
a.setAttribute('href', getSanitizedURL(`/${locale.region}/node/add/${type.name}/${locale.lang}`));
a.setAttribute('target', '_blank');
a.setAttribute('data-type', type.name);
li.appendChild(a);
ul.appendChild(li);
});
field.classList.add('can-add-new');
button.appendChild(ul);
}
button.onclick = function (event) {
event.preventDefault();
event.stopPropagation();
const a = event.target;
const href = a.getAttribute('href');
const target = a.getAttribute('target') || '_blank';
const type = a.getAttribute('data-type') || (href && href.replace(/^[\w\W]*\/node\/add\/([^/]+)[\w\W]*$/, '$1'));
if (!href) {
return;
}
onWindowEveryLoad(window.open(href, target), function (w) {
if (!w.document.body.classList.contains(`node-type-${type}`)) {
if (!w.document.body.classList.contains(`page-node-add-${type}`)) {
w.close();
}
return;
}
const title = w.document.querySelector('h1.page-title');
const links = w.document.querySelectorAll('#navigation .tabs.primary a');
const nid = Array.from(links).reduce((acc, link) => {
const href = link.getAttribute('href');
const m = href && href.match(/\/node\/(\d+)\/edit(\/|$)/);
return (m && m[1]) || acc;
}, '');
if (title && nid) {
input.value = `${title.textContent} (${nid})`;
const e = new Event('change');
input.dispatchEvent(e);
w.close();
const addMore = field.querySelector('input.field-add-more-submit');
if (addMore) {
addMore.dispatchEvent(new Event('mousedown'));
}
}
});
};
const updater = throttle(() => updateAddButtonHrefWithValue(field, button, input.value), 50);
input.addEventListener('change', event => updater());
input.addEventListener('keyup', event => updater());
updateAddButtonHrefWithValue(field, button, input.value);
return button;
}
function updateEditButtonHrefWithValue (field, button, value) {
const nid = getNIDFromAutocompleteValue(value);
if (nid) {
button.setAttribute('href', getSanitizedURL(`/${locale.region}/node/${nid}/edit/${locale.lang}`));
button.classList.remove('disabled');
}
else {
button.setAttribute('href', '');
button.classList.add('disabled');
}
}
function createEditButton (field, input) {
const button = document.createElement('a');
button.classList.add('form-edit-entityreference', 'form-submit', 'edit-submit');
button.setAttribute('href', '');
button.setAttribute('target', '_blank');
button.appendChild(document.createTextNode('Edit'));
button.onclick = function (event) {
const href = this.getAttribute('href');
if (!href) {
event.preventDefault();
event.stopPropagation();
return;
}
};
const updater = throttle(() => updateEditButtonHrefWithValue(field, button, input.value), 50);
input.addEventListener('change', event => updater());
input.addEventListener('keyup', event => updater());
updateEditButtonHrefWithValue(field, button, input.value);
return button;
}
function updateRemoveButtonHrefWithValue (field, button, value) {
const nid = getNIDFromAutocompleteValue(value);
if (nid) {
button.setAttribute('href', getSanitizedURL(`/${locale.region}/node/${nid}/translate/delete/${locale.lang}`));
button.classList.remove('disabled');
}
else {
button.setAttribute('href', '');
button.classList.add('disabled');
}
}
function createRemoveButton (field, input) {
const id = input.id;
const button = document.createElement('a');
button.classList.add('form-edit-entityreference', 'form-submit', 'edit-delete');
button.setAttribute('href', '');
button.setAttribute('target', '_blank');
button.appendChild(document.createTextNode('Remove'));
button.onclick = function (event) {
const href = this.getAttribute('href');
event.preventDefault();
event.stopPropagation();
input.value = '';
input.dispatchEvent(new Event('change'));
const tr = input.closest && input.closest('.field-multiple-table tr');
if (tr) {
tr.style.display = 'none';
}
};
const updater = throttle(() => updateRemoveButtonHrefWithValue(field, button, input.value), 50);
input.addEventListener('change', event => updater());
input.addEventListener('keyup', event => updater());
updateRemoveButtonHrefWithValue(field, button, input.value);
return button;
}
function initializeFieldInputButtons (field, input) {
if (input.classList.contains('autocompleteButtonsReady')) {
return;
}
input.classList.add('autocompleteButtonsReady');
const nextSibling = input.nextSibling;
const addButton = createAddButton(field, input);
if (addButton) {
input.parentNode.insertBefore(addButton, nextSibling);
}
input.parentNode.insertBefore(createEditButton(field, input), nextSibling);
input.parentNode.insertBefore(createRemoveButton(field, input), nextSibling);
}
function initializeFieldButtons (field) {
Array.from(field.querySelectorAll('input.form-autocomplete'))
.forEach(input => initializeFieldInputButtons(field, input));
}
function initializeFieldDescriptionLinks (field) {
const description = field.querySelector('.form-item > .description');
if (!description) {
return;
}
const links = description.querySelectorAll('a[href^="/node/add/"][target=_blank]');
if (!links) {
return;
}
Array.from(links).forEach(link => {
link.setAttribute('href', getSanitizedURL(`/${locale.region}/${link.getAttribute('href')}`));
});
}
function initializeAutocompleteButtons () {
const references = document.querySelectorAll('form.node-form .field-type-entityreference');
Array.from(references).forEach(field => {
fieldObserver.observe(field, fieldObserverConfig);
initializeFieldButtons(field);
initializeFieldDescriptionLinks(field);
});
}
function initialize () {
initializeAutocompleteButtons();
}
initialize();
})();
</script>
<style>
.field-type-entityreference .form-edit-entityreference.form-submit,
.field-type-entityreference .form-edit-entityreference.form-submit .select-content-type {
cursor: pointer;
border-radius: 0;
border: none;
padding: 5px 10px;
margin-left: 1em;
color: #fff;
text-align: center;
font-weight: normal;
font-size: 1.077em;
font-family: "Lucida Grande",Verdana,sans-serif;
background: #e3e3e3;
transition: all 0.3s;
text-decoration: none;
vertical-align: middle;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit {
display: none;
cursor: context-menu;
position: relative;
background: #0074bd;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit:hover {
z-index: 2;
}
.field-type-entityreference.can-add-new .form-edit-entityreference.form-submit.add-submit {
display: inline;
}
.field-type-entityreference.can-add-new .form-edit-entityreference.form-submit.add-submit.disabled {
display: none;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit:after {
display: inline-block;
content: '»';
width: 1em;
margin-left: 0.3em;
vertical-align: text-bottom;
transform: rotate(0);
transition: all 0.3s;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit:hover:after {
transform: rotate(90deg);
}
.field-type-entityreference .form-edit-entityreference.form-submit.edit-submit {
background: #4d8f46;
}
.field-type-entityreference .form-edit-entityreference.form-submit.edit-delete {
background: #b73939;
}
.field-type-entityreference .form-edit-entityreference.form-submit.disabled {
cursor: default;
background: #e3e3e3;
opacity: 0.5;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit .select-content-type {
display: none;
position: absolute;
right: 0;
margin: 0;
padding: 0;
background: #0074BD;
text-align: left;
list-style: none;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit .select-content-type .value {
display: inline-block;
cursor: pointer;
width: 100%;
height: 100%;
padding: 5px 10px;
color: #fff;
white-space: nowrap;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit .select-content-type .option:nth-child(odd) .value {
background: rgba(255, 255, 255, 0.1);
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit:hover .select-content-type {
display: block;
}
.field-type-entityreference .form-edit-entityreference.form-submit.add-submit.disabled:hover .select-content-type {
display: none;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment