-
Notifications
You must be signed in to change notification settings - Fork 426
/
Copy pathtoken_list_observer.ts
125 lines (100 loc) · 3.61 KB
/
token_list_observer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { AttributeObserver, AttributeObserverDelegate } from "./attribute_observer"
import { Multimap } from "../multimap"
export interface Token {
element: Element
attributeName: string
index: number
content: string
}
export interface TokenListObserverDelegate {
tokenMatched(token: Token): void
tokenUnmatched(token: Token): void
}
export class TokenListObserver implements AttributeObserverDelegate {
private attributeObserver: AttributeObserver
private delegate: TokenListObserverDelegate
private tokensByElement: Multimap<Element, Token>
constructor(element: Element, attributeName: string, delegate: TokenListObserverDelegate) {
this.attributeObserver = new AttributeObserver(element, attributeName, this)
this.delegate = delegate
this.tokensByElement = new Multimap()
}
get started(): boolean {
return this.attributeObserver.started
}
start() {
this.attributeObserver.start()
}
pause(callback: () => void) {
this.attributeObserver.pause(callback)
}
stop() {
this.attributeObserver.stop()
}
refresh() {
this.attributeObserver.refresh()
}
get element(): Element {
return this.attributeObserver.element
}
get attributeName(): string {
return this.attributeObserver.attributeName
}
// Attribute observer delegate
elementMatchedAttribute(element: Element) {
this.tokensMatched(this.readTokensForElement(element))
}
elementAttributeValueChanged(element: Element) {
const [unmatchedTokens, matchedTokens] = this.refreshTokensForElement(element)
this.tokensUnmatched(unmatchedTokens)
this.tokensMatched(matchedTokens)
}
elementUnmatchedAttribute(element: Element) {
this.tokensUnmatched(this.tokensByElement.getValuesForKey(element))
}
private tokensMatched(tokens: Token[]) {
tokens.forEach((token) => this.tokenMatched(token))
}
private tokensUnmatched(tokens: Token[]) {
tokens.forEach((token) => this.tokenUnmatched(token))
}
private tokenMatched(token: Token) {
this.delegate.tokenMatched(token)
this.tokensByElement.add(token.element, token)
}
private tokenUnmatched(token: Token) {
this.delegate.tokenUnmatched(token)
this.tokensByElement.delete(token.element, token)
}
private refreshTokensForElement(element: Element): [Token[], Token[]] {
const previousTokens = this.tokensByElement.getValuesForKey(element)
const currentTokens = this.readTokensForElement(element)
const firstDifferingIndex = zip(previousTokens, currentTokens).findIndex(
([previousToken, currentToken]) => !tokensAreEqual(previousToken, currentToken)
)
if (firstDifferingIndex == -1) {
return [[], []]
} else {
return [previousTokens.slice(firstDifferingIndex), currentTokens.slice(firstDifferingIndex)]
}
}
private readTokensForElement(element: Element): Token[] {
const attributeName = this.attributeName
const tokenString = element.getAttribute(attributeName) || ""
return parseTokenString(tokenString, element, attributeName)
}
}
function parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] {
return tokenString
.trim()
.split(/\s+/)
.filter((content) => content.length)
.map((content, index) => ({ element, attributeName, content, index }))
}
function zip<L, R>(left: L[], right: R[]): [L | undefined, R | undefined][] {
const length = Math.max(left.length, right.length)
return Array.from({ length }, (_, index) => [left[index], right[index]] as [L, R])
}
function tokensAreEqual(left?: Token, right?: Token) {
return left && right && left.index == right.index && left.content == right.content
}