aboutsummaryrefslogtreecommitdiff
path: root/src/fields/RichTextField.ts
blob: 79ba34ada42063d31f2664f8498e81b0a486601d (plain)
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { serializable } from 'serializr';
import { scriptingGlobal } from '../client/util/ScriptingGlobals';
import { Deserializable } from '../client/util/SerializationHelper';
import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
import { ObjectField } from './ObjectField';

@scriptingGlobal
@Deserializable('RichTextField')
export class RichTextField extends ObjectField {
    @serializable(true)
    readonly Data: string;

    @serializable(true)
    readonly Text: string;

    /**
     * NOTE: if 'text' doesn't match the plain text of 'data', this can cause infinite loop problems or other artifacts when rendered.
     * @param data this is the formatted text representation of the RTF
     * @param text this is the plain text of whatever text is in the 'data'
     */
    constructor(data: string, text: string) {
        super();
        this.Data = data;
        this.Text = text; // ideally, we'd compute 'text' from 'data' by doing what Prosemirror does at run-time ... just need to figure out how to write that function accurately
    }

    Empty() {
        return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align'));
    }

    [Copy]() {
        return new RichTextField(this.Data, this.Text);
    }

    [ToJavascriptString]() {
        return '`' + this.Text + '`';
    }
    [ToScriptString]() {
        return `new RichTextField(\`${this.Data?.replace(/"/g, '\\"')}\`, \`${this.Text}\`)`;
    }
    [ToString]() {
        return this.Text;
    }

    public static RTFfield() {
        return new RichTextField(
            `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`,
            ''
        );
    }
    static Initialize = (initial?: string) => {
        const content: object[] = [];
        const state = {
            doc: {
                type: 'doc',
                content,
            },
            selection: {
                type: 'text',
                anchor: 0,
                head: 0,
            },
        };
        if (initial && initial.length) {
            content.push({
                type: 'paragraph',
                content: {
                    type: 'text',
                    text: initial,
                },
            });
            state.selection.anchor = state.selection.head = initial.length + 1;
        }
        return JSON.stringify(state);
    };

    private static ToProsemirrorState = (plainText: string, selectBack?: number, delimeter = '\n') => {
        // Remap the text, creating blocks split on newlines
        const elements = plainText.split(delimeter);

        // Google Docs adds in an extra carriage return automatically, so this counteracts it
        !elements[elements.length - 1].length && elements.pop();

        // Preserve the current state, but re-write the content to be the blocks
        const parsed: Record<string, unknown> = JSON.parse(RichTextField.Initialize());
        (parsed.doc as Record<string, unknown>).content = elements.map(text => {
            const paragraph: object = {
                type: 'paragraph',
                content: text.length ? [{ type: 'text', marks: [], text }] : undefined, // An empty paragraph gets treated as a line break
            };
            return paragraph;
        });

        // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
        parsed.selection = { type: 'text', anchor: 2 + plainText.length - (selectBack ?? 0), head: 2 + plainText.length };

        // Export the ProseMirror-compatible state object we've just built
        return JSON.stringify(parsed);
    };

    public static textToRtf(text: string, imgDocId?: string, selectBack?: number) {
        return new RichTextField(
            !imgDocId
                ? this.ToProsemirrorState(text, selectBack)
                : JSON.stringify({
                      // this is a RichText json that has the question text placed above a related image
                      doc: {
                          type: 'doc',
                          content: [
                              {
                                  type: 'paragraph',
                                  attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
                                  content: [
                                      ...(text ? [{ type: 'text', text }] : []), 
                                      ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []),
                                  ],
                              },
                          ],
                      },
                      selection: { type: 'text', anchor: 2 + text.length, head: 2 + text.length },
                  }),
            text
        );
    }

    // AARAV ADD

    public static textToRtfFormatting(
        text: string, 
        imgDocId?: string, 
        selectBack?: number, 
        styles?: { bold?: boolean, italic?: boolean, fontSize?: number, color?: string }
    ) {
        return new RichTextField(
            !imgDocId
                ? this.ToProsemirrorState(text, selectBack)
                : JSON.stringify({
                      // This is the RichText JSON with the text and optional image
                      doc: {
                          type: 'doc',
                          content: [
                              {
                                  type: 'paragraph',
                                  attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
                                  content: [
                                      {
                                          type: 'text',
                                          text: text,
                                          marks: [
                                            ...(styles?.bold ? [{ type: 'bold' }] : []),  
                                            ...(styles?.italic ? [{ type: 'italic' }] : []),  
                                            ...(styles?.fontSize ? [{ type: 'textStyle', style: `font-size:${styles.fontSize}px` }] : []),  
                                            ...(styles?.color ? [{ type: 'textStyle', style: `color:${styles.color}` }] : []), 
                                        ]
                                      }
                                  ]
                              }
                          ]
                      },
                      selection: { type: 'text', anchor: 2 + text.length, head: 2 + text.length },
                  }),
            text
        );
    }
    
    
    
    
}