Skip to content

Commit a9634f2

Browse files
committed
feat: migrate colorview and other cm related to ts
1 parent 3f325ac commit a9634f2

File tree

3 files changed

+157
-104
lines changed

3 files changed

+157
-104
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
syntaxHighlighting,
99
} from "@codemirror/language";
1010
import { highlightSelectionMatches } from "@codemirror/search";
11+
import type { Extension } from "@codemirror/state";
1112
import { EditorState } from "@codemirror/state";
1213
import {
1314
crosshairCursor,
@@ -22,9 +23,8 @@ import {
2223

2324
/**
2425
* Base extensions roughly matching the useful parts of CodeMirror's basicSetup
25-
* @returns {import("@codemirror/state").Extension[]}
2626
*/
27-
export default function createBaseExtensions() {
27+
export default function createBaseExtensions(): Extension[] {
2828
return [
2929
highlightActiveLineGutter(),
3030
highlightSpecialChars(),
Lines changed: 92 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Range, Text } from "@codemirror/state";
2+
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
13
import {
24
Decoration,
35
EditorView,
@@ -8,8 +10,20 @@ import pickColor from "dialogs/color";
810
import color from "utils/color";
911
import { colorRegex, HEX } from "utils/color/regex";
1012

13+
interface ColorWidgetState {
14+
from: number;
15+
to: number;
16+
colorType: string;
17+
alpha?: string;
18+
}
19+
20+
interface ColorWidgetParams extends ColorWidgetState {
21+
color: string;
22+
colorRaw: string;
23+
}
24+
1125
// WeakMap to carry state from widget DOM back into handler
12-
const colorState = new WeakMap();
26+
const colorState = new WeakMap<HTMLElement, ColorWidgetState>();
1327

1428
const HEX_RE = new RegExp(HEX, "gi");
1529

@@ -21,7 +35,7 @@ const disallowedBoundaryBefore = new Set(["-", ".", "/", "#"]);
2135
const disallowedBoundaryAfter = new Set(["-", ".", "/"]);
2236
const ignoredLeadingWords = new Set(["url"]);
2337

24-
function isWhitespace(char) {
38+
function isWhitespace(char: string): boolean {
2539
return (
2640
char === " " ||
2741
char === "\t" ||
@@ -31,7 +45,7 @@ function isWhitespace(char) {
3145
);
3246
}
3347

34-
function isAlpha(char) {
48+
function isAlpha(char: string): boolean {
3549
if (!char) return false;
3650
const code = char.charCodeAt(0);
3751
return (
@@ -40,41 +54,41 @@ function isAlpha(char) {
4054
);
4155
}
4256

43-
function charAt(doc, index) {
57+
function charAt(doc: Text, index: number): string {
4458
if (index < 0 || index >= doc.length) return "";
4559
return doc.sliceString(index, index + 1);
4660
}
4761

48-
function findPrevNonWhitespace(doc, index) {
62+
function findPrevNonWhitespace(doc: Text, index: number): number {
4963
for (let i = index - 1; i >= 0; i--) {
5064
if (!isWhitespace(charAt(doc, i))) return i;
5165
}
5266
return -1;
5367
}
5468

55-
function findNextNonWhitespace(doc, index) {
69+
function findNextNonWhitespace(doc: Text, index: number): number {
5670
for (let i = index; i < doc.length; i++) {
5771
if (!isWhitespace(charAt(doc, i))) return i;
5872
}
5973
return doc.length;
6074
}
6175

62-
function readWordBefore(doc, index) {
76+
function readWordBefore(doc: Text, index: number): string {
6377
let pos = index;
6478
while (pos >= 0 && isWhitespace(charAt(doc, pos))) pos--;
6579
if (pos < 0) return "";
6680
if (charAt(doc, pos) === "(") {
6781
pos--;
6882
}
6983
while (pos >= 0 && isWhitespace(charAt(doc, pos))) pos--;
70-
let end = pos;
84+
const end = pos;
7185
while (pos >= 0 && isAlpha(charAt(doc, pos))) pos--;
7286
const start = pos + 1;
7387
if (end < start) return "";
7488
return doc.sliceString(start, end + 1).toLowerCase();
7589
}
7690

77-
function shouldRenderColor(doc, start, end) {
91+
function shouldRenderColor(doc: Text, start: number, end: number): boolean {
7892
const immediatePrev = charAt(doc, start - 1);
7993
if (disallowedBoundaryBefore.has(immediatePrev)) return false;
8094

@@ -99,13 +113,18 @@ function shouldRenderColor(doc, start, end) {
99113
}
100114

101115
class ColorWidget extends WidgetType {
102-
constructor({ color, colorRaw, ...state }) {
116+
state: ColorWidgetState;
117+
color: string;
118+
colorRaw: string;
119+
120+
constructor({ color, colorRaw, ...state }: ColorWidgetParams) {
103121
super();
104122
this.state = state; // from, to, colorType, alpha
105123
this.color = color; // hex for input value
106124
this.colorRaw = colorRaw; // original css color string
107125
}
108-
eq(other) {
126+
127+
eq(other: ColorWidget): boolean {
109128
return (
110129
other.state.colorType === this.state.colorType &&
111130
other.color === this.color &&
@@ -114,7 +133,8 @@ class ColorWidget extends WidgetType {
114133
(other.state.alpha || "") === (this.state.alpha || "")
115134
);
116135
}
117-
toDOM() {
136+
137+
toDOM(): HTMLElement {
118138
const wrapper = document.createElement("span");
119139
wrapper.className = "cm-color-chip";
120140
wrapper.style.display = "inline-block";
@@ -132,20 +152,21 @@ class ColorWidget extends WidgetType {
132152
colorState.set(wrapper, this.state);
133153
return wrapper;
134154
}
135-
ignoreEvent() {
155+
156+
ignoreEvent(): boolean {
136157
return false;
137158
}
138159
}
139160

140-
function colorDecorations(view) {
141-
const deco = [];
161+
function colorDecorations(view: EditorView): DecorationSet {
162+
const deco: Range<Decoration>[] = [];
142163
const ranges = view.visibleRanges;
143164
const doc = view.state.doc;
144165
for (const { from, to } of ranges) {
145166
const text = doc.sliceString(from, to);
146167
// Any color using global matcher from utils (captures named/rgb/rgba/hsl/hsla/hex)
147168
RGBG.lastIndex = 0;
148-
for (let m; (m = RGBG.exec(text)); ) {
169+
for (let m: RegExpExecArray | null; (m = RGBG.exec(text)); ) {
149170
const raw = m[2];
150171
const start = from + m.index + m[1].length;
151172
const end = start + raw.length;
@@ -167,66 +188,69 @@ function colorDecorations(view) {
167188
}
168189
}
169190

170-
return Decoration.set(deco, { sort: true });
191+
return Decoration.set(deco, true);
171192
}
172193

173-
export const colorView = (showPicker = true) =>
174-
ViewPlugin.fromClass(
175-
class ColorViewPlugin {
176-
constructor(view) {
177-
this.decorations = colorDecorations(view);
178-
}
179-
update(update) {
180-
if (update.docChanged || update.viewportChanged) {
181-
this.decorations = colorDecorations(update.view);
182-
}
183-
const readOnly = update.view.contentDOM.ariaReadOnly === "true";
184-
const editable = update.view.contentDOM.contentEditable === "true";
185-
const canBeEdited = readOnly === false && editable;
186-
this.changePicker(update.view, canBeEdited);
187-
}
188-
changePicker(view, canBeEdited) {
189-
const doms = view.contentDOM.querySelectorAll("input[type=color]");
190-
doms.forEach((inp) => {
191-
if (!showPicker) {
192-
inp.setAttribute("disabled", "");
193-
} else {
194-
canBeEdited
195-
? inp.removeAttribute("disabled")
196-
: inp.setAttribute("disabled", "");
197-
}
198-
});
194+
class ColorViewPlugin {
195+
decorations: DecorationSet;
196+
197+
constructor(view: EditorView) {
198+
this.decorations = colorDecorations(view);
199+
}
200+
201+
update(update: ViewUpdate): void {
202+
if (update.docChanged || update.viewportChanged) {
203+
this.decorations = colorDecorations(update.view);
204+
}
205+
const readOnly = update.view.contentDOM.ariaReadOnly === "true";
206+
const editable = update.view.contentDOM.contentEditable === "true";
207+
const canBeEdited = readOnly === false && editable;
208+
this.changePicker(update.view, canBeEdited);
209+
}
210+
211+
changePicker(view: EditorView, canBeEdited: boolean): void {
212+
const doms = view.contentDOM.querySelectorAll("input[type=color]");
213+
doms.forEach((inp) => {
214+
const input = inp as HTMLInputElement;
215+
if (canBeEdited) {
216+
input.removeAttribute("disabled");
217+
} else {
218+
input.setAttribute("disabled", "");
199219
}
200-
},
201-
{
202-
decorations: (v) => v.decorations,
203-
eventHandlers: {
204-
click: async (e, view) => {
205-
const target = e.target;
206-
const chip = target?.closest?.(".cm-color-chip");
207-
if (!chip) return false;
208-
// Respect read-only and setting toggle
209-
const readOnly = view.contentDOM.ariaReadOnly === "true";
210-
const editable = view.contentDOM.contentEditable === "true";
211-
const canBeEdited = !readOnly && editable;
212-
if (!canBeEdited) return true;
213-
const data = colorState.get(chip);
214-
if (!data) return false;
215-
try {
216-
const picked = await pickColor(
217-
chip.dataset.colorraw || chip.dataset.color,
218-
);
219-
if (!picked) return true;
220+
});
221+
}
222+
}
223+
224+
export const colorView = (showPicker = true) =>
225+
ViewPlugin.fromClass(ColorViewPlugin, {
226+
decorations: (v) => v.decorations,
227+
eventHandlers: {
228+
click: (e: PointerEvent, view: EditorView): boolean => {
229+
const target = e.target as HTMLElement | null;
230+
const chip = target?.closest?.(".cm-color-chip") as HTMLElement | null;
231+
if (!chip) return false;
232+
// Respect read-only and setting toggle
233+
const readOnly = view.contentDOM.ariaReadOnly === "true";
234+
const editable = view.contentDOM.contentEditable === "true";
235+
const canBeEdited = !readOnly && editable;
236+
if (!canBeEdited) return true;
237+
const data = colorState.get(chip);
238+
if (!data) return false;
239+
240+
pickColor(chip.dataset.colorraw || chip.dataset.color || "")
241+
.then((picked: string | null) => {
242+
if (!picked) return;
220243
view.dispatch({
221244
changes: { from: data.from, to: data.to, insert: picked },
222245
});
223-
} catch {
246+
})
247+
.catch(() => {
224248
/* ignore */
225-
}
226-
return true;
227-
},
249+
});
250+
251+
return true;
228252
},
229253
},
230-
);
254+
});
231255

232256
export default colorView;

0 commit comments

Comments
 (0)