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 = JSON.parse(RichTextField.Initialize()); (parsed.doc as Record).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 ); } }