import { Injectable } from '@angular/core'; import { createSelector, MemoizedSelector, select, Store, } from '@ngrx/store'; import { Observable } from 'rxjs'; import { AppState, keySelector, } from '../../app.reducer'; import { buildPaginatedList, PaginatedList, } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { hasValue, isNotEmpty, } from '../empty.util'; import { KeyValuePair } from '../key-value-pair.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { AddAllCSSVariablesAction, AddCSSVariableAction, ClearCSSVariablesAction, } from './css-variable.actions'; import { CSSVariablesState } from './css-variable.reducer'; /** * This service deals with adding and retrieving CSS variables to and from the store */ @Injectable({ providedIn: 'root', }) export class CSSVariableService { isSameDomain = (styleSheet) => { // Internal style blocks won't have an href value if (!styleSheet.href) { return true; } return styleSheet.href.indexOf(window.location.origin) === 0; }; /** * Checks whether the specific stylesheet object has the property cssRules * @param styleSheet The stylesheet */ hasCssRules = (styleSheet) => { // Injected (cross-origin) styles might have no css rules value and throw some exception try { return styleSheet.cssRules; } catch (e) { return false; } }; /* Determine if the given rule is a CSSStyleRule See: https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants */ isStyleRule = (rule) => rule.type === 1; constructor( protected store: Store<AppState>) { } /** * Adds a CSS variable to the store * @param name The name/key of the CSS variable * @param value The value of the CSS variable */ addCSSVariable(name: string, value: string) { this.store.dispatch(new AddCSSVariableAction(name, value)); } /** * Adds multiples CSS variables to the store * @param variables The key-value pairs with the CSS variables to be added */ addCSSVariables(variables: KeyValuePair<string, string>[]) { this.store.dispatch(new AddAllCSSVariablesAction(variables)); } /** * Clears all CSS variables ƒrom the store */ clearCSSVariables() { this.store.dispatch(new ClearCSSVariablesAction()); } /** * Returns the value of a specific CSS key * @param name The name/key of the CSS value */ getVariable(name: string): Observable<string> { return this.store.pipe(select(themeVariableByNameSelector(name))); } /** * Returns the CSSVariablesState of the store containing all variables */ getAllVariables(): Observable<CSSVariablesState> { return this.store.pipe(select(themeVariablesSelector)); } /** * Method to find CSS variables by their partially supplying their key. Case sensitive. Returns a paginated list of KeyValuePairs with CSS variables that match the query. * @param query The query to look for in the keys * @param paginationOptions The pagination options for the requested page */ searchVariable(query: string, paginationOptions: PaginationComponentOptions): Observable<PaginatedList<KeyValuePair<string, string>>> { return this.store.pipe(select(themePaginatedVariablesByQuery(query, paginationOptions))); } /** * Get all custom properties on a page * @return array<KeyValuePair<string, string>> * ex; [{key: "--color-accent", value: "#b9f500"}, {key: "--color-text", value: "#252525"}, ...] */ getCSSVariablesFromStylesheets(document: Document): KeyValuePair<string, string>[] { if (isNotEmpty(document.styleSheets)) { // styleSheets is array-like, so we convert it to an array. // Filter out any stylesheets not on this domain // Filter out any stylesheets that have no cssRules property return [...document.styleSheets] .filter(this.isSameDomain) .filter(this.hasCssRules) .reduce( (finalArr, sheet) => finalArr.concat( // cssRules is array-like, so we convert it to an array [...sheet.cssRules].filter(this.isStyleRule).reduce((propValArr, rule: any) => { const props = [...rule.style] .map((propName) => { return { key: propName.trim(), value: rule.style.getPropertyValue(propName).trim(), } as KeyValuePair<string, string>; }, ) // Discard any props that don't start with "--". Custom props are required to. .filter(({ key }: KeyValuePair<string, string>) => key.indexOf('--') === 0); return [...propValArr, ...props]; }, []), ), [], ); } else { return []; } } } const themeVariablesSelector = (state: AppState) => state.cssVariables; const themeVariableByNameSelector = (name: string): MemoizedSelector<AppState, string> => { return keySelector<string>(name, themeVariablesSelector); }; // Split this up into two memoized selectors so the query search gets cached separately from the pagination, // since the entire list has to be retrieved every time anyway const themePaginatedVariablesByQuery = (query: string, pagination: PaginationComponentOptions): MemoizedSelector<AppState, PaginatedList<KeyValuePair<string, string>>> => { return createSelector(themeVariablesByQuery(query), (pairs) => { if (hasValue(pairs)) { const { currentPage, pageSize } = pagination; const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const pairsPage = pairs.slice(startIndex, endIndex); const totalPages = Math.ceil(pairs.length / pageSize); const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalElements: pairs.length, totalPages }); return buildPaginatedList(pageInfo, pairsPage); } else { return undefined; } }); }; const themeVariablesByQuery = (query: string): MemoizedSelector<AppState, KeyValuePair<string, string>[]> => { return createSelector(themeVariablesSelector, (state) => { if (hasValue(state)) { return Object.keys(state) .filter((key: string) => key.includes(query)) .map((key: string) => { return { key, value: state[key] }; }); } else { return undefined; } }); };