-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[lexical] Feature: add a generic state property to all nodes #7117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a6288f9
a90a719
448e2af
bf70110
6ebd4e5
e46d735
e404d32
c1cfce4
6d00c64
e0c5945
7728780
d807d4f
90a032b
c663540
113a4f1
91148f3
47435b0
fb7e164
eab152e
d2b4f21
2bc4a0e
fca2071
8cf6855
abd3d3e
3f8bba0
4624e2f
a62cf45
bdd3971
27910f2
9499667
cb2369b
62eac57
4f2005f
62d4eaa
26140ad
48ba0f3
7d737ec
f82807e
858c115
f37ab4c
6d43a56
0529925
ffe2bed
44773aa
ba04882
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,14 @@ | |
|
||
import type {JSX} from 'react'; | ||
|
||
import {makeStateWrapper} from '@lexical/utils'; | ||
import { | ||
createState, | ||
DecoratorNode, | ||
DOMConversionMap, | ||
DOMConversionOutput, | ||
DOMExportOutput, | ||
LexicalNode, | ||
NodeKey, | ||
SerializedLexicalNode, | ||
Spread, | ||
} from 'lexical'; | ||
|
@@ -75,16 +76,44 @@ function $convertPollElement(domNode: HTMLElement): DOMConversionOutput | null { | |
return null; | ||
} | ||
|
||
export class PollNode extends DecoratorNode<JSX.Element> { | ||
__question: string; | ||
__options: Options; | ||
function parseOptions(json: unknown): Options { | ||
const options = []; | ||
if (Array.isArray(json)) { | ||
for (const row of json) { | ||
if ( | ||
row && | ||
typeof row.text === 'string' && | ||
typeof row.uid === 'string' && | ||
Array.isArray(row.votes) && | ||
row.votes.every((v: unknown) => typeof v === 'number') | ||
) { | ||
options.push(row); | ||
} | ||
} | ||
} | ||
return options; | ||
} | ||
|
||
const questionState = makeStateWrapper( | ||
createState('question', { | ||
parse: (v) => (typeof v === 'string' ? v : ''), | ||
}), | ||
); | ||
const optionsState = makeStateWrapper( | ||
createState('options', { | ||
isEqual: (a, b) => | ||
a.length === b.length && JSON.stringify(a) === JSON.stringify(b), | ||
parse: parseOptions, | ||
}), | ||
); | ||
Comment on lines
+97
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The state wrapper is only used to generate the accessors on the class, mostly to save a bit of code and make it easier to get the types right. I'd be fine removing the state wrapper utility if we want to scope it down, but it's already not in the core. |
||
|
||
export class PollNode extends DecoratorNode<JSX.Element> { | ||
static getType(): string { | ||
return 'poll'; | ||
} | ||
|
||
static clone(node: PollNode): PollNode { | ||
return new PollNode(node.__question, node.__options, node.__key); | ||
return new PollNode(node.__key); | ||
} | ||
|
||
static importJSON(serializedNode: SerializedPollNode): PollNode { | ||
|
@@ -94,59 +123,56 @@ export class PollNode extends DecoratorNode<JSX.Element> { | |
).updateFromJSON(serializedNode); | ||
} | ||
|
||
constructor(question: string, options: Options, key?: NodeKey) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switching to a zero required arg constructor is more compatible with our longer term goals and makes the yjs stuff more sound since it's going to call the constructor with zero args anyway |
||
super(key); | ||
this.__question = question; | ||
this.__options = options; | ||
} | ||
|
||
exportJSON(): SerializedPollNode { | ||
return { | ||
...super.exportJSON(), | ||
options: this.__options, | ||
question: this.__question, | ||
}; | ||
} | ||
getQuestion = questionState.makeGetterMethod<this>(); | ||
setQuestion = questionState.makeSetterMethod<this>(); | ||
getOptions = optionsState.makeGetterMethod<this>(); | ||
setOptions = optionsState.makeSetterMethod<this>(); | ||
Comment on lines
+126
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are just conveniences, could be written out explicitly |
||
|
||
addOption(option: Option): void { | ||
const self = this.getWritable(); | ||
const options = Array.from(self.__options); | ||
options.push(option); | ||
self.__options = options; | ||
addOption(option: Option): this { | ||
return this.setOptions((options) => [...options, option]); | ||
} | ||
|
||
deleteOption(option: Option): void { | ||
const self = this.getWritable(); | ||
const options = Array.from(self.__options); | ||
const index = options.indexOf(option); | ||
options.splice(index, 1); | ||
self.__options = options; | ||
deleteOption(option: Option): this { | ||
return this.setOptions((prevOptions) => { | ||
const index = prevOptions.indexOf(option); | ||
if (index === -1) { | ||
return prevOptions; | ||
} | ||
const options = Array.from(prevOptions); | ||
options.splice(index, 1); | ||
return options; | ||
}); | ||
} | ||
|
||
setOptionText(option: Option, text: string): void { | ||
const self = this.getWritable(); | ||
const clonedOption = cloneOption(option, text); | ||
const options = Array.from(self.__options); | ||
const index = options.indexOf(option); | ||
options[index] = clonedOption; | ||
self.__options = options; | ||
setOptionText(option: Option, text: string): this { | ||
return this.setOptions((prevOptions) => { | ||
const clonedOption = cloneOption(option, text); | ||
const options = Array.from(prevOptions); | ||
const index = options.indexOf(option); | ||
options[index] = clonedOption; | ||
return options; | ||
}); | ||
} | ||
|
||
toggleVote(option: Option, clientID: number): void { | ||
const self = this.getWritable(); | ||
const votes = option.votes; | ||
const votesClone = Array.from(votes); | ||
const voteIndex = votes.indexOf(clientID); | ||
if (voteIndex === -1) { | ||
votesClone.push(clientID); | ||
} else { | ||
votesClone.splice(voteIndex, 1); | ||
} | ||
const clonedOption = cloneOption(option, option.text, votesClone); | ||
const options = Array.from(self.__options); | ||
const index = options.indexOf(option); | ||
options[index] = clonedOption; | ||
self.__options = options; | ||
toggleVote(option: Option, clientID: number): this { | ||
return this.setOptions((prevOptions) => { | ||
const index = prevOptions.indexOf(option); | ||
if (index === -1) { | ||
return prevOptions; | ||
} | ||
const votes = option.votes; | ||
const votesClone = Array.from(votes); | ||
const voteIndex = votes.indexOf(clientID); | ||
if (voteIndex === -1) { | ||
votesClone.push(clientID); | ||
} else { | ||
votesClone.splice(voteIndex, 1); | ||
} | ||
const clonedOption = cloneOption(option, option.text, votesClone); | ||
const options = Array.from(prevOptions); | ||
options[index] = clonedOption; | ||
return options; | ||
}); | ||
} | ||
|
||
static importDOM(): DOMConversionMap | null { | ||
|
@@ -165,10 +191,10 @@ export class PollNode extends DecoratorNode<JSX.Element> { | |
|
||
exportDOM(): DOMExportOutput { | ||
const element = document.createElement('span'); | ||
element.setAttribute('data-lexical-poll-question', this.__question); | ||
element.setAttribute('data-lexical-poll-question', this.getQuestion()); | ||
element.setAttribute( | ||
'data-lexical-poll-options', | ||
JSON.stringify(this.__options), | ||
JSON.stringify(this.getOptions()), | ||
); | ||
return {element}; | ||
} | ||
|
@@ -186,16 +212,16 @@ export class PollNode extends DecoratorNode<JSX.Element> { | |
decorate(): JSX.Element { | ||
return ( | ||
<PollComponent | ||
question={this.__question} | ||
options={this.__options} | ||
question={this.getQuestion()} | ||
options={this.getOptions()} | ||
nodeKey={this.__key} | ||
/> | ||
); | ||
} | ||
} | ||
|
||
export function $createPollNode(question: string, options: Options): PollNode { | ||
return new PollNode(question, options); | ||
return new PollNode().setQuestion(question).setOptions(options); | ||
} | ||
|
||
export function $isPollNode( | ||
|
Uh oh!
There was an error while loading. Please reload this page.