1+ export default class Typed {
2+ constructor ( element , config ) {
3+ config . typeSpeed = config . typeSpeed ?? 100 ;
4+ this . speed = config . typeSpeed ; // use default type speed
5+ this . nextPause = null ;
6+ this . config = config ;
7+ this . timeout = null ;
8+
9+ this . el = typeof element === 'string'
10+ ? document . querySelector ( element )
11+ : element ;
12+
13+ this . nodeCounter = 0 ;
14+ this . setDisplay ( this . el , config . strings [ 0 ] ) ;
15+ this . spans = this . el . querySelectorAll ( 'span' ) ;
16+
17+ // We only have one string per instance, so these callbacks are equivalent.
18+ this . config . onBegin ( this ) ;
19+ this . config . preStringTyped ( 0 , this ) ;
20+
21+ this . typewrite ( ) ;
22+ }
23+
24+ get strings ( ) {
25+ return this . config . strings ;
26+ }
27+
28+ /**
29+ @param {HTMLElement } element
30+ @param {string } curString
31+ */
32+ setDisplay ( element , curString ) {
33+ const newElement = document . createElement ( 'div' ) ;
34+ newElement . innerHTML = curString ;
35+ const textNodes = getLeafNodes ( newElement ) ;
36+ this . actions = [ ] ;
37+ for ( const textNode of textNodes ) {
38+ const [ nodes , actions ] = this . parseString ( textNode . textContent ) ;
39+ this . actions = this . actions . concat ( ...actions ) ;
40+
41+ // overwrite the node with <span> text
42+ textNode . replaceWith ( ...nodes ) ;
43+ }
44+ element . replaceChildren ( ...newElement . childNodes ) ;
45+ }
46+
47+ stop ( ) {
48+ clearTimeout ( this . timeout ) ;
49+ this . config . onStop ( 0 , this ) ;
50+ }
51+
52+ start ( ) {
53+ if ( ! this . timeout ) {
54+ this . typewrite ( ) ;
55+ this . config . onStart ( 0 , this ) ;
56+ }
57+ }
58+
59+ parseString ( curString ) {
60+
61+ // Separate curString into text and action sections
62+ // eg: "{speed:10}hello {pause:1000}{speed:1000}world!"
63+ // -> [ '', '{speed:10}', 'hello ', '{pause:1000}', '', '{speed:1000}', 'world!' ]
64+ // `(?:<pattern>)` is a non-capturing group, see https://devdocs.io/javascript/regular_expressions/non-capturing_group
65+ const actionPattern = / ( \{ (?: p a u s e | s p e e d ) : \d + \} ) / ;
66+ const sections = curString . split ( actionPattern ) ;
67+
68+ const nodes = [ ] ;
69+ const actions = [ ] ;
70+ let nodeCounter = 0 ;
71+
72+ sections . forEach ( ( section , i ) => {
73+ // text section
74+ if ( i % 2 === 0 ) {
75+ // iterate over the string, adding <span>s to the element as we go
76+ for ( const char of section ) {
77+ const textNode = document . createTextNode ( char ) ;
78+ let node ;
79+ const isWhite = / \s / . test ( char ) ;
80+ if ( isWhite ) {
81+ node = textNode ;
82+ } else {
83+ nodeCounter ++ ;
84+ node = document . createElement ( 'span' ) ;
85+ node . append ( textNode ) ;
86+ node . style . visibility = 'hidden' ;
87+ }
88+ nodes . push ( node ) ;
89+ }
90+
91+ // action section
92+ } else {
93+ // extract action and parameter
94+ const match = / \{ (?< action > p a u s e | s p e e d ) : (?< n > \d + ) \} / . exec ( section ) ;
95+ actions [ nodeCounter ] = {
96+ action : match . groups . action ,
97+ n : match . groups . n ,
98+ } ;
99+ }
100+ } ) ;
101+ // Force length of 'actions' to equal nodeCounter
102+ actions [ nodeCounter - 1 ] = actions [ nodeCounter - 1 ] ;
103+ return [ nodes , actions ] ;
104+ }
105+
106+ executeAction ( action ) {
107+ if ( action . action == 'speed' ) {
108+ this . speed = action . n ; // overwrites speed value permanently
109+ } else if ( action . action == 'pause' ) {
110+ this . config . onTypingPaused ( 0 , this ) ;
111+ this . nextPause = action . n ; // sets a wait time temporarily
112+ }
113+
114+ }
115+
116+ typewrite ( ) {
117+ if ( this . actions [ this . nodeCounter ] ) {
118+ this . executeAction ( this . actions [ this . nodeCounter ] ) ;
119+ }
120+ const waitTime = this . nextPause ?? this . speed ;
121+ this . timeout = setTimeout ( ( ) => {
122+ if ( this . nextPause ) {
123+ this . nextPause = null ;
124+ this . config . onTypingResumed ( 0 , this ) ;
125+ }
126+ this . spans [ this . nodeCounter ] . style = '' ;
127+ this . nodeCounter += 1 ;
128+ if ( this . nodeCounter < this . spans . length ) {
129+ this . typewrite ( ) ;
130+ } else {
131+ this . timeout = null ;
132+ this . config . onStringTyped ( 0 , this ) ;
133+ }
134+ } , waitTime ) ;
135+ }
136+
137+ destroy ( ) {
138+ clearTimeout ( this . timeout ) ;
139+ this . timeout = null ;
140+ this . el . replaceChildren ( ) ;
141+ this . config . onDestroy ( this ) ;
142+ }
143+ }
144+
145+ function getLeafNodes ( node ) {
146+ const leafNodes = [ ] ;
147+
148+ function traverse ( currentNode ) {
149+ if ( currentNode . childNodes . length === 0 ) {
150+ // It's a leaf node (no child nodes)
151+ leafNodes . push ( currentNode ) ;
152+ } else {
153+ // Recursively process child nodes
154+ currentNode . childNodes . forEach ( child => traverse ( child ) ) ;
155+ }
156+ }
157+
158+ traverse ( node ) ;
159+ return leafNodes ;
160+ }
0 commit comments