Sitevision development

This bookmarklet toggles three handy Sitevision functionalities:

  • Profiling: Displays a table with render times for all portlets and modules on the page, allowing you to evaluate performance.
  • JSDebug: Disables asset minification and bundling, revealing the original filenames for stylesheets (CSS) and client scripts (JavaScript), useful for pinpointing source files.
  • Version: Switches the view to the offline version of the page, showing the draft in the CMS without the editor interface. Ideal for previewing unpublished content.

The Version switch is only shown when you are logged in since the offline version requires you to be authenticated.

Screenshot with the bookmarklet's modal in view. Screenshot with profiling activated.
Created
2024-10-20
Bookmarklet size
6.98 kB

Drag this link to your bookmarks to save it.

Sitevision development

Bookmarklet code

sitevision-development

import createElement from '../../_lib/shared/create-element';
import { getInstance } from '../../_lib/shared/Dialog';
import Switch from '../../_lib/shared/Switch';

((window, document) => {
// Bail early if envision is not found.
if (!window.envision) {
console.warn('Envision not found. Exiting since most likely not Sitevision.');
return;
}

const LOCATION = window.location;

const getElement = (selector) => document.querySelector(selector);
const getSearchParams = () => new URLSearchParams(LOCATION.search);

const updateSearchParam = (param, value) => {
const searchParams = getSearchParams();

searchParams.set(param, value);
LOCATION.search = searchParams;
};

getInstance('sitevision-development-dialog', {
title: 'Sitevision development',
})
.init(() => {
const PROFILING_PARAM = 'profiling';
const JSDEBUG_PARAM = 'jsdebug';
const VERSION_PARAM = 'version';
const switchesContainer = createElement('<div></div>');

new Switch({
label: 'Profiling',
description: 'Shows a table with render times of all portlets on page.',
changeCallback (instance, e) {
updateSearchParam(PROFILING_PARAM, !instance.check());
},
checkCallback () {
const searchParams = getSearchParams();

if (searchParams.has(PROFILING_PARAM)) {
return searchParams.get(PROFILING_PARAM) === 'true';
}

// Profiling is active if a table exist at the top of body and contains the text "Profiling".
return getElement('body > div:not(.sv-layout) > table')?.textContent.trim().startsWith('Profiling');
}
}).appendTo(switchesContainer);

new Switch({
label: 'Javascript Debug',
description: 'Disables minification and bundling of stylesheets and client scripts.',
changeCallback (instance, e) {
updateSearchParam(JSDEBUG_PARAM, !instance.check());
},
checkCallback () {
const searchParams = getSearchParams();

if (searchParams.has(JSDEBUG_PARAM)) {
return searchParams.get(JSDEBUG_PARAM) === 'true';
}

// Javascript Debug is deemed to be active if bundle files webapp-assets.js (requires at least one webapp visible)
// and sv-template-asset.css (requires at least one css file on page/template).
// Will fail if no custom css or webapp exists on the rendered page.
return !(
getElement('body > script[src$="/webapp-assets.js"]')
|| getElement('head > link[href$="sv-template-asset.css"]')
);
}
}).appendTo(switchesContainer);

// Offline version is only usable by authenticated users.
if (document.body.classList.contains('sv-editing-mode') || getElement('body > iframe[src*="/edit-editormenu/"]')) {
new Switch({
label: 'Offline verison',
description: 'View the version used when editing in the Sitevision editor, but without the editor interface.',
changeCallback (instance, e) {
const searchParams = getSearchParams();

if (instance.check()) {
searchParams.delete(VERSION_PARAM);
} else {
searchParams.set(VERSION_PARAM, '0');
}

LOCATION.search = searchParams;
},
checkCallback () {
return getSearchParams().get(VERSION_PARAM) === '0';
}
}).appendTo(switchesContainer);
}

return switchesContainer;
})
.toggle();
})(window, window.document)

create-element.js

const DOCUMENT = window.document;
// const ENVISION = window.envision;

export default function createElement (markup) {
const template = DOCUMENT.createElement('template');
template.innerHTML = markup;

return template.content.firstElementChild;
}

Dialog.js

const CLASS_BASE = 'env-modal-dialog';
const SPINNER_CLASS = 'env-spinner-bounce';
const DOCUMENT = window.document;
const ENVISION = window.envision;

/**
* Creates the HTML markup for an envision dialog.
*
* @param {object} options An object literal with options.
* @param {string} options.dialogId A unique html ID for the dialog.
* @param {string} options.title
* @returns {string}
*/


export class Dialog {
constructor ({ dialogId, title, size }) {
this.id = dialogId;
this.isLoading = false;
this.title = title;
this.size = size;

this.tid = dialogId + '-t'; // title
this.thid = dialogId + '-th'; // title holder
this.cid = dialogId + '-c'; // content
this.fid = dialogId + '-f'; // footer
this.lid = dialogId + '-l'; // loader
}

/**
* Shortcut for Document.getElementById() for improved minification.
*
* @param {string} id An identifier for an HTMLElement.
* @returns {(HTMLElement|null)}
*/

el (id) {
return DOCUMENT.getElementById(id);
}

/**
* Initializes the dialog if it hasn't already been initialized.
*
* @param {(Function|String|HTMLElement)} initialContent
* @returns {Dialog} Returns self for chainability.
*/

init (initialContent) {
const dialogId = this.id;
const titleId = this.tid;

if (this.el(dialogId)) {
return this;
}

initialContent = typeof initialContent === 'function' ? initialContent.call(this) : initialContent;

const contentString = typeof initialContent === 'string' ? initialContent : '';
const sizeClass = this.size ? `${CLASS_BASE}__dialog--${this.size}` : '';

// Insert dialog into end of body and show dialog with envision.
DOCUMENT.body.insertAdjacentHTML('beforeend', (
`<div id="${dialogId}" class="${CLASS_BASE} ${CLASS_BASE}--inner-scroll" style="z-index:99999" role="dialog" aria-modal="true" aria-labelledby="${titleId}" aria-hidden="true" tabindex="-1">
<div class="
${CLASS_BASE}__dialog ${sizeClass}">
<section class="
${CLASS_BASE}__content">
<header class="
${CLASS_BASE}__header">
<h5 id="
${titleId}" class="env-text env-d--flex env-flex--align-items-center env-flex--justify-content-between ${CLASS_BASE}__header__title">
<span id="
${this.thid}">${this.title}</span>
<div id="
${this.lid}" class="${SPINNER_CLASS} ${SPINNER_CLASS}--hide ${SPINNER_CLASS}--fade-in" data-delay="medium">
${[1,2,3].map(i => `<div class="env-bounce${i}"></div>`).join('')}
</div>
</h5>
</header>
<div class="
${CLASS_BASE}__body">
<div id="
${this.cid}">${contentString}</div>
</div>
<footer id="
${this.fid}" class="${CLASS_BASE}__footer env-d--flex env-flex--justify-content-between">
<button type="button" data-modal-dialog-dismiss class="env-button env-m-left--a">Close</button>
</footer>
</section>
</div>
</div>
`

));

if (initialContent instanceof HTMLElement) {
this.el(this.cid).appendChild(initialContent);
}

return this;
}

/**
* @param {string} title
*/

updateTitle (title) {
this.title = title;
this.el(this.thid).innerHTML = title;
}

/**
* @param {string} html
*/

updateContent (html) {
this.el(this.cid).innerHTML = html;
}

/**
* @param {boolean} isLoading
*/

setLoading (isLoading) {
this.isLoading = !!isLoading;
this.el(this.lid).classList.toggle(`${SPINNER_CLASS}--hide`, !this.isLoading);
}

/**
* @param {string} errorMessage
*/

renderError (errorMessage) {
this.updateContent(
`<div class="env-alert env-alert--danger">${errorMessage}</div>`
);
}

/**
* @param {Function} cb Callback run when Envision.dialog promise has finished.
* @returns {Dialog}
*/

toggle (cb) {
ENVISION.dialog('#' + this.id, 'toggle').then(cb);
return this;
}
};

export default Dialog;

export const getInstance = (function () {
const instances = {};
/**
* @returns {Dialog}
*/

return function (dialogId, opts = {}) {
if (!instances[dialogId]) {
instances[dialogId] = new Dialog({ ...opts, dialogId });
}

return instances[dialogId];
};
})();

Events.js

const CLICK = 'click';
const CHANGE = 'change';
const INPUT = 'input';
const KEYDOWN = 'keydown';

const add = (el, event, cb) => el.addEventListener(event, cb);
const remove = (el, event, cb) => el.removeEventListener(event, cb);

export default {
onClick: (el, cb) => add(el, CLICK, cb),
offClick: (el, cb) => remove(el, CLICK, cb),
onChange: (el, cb) => add(el, CHANGE, cb),
onInput: (el, cb) => add(el, INPUT, cb),
onKeydown: (el, cb) => add(el, KEYDOWN, cb),
};

uid.js

export default function uid () {
return '-' + Math.random().toString(36).slice(2, 11);
}

Switch.js

import createElement from "./create-element";
import Events from "./Events";
import uid from "./uid";

export class Switch {
constructor ({ label, description, changeCallback, checkCallback }) {
this.label = label;
this.description = description;
this.changeCallback = changeCallback;
this.checkCallback = checkCallback;
this.el = null;
this.id = uid();

this.init();
}

check () {
return !!this.checkCallback.call(null, this);
}

init () {
const checked = this.check() ? 'checked' : '';
const description = this.description ? `<p class="env-text" style="margin: 0 0 var(--env-spacing-x-large) calc(2.625em + var(--env-spacing-x-small))">${this.description}</p>` : '';

this.el = createElement(
`<div class="env-form-field">
<div class="env-form-control">
<input class="env-switch" type="checkbox" id="
${this.id}" name="switches" ${checked} />
<label class="env-form-label" for="
${this.id}">${this.label}</label>
</div>
${description}
</div>
`

);

Events.onChange(this.el.querySelector('input'), (event) => this.changeCallback.call(null, this, event));
}

appendTo (target) {
target.appendChild(this.el);
}
}

export default Switch;