@mdn/bob
Version:
Builder of Bits aka The MDN Web Docs interactive examples, example builder
167 lines • 8.01 kB
JavaScript
export let editTimer;
export function applyCode(code, choice, targetElement, immediateInvalidChange = false) {
// http://regexr.com/3fvik
const cssCommentsMatch = /(\/\*)[\s\S]+(\*\/)/g;
const element = targetElement ||
document.getElementById("example-element");
// strip out any CSS comments before applying the code
code = code.replace(cssCommentsMatch, "");
// Checking if every CSS declaration in passed code, is supported by the browser
const codeSupported = isCodeSupported(element, code);
element.style.cssText = code;
// clear any existing timer
clearTimeout(editTimer);
/**
* Adding or removing class "invalid" from choice parent, which will typically be <div class="example-choice">
*/
function setInvalidClass() {
if (codeSupported) {
choice.classList.remove("invalid");
}
else {
choice.classList.add("invalid");
}
}
if (immediateInvalidChange) {
// Setting class immediately
setInvalidClass();
}
else {
/* Start a new timer. This will ensure that the state is
not marked as invalid, until the user has stopped typing
for 500ms */
editTimer = setTimeout(setInvalidClass, 500);
}
}
/**
* Creates a temporary element and tests whether any of the provided CSS sets of declarations are fully supported by the user's browser
* @param {Array} declarationSets - Array in which every element is one or multiple declarations separated by semicolons
*/
export function isAnyDeclarationSetSupported(declarationSets) {
const tmpElem = document.createElement("div");
return declarationSets.some(isCodeSupported.bind(null, tmpElem));
}
/**
* Checks if every passed declaration is supported by the browser.
* In case browser recognizes property with vendor prefix(like -webkit-), lacking support for unprefixed property is ignored.
* Properties with vendor prefix not recognized by the browser are always ignored.
* @param element - any element on which cssText can be tested
* @param declarations - list of css declarations with no curly brackets. They need to be separated by ";" and declaration key-value needs to be separated by ":". Function expects no comments.
* @returns {boolean} - true if every declaration is supported by the browser. Properties with vendor prefix are excluded.
*/
export function isCodeSupported(element, declarations) {
const vendorPrefixMatch = /^-(?:webkit|moz|ms|o)-/;
const style = element.style;
// Expecting declarations to be separated by ";"
// Declarations with just white space are ignored
const declarationsArray = declarations
.split(";")
.map((d) => d.trim())
.filter((d) => d.length > 0);
/**
* @returns {boolean} - true if declaration starts with -webkit-, -moz-, -ms- or -o-
*/
function hasVendorPrefix(declaration) {
return vendorPrefixMatch.test(declaration);
}
/**
* Looks for property name by cutting off optional vendor prefix at the beginning
* and then cutting off rest of the declaration, starting from any whitespace or ":" in property name.
* @param declaration - single css declaration, with not white space at the beginning
* @returns {string} - property name without vendor prefix.
*/
function getPropertyNameNoPrefix(declaration) {
const prefixMatch = vendorPrefixMatch.exec(declaration);
const prefix = prefixMatch === null ? "" : prefixMatch[0];
const declarationNoPrefix = prefix === null ? declaration : declaration.slice(prefix.length);
// Expecting property name to be over, when any whitespace or ":" is found
const propertyNameSeparator = /[\s:]/;
return declarationNoPrefix.split(propertyNameSeparator)[0];
}
// Clearing previous state
style.cssText = "";
// List of found and applied properties with vendor prefix
const appliedPropertiesWithPrefix = new Set();
// List of not applied properties - because of lack of support for its name or value
const notAppliedProperties = new Set();
for (const declaration of declarationsArray) {
const previousCSSText = style.cssText;
// Declarations are added one by one, because browsers sometimes combine multiple declarations into one
// For example Chrome changes "column-count: auto;column-width: 8rem;" into "columns: 8rem auto;"
style.cssText += declaration + ";"; // ";" was previous removed while using split method
// In case property name or value is not supported, browsers skip single declaration, while leaving rest of them intact
const correctlyApplied = style.cssText !== previousCSSText;
const vendorPrefixFound = hasVendorPrefix(declaration);
const propertyName = getPropertyNameNoPrefix(declaration);
if (correctlyApplied && vendorPrefixFound) {
// We are saving applied properties with prefix, so equivalent property with no prefix doesn't need to be supported
appliedPropertiesWithPrefix.add(propertyName);
}
else if (!correctlyApplied && !vendorPrefixFound) {
notAppliedProperties.add(propertyName);
}
}
if (notAppliedProperties.size !== 0) {
// If property with vendor prefix is supported, we can ignore the fact that browser doesn't support property with no prefix
for (const substitute of appliedPropertiesWithPrefix) {
notAppliedProperties.delete(substitute);
}
// If any other declaration is not supported, whole block should be marked as invalid
if (notAppliedProperties.size !== 0)
return false;
}
return true;
}
/**
* Checking support for choices inner code and based on that information adding or removing class "invalid" from them.
* This function will change styles of 'example-element', so it is important to apply them again.
* @param choices - elements containing element code, containing css declarations to apply
*/
export function applyInitialSupportWarningState(choices) {
for (const choice of choices) {
const codeBlock = choice.querySelector(".cm-content");
applyCode(codeBlock.textContent || "", choice, undefined, true);
}
}
/**
* Sets the choice to selected, changes the nested code element to be editable,
* turns of spellchecking. Lastly, it applies the code to the example element
* by calling applyCode.
* @param {HTMLElement} choice - The selected `example-choice` element
*/
export function choose(choice) {
choice.classList.add("selected");
const codeBlock = choice.querySelector(".cm-content");
applyCode(codeBlock.textContent || "", choice);
}
/**
* Resets the default example to visible but, only if it is currently hidden
*/
export function resetDefault() {
const defaultExample = document.getElementById("default-example");
const output = document.getElementById("output");
// only reset to default if the default example is hidden
if (defaultExample.classList.contains("hidden")) {
const sections = output.querySelectorAll("section");
// loop over all sections and set to hidden
for (let i = 0, l = sections.length; i < l; i++) {
sections[i].classList.add("hidden");
sections[i].setAttribute("aria-hidden", "true");
}
// show the default example
defaultExample.classList.remove("hidden");
defaultExample.setAttribute("aria-hidden", "false");
}
resetUIState();
}
/**
* Resets the UI state by deselecting all example choice
*/
export function resetUIState() {
const exampleChoiceList = document.getElementById("example-choice-list");
const exampleChoices = exampleChoiceList.querySelectorAll(".example-choice");
for (let i = 0, l = exampleChoices.length; i < l; i++) {
exampleChoices[i].classList.remove("selected");
}
}
//# sourceMappingURL=css-editor-utils.js.map