Skip to content

Commit

Permalink
Refactor DOM manipuation and style (#186)
Browse files Browse the repository at this point in the history
* start removing clsx

* working through style refactor to use griffel

* refactor Doc components

* update rich text infra

* run prettier

* unsaved changes

* remove Doc.css

* remove Editor.css

* remove Map.css

* deleted ui/shared css

* remove common.css

* remove Header.css

* clean up

* clean up 2
  • Loading branch information
Pistonight authored Feb 14, 2024
1 parent 5ccb484 commit 6ef1fd3
Show file tree
Hide file tree
Showing 90 changed files with 2,096 additions and 1,589 deletions.
1 change: 0 additions & 1 deletion web-client/package-lock.json

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

1 change: 0 additions & 1 deletion web-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"@fluentui/react-components": "^9.21.0",
"@fluentui/react-icons": "^2.0.214",
"@reduxjs/toolkit": "^1.9.5",
"clsx": "^1.2.1",
"denque": "^2.1.0",
"immer": "^10.0.2",
"is-equal": "^1.6.4",
Expand Down
39 changes: 0 additions & 39 deletions web-client/src/common.css

This file was deleted.

4 changes: 4 additions & 0 deletions web-client/src/core/editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! core/editor
//! Editor related state

import { DOMId } from "low/utils";

export * from "./state";
export * as editorViewReducers from "./viewReducers";
export * as editorSettingsReducers from "./settingsReducers";

export const EditorContainerDOM = new DOMId("editor-container");
31 changes: 1 addition & 30 deletions web-client/src/core/kernel/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export class Kernel {
/// The kernel owns the store. The store is shared
/// between app boots (i.e. when switching routes)
private store: AppStore;
/// The link tag that loads the theme css
private themeLinkTag: HTMLLinkElement | null = null;
/// The function to initialize react
private initReact: InitUiFunction;
/// The function to unmount react
Expand Down Expand Up @@ -71,23 +69,16 @@ export class Kernel {
private initStore(): AppStore {
this.log.info("initializing store...");
const store = initStore();
// switch theme on boot base on store settings
this.switchTheme(settingsSelector(store.getState()).theme);

const watchSettings = reduxWatch(() =>
settingsSelector(store.getState()),
);

store.subscribe(
watchSettings((newVal: SettingsState, oldVal: SettingsState) => {
watchSettings((newVal: SettingsState, _oldVal: SettingsState) => {
// save settings to local storage
this.log.info("saving settings...");
saveSettings(newVal);

// switch theme
if (newVal.theme !== oldVal.theme) {
this.switchTheme(newVal.theme);
}
}),
);

Expand Down Expand Up @@ -163,26 +154,6 @@ export class Kernel {
};
}

/// Switch theme to the given theme
///
/// This replaces the theme css link tag.
/// The theme files are built by the web-themes project.
public switchTheme(theme: string) {
if (!this.themeLinkTag) {
const e = document.createElement("link");
e.rel = "stylesheet";
e.type = "text/css";
this.themeLinkTag = e;
const head = document.querySelector("head");
if (!head) {
this.log.error("Could not find head tag to attach theme to");
return;
}
head.appendChild(e);
}
this.themeLinkTag.href = `/themes/${theme}.min.css`;
}

public getAlertMgr(): AlertMgr {
return this.alertMgr;
}
Expand Down
12 changes: 4 additions & 8 deletions web-client/src/core/kernel/editor/FileMgr.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as monaco from "monaco-editor";

import { AppDispatcher, viewActions } from "core/store";
import { EditorContainerDOM } from "core/editor";
import {
FileAccess,
FileSys,
Expand All @@ -17,12 +18,7 @@ import {
sleep,
} from "low/utils";

import {
EditorContainerId,
EditorLog,
detectLanguageByFileName,
toFsPath,
} from "./utils";
import { EditorLog, detectLanguageByFileName, toFsPath } from "./utils";

type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;

Expand Down Expand Up @@ -410,11 +406,11 @@ export class FileMgr implements FileAccess {
}

private async attachEditor() {
let div = document.getElementById(EditorContainerId);
let div = EditorContainerDOM.get();
while (!div) {
EditorLog.warn("editor container not found. Will try again.");
await sleep(100);
div = document.getElementById(EditorContainerId);
div = EditorContainerDOM.get();
}
let alreadyAttached = false;
div.childNodes.forEach((node) => {
Expand Down
9 changes: 7 additions & 2 deletions web-client/src/core/kernel/editor/WebEditorKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
SettingsState,
} from "core/store";
import { FileAccess, FileSys, FsResult } from "low/fs";
import { isInDarkMode, IdleMgr } from "low/utils";
import { isInDarkMode, IdleMgr, DOMId } from "low/utils";

import { EditorKernel } from "./EditorKernel";
import { EditorLog, KernelAccess, toFsPath } from "./utils";
Expand Down Expand Up @@ -44,6 +44,11 @@ export const initWebEditor = (
return new WebEditorKernel(kernelAccess, fileSys, store);
};

const MonacoEditorDOM = new DOMId("monaco-editor");
MonacoEditorDOM.style({
height: "100%",
});

class WebEditorKernel implements EditorKernel {
private deleted = false;
private store: AppStore;
Expand All @@ -70,7 +75,7 @@ class WebEditorKernel implements EditorKernel {
);

const monacoDom = document.createElement("div");
monacoDom.id = "monaco-editor";
monacoDom.id = MonacoEditorDOM.id;
const monacoEditor = monaco.editor.create(monacoDom, {
theme: isInDarkMode() ? "vs-dark" : "vs",
tabSize: 2,
Expand Down
2 changes: 0 additions & 2 deletions web-client/src/core/kernel/editor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { Logger } from "low/utils";

export const EditorLog = new Logger("edt");

export const EditorContainerId = "editor-container";

export const toFsPath = (path: string[]): FsPath => {
let fsPath = fsRootPath;
for (let i = 0; i < path.length; i++) {
Expand Down
133 changes: 101 additions & 32 deletions web-client/src/low/utils/html.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Basic utilities for working in browser environments.

import clsx from "clsx";
import { mergeClasses } from "@fluentui/react-components";

export const isInDarkMode = () =>
!!(
Expand All @@ -14,13 +14,20 @@ export const isInDarkMode = () =>
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

/// Append "px" to a number
export const px = (n: number) => `${n}px`;

/// Accessor for DOM element of type E identified by an id
export class DOMId<E extends HTMLElement> {
readonly id: string;
constructor(id: string) {
export class DOMId<N extends string, E extends HTMLElement = HTMLElement> {
readonly id: N;
constructor(id: N) {
this.id = id;
}

public as<E2 extends E>(): DOMId<N, E2> {
return this as unknown as DOMId<N, E2>;
}

public get(): E | undefined {
const element = document.getElementById(this.id) || undefined;
return element as E | undefined;
Expand All @@ -34,33 +41,39 @@ export class DOMId<E extends HTMLElement> {
return `${selector}{${group}}`;
})
.join("");
new DOMStyleInject(`${this.id}-styles-byid`).setStyle(css);
injectDOMStyle(`${this.id}-styles-byid`, css);
}
}

/// Accessor for runtime injected style tag
export class DOMStyleInject {
readonly id: string;
constructor(id: string) {
this.id = id;
}

public setStyle(style: string) {
let styleTag = document.querySelector(`style[data-inject="${this.id}"`);
if (!styleTag) {
styleTag = document.createElement("style");
styleTag.setAttribute("data-inject", this.id);
const head = document.querySelector("head");
if (!head) {
return;
} else {
head.appendChild(styleTag);
}
/// Inject a css string into a style tag identified by the id
export function injectDOMStyle(id: string, style: string) {
const styleTags = document.querySelectorAll(`style[data-inject="${id}"`);
if (styleTags.length !== 1) {
const styleTag = document.createElement("style");
styleTag.setAttribute("data-inject", id);
styleTag.innerText = style;
document.head.appendChild(styleTag);
setTimeout(() => {
styleTags.forEach((tag) => tag.remove());
}, 0);
} else {
const e = styleTags[0] as HTMLStyleElement;
if (e.innerText !== style) {
e.innerText = style;
}
(styleTag as HTMLStyleElement).innerText = style;
}
}

/// Wrap CSS inside a query
export function cssQuery(selector: string, css: string): string {
return `${selector}{${css}}`;
}

/// Get the media query for the given theme
export function prefersColorScheme(theme: "light" | "dark"): string {
return `@media(prefers-color-scheme: ${theme})`;
}

function addCssObjectToMap(
selector: string,
obj: Record<string, unknown>,
Expand Down Expand Up @@ -91,21 +104,19 @@ export class DOMClass<N extends string, E extends HTMLElement = HTMLElement> {
this.className = className;
}

/// Get the combined class name from the given Griffel style map
public styledClassName(style: Record<N, string>) {
return clsx(this.className, style[this.className]);
}

/// Inject a raw css map into the head that targets this class
public injectStyle(style: Record<string, string>) {
public style(style: Record<string, unknown>, query?: string) {
const map = {};
addCssObjectToMap(`.${this.className}`, style, map);
const css = Object.entries(map)
let css = Object.entries(map)
.map(([selector, group]) => {
return `${selector}{${group}}`;
})
.join("");
new DOMStyleInject(`${this.className}-styles`).setStyle(css);
if (query) {
css = cssQuery(query, css);
}
injectDOMStyle(`c-${this.className}-styles`, css);
}

public query(selector?: string): E | undefined {
Expand All @@ -118,4 +129,62 @@ export class DOMClass<N extends string, E extends HTMLElement = HTMLElement> {
public queryAll(selector?: string): NodeListOf<E> {
return document.querySelectorAll(`.${this.className}${selector || ""}`);
}

public queryAllIn(element: HTMLElement, selector?: string): NodeListOf<E> {
return element.querySelectorAll(`.${this.className}${selector || ""}`);
}

public addTo(element: HTMLElement) {
element.classList.add(this.className);
}

public removeFrom(element: HTMLElement) {
element.classList.remove(this.className);
}
}

/// Merge a set of DOMClasses and raw class names into a single class name.
export function smartMergeClasses<N extends string>(
style: Record<N, string>,
...classes: (DOMClass<N> | string | false | undefined | null | 0)[]
): string {
const inputs = [];
for (let i = 0; i < classes.length; i++) {
const c = classes[i];
if (!c) {
continue;
}
if (typeof c === "string") {
inputs.push(c);
} else {
inputs.push(style[c.className]);
inputs.push(c.className);
}
}
return mergeClasses(...inputs);
}

export function concatClassName<TBase extends string, TName extends string>(
base: TBase,
name: TName,
): `${TBase}-${TName}` {
return `${base}-${name}`;
}

/// Wrapper for a CSS variable
export class CSSVariable<N extends `--${string}`> {
readonly name: N;
constructor(name: N) {
this.name = name;
}

/// Get the var(name) expression
public varExpr() {
return `var(${this.name})` as const;
}

/// Get the var(name, fallback) expression
public fallback<S>(value: S) {
return `var(${this.name}, ${value})` as const;
}
}
Loading

0 comments on commit 6ef1fd3

Please sign in to comment.