Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not supported property values #763

Merged
merged 3 commits into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion editor/css/editable-css.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
}

@media screen and (min-width: 590px) {
.example-choice.selected:before {
.example-choice.selected:before, .example-choice.invalid:before {
opacity: 1;
right: -1rem;
}
Expand Down
6 changes: 6 additions & 0 deletions editor/js/editable-css.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as clippy from "./editor-libs/clippy.js";
import * as mceEvents from "./editor-libs/events.js";
import * as mceUtils from "./editor-libs/mce-utils.js";
import * as cssEditorUtils from "./editor-libs/css-editor-utils.js";

import "../css/editor-libs/ui-fonts.css";
import "../css/editor-libs/common.css";
Expand Down Expand Up @@ -47,6 +48,8 @@ import "../css/editable-css.css";
mceEvents.register();
handleResetEvents();
handleChoiceHover();
// Adding or removing class "invalid"
cssEditorUtils.applyInitialSupportWarningState(exampleChoices);

clippy.addClippy();
}
Expand All @@ -70,6 +73,9 @@ import "../css/editable-css.css";
exampleChoices[i].querySelector("code").innerHTML = highlighted;
}

// Adding or removing class "invalid"
cssEditorUtils.applyInitialSupportWarningState(exampleChoices);

// if there is an initial choice set, set it as selected
if (initialChoice) {
mceEvents.onChoose(exampleChoices[initialChoice]);
Expand Down
126 changes: 114 additions & 12 deletions editor/js/editor-libs/css-editor-utils.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,129 @@
export let editTimer = undefined;

export function applyCode(code, choice, targetElement) {
export function applyCode(code, choice, targetElement, immediateInvalidChange) {
// http://regexr.com/3fvik
var cssCommentsMatch = /(\/\*)[\s\S]+(\*\/)/g;
var element = targetElement || document.getElementById("example-element");

// strip out any CSS comments before applying the code
code.replace(cssCommentsMatch, "");
code = code.replace(cssCommentsMatch, "");
// Checking if every CSS declaration in passed code, is supported by the browser
let codeSupported = isCodeSupported(element, code);

element.style.cssText = code;

// clear any existing timer
clearTimeout(editTimer);
/* 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(function () {
if (!element.style.cssText) {
choice.parentNode.classList.add("invalid");

/**
* Adding or removing class "invalid" from choice parent, which will typically be <div class="example-choice">
*/
let setInvalidClass = function() {
if (codeSupported) {
choice.parentNode.classList.remove('invalid');
} else {
choice.parentNode.classList.remove("invalid");
choice.parentNode.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);
}
}

/**
* 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) {
var vendorPrefixMatch = /^-(?:webkit|moz|ms|o)-/;
var style = element.style;
// Expecting declarations to be separated by ";"
// Declarations with just white space are ignored
var 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) {
var prefixMatch = vendorPrefixMatch.exec(declaration);
var prefix = prefixMatch === null ? "" : prefixMatch[0];
var declarationNoPrefix = prefix === null ? declaration : declaration.slice(prefix.length);
// Expecting property name to be over, when any whitespace or ":" is found
var propertyNameSeparator = /[\s:]/;
return declarationNoPrefix.split(propertyNameSeparator)[0];
}
// Clearing previous state
style.cssText = "";

// List of found and applied properties with vendor prefix
let appliedPropertiesWithPrefix = new Set();
// List of not applied properties - because of lack of support for its name or value
let notAppliedProperties = new Set();

for (let declaration of declarationsArray) {
let 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
let correctlyApplied = style.cssText !== previousCSSText;

let vendorPrefixFound = hasVendorPrefix(declaration);
let 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 (let 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(let choice of choices) {
let codeBlock = choice.querySelector("code");
applyCode(codeBlock.textContent, codeBlock.parentNode, undefined, true);
}
}, 500);
}

/**
Expand All @@ -38,7 +140,7 @@ export function choose(choice) {
codeBlock.setAttribute("contentEditable", true);
codeBlock.setAttribute("spellcheck", false);

applyCode(codeBlock.textContent, choice);
applyCode(codeBlock.textContent, codeBlock.parentNode);
}

/**
Expand All @@ -65,7 +167,7 @@ export function resetDefault() {
}

/**
* Resets the UI state by deselcting all example choice
* Resets the UI state by deselecting all example choice
*/
export function resetUIState() {
var exampleChoiceList = document.getElementById("example-choice-list");
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.