Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/survey-core/entries/chunks/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export {
export { IsMobile, IsTouch, _setIsTouch, _setIsTablet } from "../../src/utils/devices";
export * from "../../src/utils/browser";
export * from "../../src/utils/color";
export * from "../../src/utils/css-variables";
export * from "../../src/utils/confirm-dialog";
export * from "../../src/utils/dom-utils";
export * from "../../src/utils/file-utils";
Expand Down
14 changes: 14 additions & 0 deletions packages/survey-core/src/global_variables_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ export class DomDocumentHelper {
if (!DomDocumentHelper.isAvailable()) return;
return document.createElement(tagName);
}
public static setStyles(element: HTMLElement, styles: Record<string, any>): void {
if (!element || !styles) return;

Object.keys(styles).forEach(property => {
element.style.setProperty(property, styles[property]);
});
}
public static removeStyles(element: HTMLElement, properties: string[]): void {
if (!element || !properties) return;

properties.forEach(property => {
element.style.removeProperty(property);
});
}
public static getComputedStyle(elt: Element): CSSStyleDeclaration {
if (!DomDocumentHelper.isAvailable()) return new CSSStyleDeclaration();
return document.defaultView.getComputedStyle(elt);
Expand Down
4 changes: 2 additions & 2 deletions packages/survey-core/src/question_rating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { DropdownListModel } from "./dropdownListModel";
import { SurveyModel } from "./survey";
import { ISurveyImpl } from "./base-interfaces";
import { IsTouch } from "./utils/devices";
import { getColorFromProperty } from "./utils/utils";
import { getRGBaColor } from "./utils/color";
import { getComputedCssVariableValue } from "./utils/css-variables";
import { ITheme } from "./themes";
import { DomDocumentHelper } from "./global_variables_utils";
import { HashTable } from "./helpers";
Expand All @@ -23,7 +23,7 @@ const RGBA_BLACK = "rgba(0, 0, 0, 1)";

function getRGBColor(themeVariables: any, colorName: string, varName: string, rootElement: HTMLElement): number[] | null {
let str: string = !!themeVariables && themeVariables[colorName] as any;
const fallback = getColorFromProperty(varName, rootElement);
const fallback = getComputedCssVariableValue(varName, rootElement);
if (!str) str = fallback;
if (!str) return null;

Expand Down
9 changes: 3 additions & 6 deletions packages/survey-core/src/question_signaturepad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { SurveyModel } from "./survey";
import { ConsoleWarnings } from "./console-warnings";
import { ITheme } from "./themes";
import { dataUrl2File, FileLoader, QuestionFileModelBase } from "./question_file";
import { getColorFromProperty } from "./utils/utils";
import { isBase64URL } from "./utils/dom-utils";
import { DomDocumentHelper, DomWindowHelper } from "./global_variables_utils";
import { Action } from "./actions/action";
import { ComputedUpdater } from "./base";
import { ActionContainer } from "./actions/container";
import { getComputedCssVariableValue } from "./utils/css-variables";

var defaultWidth = 300;
var defaultHeight = 200;
Expand All @@ -34,12 +34,9 @@ export class QuestionSignaturePadModel extends QuestionFileModelBase {
}
}
}
private getPenColorFromTheme(element: HTMLElement): string {
const cssVariable = "--sjs2-color-bg-brand-primary";
return getColorFromProperty(cssVariable, element);
}

private updateColors(signaturePad: SignaturePad, element?: HTMLElement) {
const penColorFromTheme = this.getPenColorFromTheme(element);
const penColorFromTheme = getComputedCssVariableValue("--sjs2-color-bg-brand-primary", element);
const penColorProperty = this.getPropertyByName("penColor");
signaturePad.penColor = this.penColor || penColorFromTheme || penColorProperty.defaultValue || "#1ab394";

Expand Down
103 changes: 103 additions & 0 deletions packages/survey-core/src/utils/css-variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { DomDocumentHelper } from "../global_variables_utils";
import { getRGBaColor } from "./color";
import { createBoxShadow, parseBoxShadow } from "./shadow-effects";

const THEME_ROOT_CLASS = "sd-theme-root";

function getCssVariableProxyProperty(varName: string): string | null {
if (varName.indexOf("color") !== -1 || varName.indexOf("palette") !== -1) return "color";
if (varName.indexOf("border-effect") !== -1) return "box-shadow";
if (varName.indexOf("font-family") !== -1) return null;
if (varName.indexOf("font-weight") !== -1) return null;
if (varName.indexOf("text-case") !== -1) return null;
if (varName.indexOf("opacity") !== -1) return null;
if (varName.indexOf("scale") !== -1) return null;
return "width";
}

function normalizeCssVariableValue(value: string, proxyProperty: string | null): string {
if (value == null) return value;
if (proxyProperty === "color" && value !== "transparent") {
return getRGBaColor(value) || value;
}
if (proxyProperty === "box-shadow") {
return createBoxShadow(parseBoxShadow(value)) || value;
}
return value;
}

function createCssVariableProbeElement(host: HTMLElement): HTMLElement | null {
const div = DomDocumentHelper.createElement("div") as HTMLElement;
if (!div) return null;
div.style.position = "absolute";
div.style.visibility = "hidden";
div.style.top = "0";
div.style.left = "0";
host.appendChild(div);
return div;
}

export function getComputedCssVariableValue(varName: string, element?: HTMLElement): string {
const el = element || DomDocumentHelper.getBody() || DomDocumentHelper.getDocumentElement();
if (!el) return "";

const computedStyle = DomDocumentHelper.getComputedStyle(el);
if (!computedStyle || !computedStyle.getPropertyValue) return "";

const value = computedStyle.getPropertyValue(varName);
const proxyProperty = getCssVariableProxyProperty(varName);
return normalizeCssVariableValue(value, proxyProperty);
}

function resolveComputedCssVariableValue(
probe: HTMLElement,
computed: CSSStyleDeclaration,
varName: string
): string {
const rawValue = computed.getPropertyValue(varName);
if (typeof rawValue !== "string" || rawValue.trim() === "") return "";
const proxyProperty = getCssVariableProxyProperty(varName);
if (proxyProperty === null) return rawValue.trim();

probe.style.setProperty(proxyProperty, `var(${varName})`);
const raw = computed.getPropertyValue(proxyProperty);
probe.style.removeProperty(proxyProperty);
const proxyValue = normalizeCssVariableValue(raw, proxyProperty);

return typeof proxyValue === "string" ? proxyValue.trim() : "";
}

export function getComputedCssVariableValues(
sourceCssVariables: { [key: string]: string } = {},
additionalCssVariables: string[] = [],
rootElement?: HTMLElement
): { [key: string]: string } {
const fallbackResult = { ...sourceCssVariables };
if (!DomDocumentHelper.isAvailable()) return fallbackResult;

const host = rootElement || DomDocumentHelper.getBody() || DomDocumentHelper.getDocumentElement();
const probe = host ? createCssVariableProbeElement(host) : null;
if (!probe) return fallbackResult;

if (Object.keys(sourceCssVariables).length !== 0) {
DomDocumentHelper.setStyles(probe, sourceCssVariables);
probe.classList.add(THEME_ROOT_CLASS);
}

const result: { [key: string]: string } = {};
try {
const computed = DomDocumentHelper.getComputedStyle(probe);
const varNames = [...Object.keys(sourceCssVariables), ...additionalCssVariables];
for (const varName of varNames) {
const value = resolveComputedCssVariableValue(probe, computed, varName);
if (value !== "") {
result[varName] = value;
} else if (sourceCssVariables[varName] !== undefined) {
result[varName] = sourceCssVariables[varName];
}
}
} finally {
host.removeChild(probe);
}
return result;
}
61 changes: 33 additions & 28 deletions packages/survey-core/tests/question_ratingtests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,35 +129,40 @@ test("check rating resize observer behavior", () => {
},
],
};
const survey = new SurveyModel(json);
survey.css = defaultCss;
const q1 = <QuestionRatingModel>survey.getQuestionByName("q1");
q1.afterRender(rootElement);
expect(q1["resizeObserver"]).toBeTruthy();
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 300;
currentScrollWidth = 300;
(<any>q1["resizeObserver"]).call();
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 200;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("dropdown");
currentOffsetWidth = 400;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 200;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("dropdown");
q1["destroyResizeObserver"]();
expect(q1.renderAs, "https://github.com/surveyjs/survey-creator/issues/2966: after destroying resize observer renderAs should return to default state").toBe("default");
window.getComputedStyle = getComputedStyle;
window.ResizeObserver = ResizeObserver;

contentElement.remove();
rootElement.remove();
try {
const survey = new SurveyModel(json);
survey.css = defaultCss;
const q1 = <QuestionRatingModel>survey.getQuestionByName("q1");
q1.afterRender(rootElement);
expect(q1["resizeObserver"]).toBeTruthy();
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 300;
currentScrollWidth = 300;
(<any>q1["resizeObserver"]).call();
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 200;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("dropdown");
currentOffsetWidth = 400;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("default");
currentOffsetWidth = 200;
(<any>q1["resizeObserver"]).call();
(<any>q1["resizeObserver"]).call(); //double process to reset isProcessed flag
expect(q1.renderAs).toBe("dropdown");
q1["destroyResizeObserver"]();
expect(q1.renderAs, "https://github.com/surveyjs/survey-creator/issues/2966: after destroying resize observer renderAs should return to default state").toBe("default");

} finally {
window.getComputedStyle = getComputedStyle;
window.ResizeObserver = ResizeObserver;

contentElement.remove();
rootElement.remove();
}
});

test("check rating in case of state 'collapsed'", () => {
Expand Down
25 changes: 15 additions & 10 deletions packages/survey-core/tests/question_signaturepadtests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ describe("question signaturepad", () => {
});

test("check penColor & background color from theme", () => {
const defaultThemePenColor = "#1ab394";
const defaultBackgroundColor = "transparent";

const json = {
elements: [
{
Expand All @@ -242,27 +245,29 @@ describe("question signaturepad", () => {

expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe("#1ab394");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe(defaultThemePenColor);
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor default").toBe("#ffffff");

containerEl.style.setProperty("--sjs2-color-bg-brand-primary", "rgba(103, 58, 176, 1)");
survey.applyTheme({ "cssVariables": { "--sjs2-color-bg-brand-primary": "rgba(103, 58, 176, 1)" } });
expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor from theme").toBe("rgba(103, 58, 176, 1)");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor from theme").toBe("transparent");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor from theme").toBe(defaultBackgroundColor);

containerEl.style.setProperty("--sjs2-color-bg-brand-primary", "");
survey.applyTheme({ "cssVariables": {} });
expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe("#1ab394");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe(defaultThemePenColor);
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor default").toBe("#ffffff");

containerEl.remove();
});

test("check penColor & background color if background image", () => {
const defaultThemePenColor = "#1ab394";
const defaultBackgroundColor = "transparent";
const json = {
elements: [
{
Expand All @@ -283,28 +288,28 @@ describe("question signaturepad", () => {

expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor #1ab394").toBe("#1ab394");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe("transparent");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe(defaultThemePenColor);
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe(defaultBackgroundColor);

containerEl.style.setProperty("--sjs2-color-bg-brand-primary", "rgba(103, 58, 176, 1)");
survey.applyTheme({ "cssVariables": { "--sjs2-color-bg-brand-primary": "rgba(103, 58, 176, 1)" } });
expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor from theme").toBe("rgba(103, 58, 176, 1)");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor from theme").toBe("transparent");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor from theme").toBe(defaultBackgroundColor);

containerEl.style.setProperty("--sjs2-color-bg-brand-primary", "");
survey.applyTheme({ "cssVariables": {} });
expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor undefined").toBeUndefined();
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor #1ab394").toBe("#1ab394");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe("transparent");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe(defaultThemePenColor);
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe(defaultBackgroundColor);

signaturepadQuestion.backgroundColor = "#dde6db";
expect(signaturepadQuestion.penColor, "penColor undefined").toBeUndefined();
expect(signaturepadQuestion.backgroundColor, "backgroundColor #dde6db").toBe("#dde6db");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor #1ab394").toBe("#1ab394");
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe("transparent");
expect(signaturepadQuestion.signaturePad.penColor, "signaturePad.penColor default").toBe(defaultThemePenColor);
expect(signaturepadQuestion.signaturePad.backgroundColor, "signaturePad.backgroundColor transparent").toBe(defaultBackgroundColor);

containerEl.remove();
});
Expand Down