1+ import type { Range , Text } from "@codemirror/state" ;
2+ import type { DecorationSet , ViewUpdate } from "@codemirror/view" ;
13import {
24 Decoration ,
35 EditorView ,
@@ -8,8 +10,20 @@ import pickColor from "dialogs/color";
810import color from "utils/color" ;
911import { 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
1428const HEX_RE = new RegExp ( HEX , "gi" ) ;
1529
@@ -21,7 +35,7 @@ const disallowedBoundaryBefore = new Set(["-", ".", "/", "#"]);
2135const disallowedBoundaryAfter = new Set ( [ "-" , "." , "/" ] ) ;
2236const 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
101115class 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
232256export default colorView ;
0 commit comments