import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getHeadingAttrs } from './ParagraphNodeSpec'; import { DocServer } from '../../../DocServer'; import { Doc, Field, FieldType } from '../../../../fields/Doc'; import { schema } from './schema_rts'; const blockquoteDOM: DOMOutputSpec = ['blockquote', 0]; const hrDOM: DOMOutputSpec = ['hr']; const preDOM: DOMOutputSpec = ['pre', ['code', 0]]; const brDOM: DOMOutputSpec = ['br']; // const ulDOM: DOMOutputSpec = ['ul', 0]; function formatAudioTime(timeIn: number) { const time = Math.round(timeIn); const hours = Math.floor(time / 60 / 60); const minutes = Math.floor(time / 60) - hours * 60; const seconds = time % 60; return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); } // :: Object // [Specs](#model.NodeSpec) for the nodes defined in this schema. export const nodes: { [index: string]: NodeSpec } = { // :: NodeSpec The top level document node. doc: { content: 'block+', marks: '_', }, paragraph: ParagraphNodeSpec, audiotag: { group: 'block', attrs: { timeCode: { default: 0 }, audioId: { default: '' }, textId: { default: '' }, }, toDOM(node) { return [ 'audiotag', { class: node.attrs.textId, // style: see FormattedTextBox.scss 'data-timecode': node.attrs.timeCode, 'data-audioid': node.attrs.audioId, 'data-textid': node.attrs.textId, }, formatAudioTime(node.attrs.timeCode.toString()), ]; }, parseDOM: [ { tag: 'audiotag', getAttrs: dom => { return { timeCode: dom.getAttribute('data-timecode'), audioId: dom.getAttribute('data-audioid'), textId: dom.getAttribute('data-textid'), }; }, }, ], }, footnote: { group: 'inline', content: 'inline*', inline: true, attrs: { visibility: { default: false }, }, // This makes the view treat the node as a leaf, even though it // technically has content atom: true, toDOM: () => ['footnote', 0], parseDOM: [{ tag: 'footnote' }], }, // :: NodeSpec A blockquote (`
`) wrapping one or more blocks. blockquote: { content: 'block*', group: 'block', defining: true, parseDOM: [{ tag: 'blockquote' }], toDOM() { return blockquoteDOM; }, }, // blockquote: { // ...ParagraphNodeSpec, // defining: true, // parseDOM: [{ // tag: "blockquote", getAttrs(dom: any) { // return getParagraphNodeAttrs(dom); // } // }], // toDOM(node: any) { // const dom = toParagraphDOM(node); // (dom as any)[0] = 'blockquote'; // return dom; // }, // }, // :: NodeSpec A horizontal rule (`
`). horizontal_rule: { group: 'block', parseDOM: [{ tag: 'hr' }], toDOM() { return hrDOM; }, }, // :: NodeSpec A heading textblock, with a `level` attribute that // should hold the number 1 to 6. Parsed and serialized as `

` to // `

` elements. heading: { ...ParagraphNodeSpec, attrs: { ...ParagraphNodeSpec.attrs, level: { default: 1 }, }, parseDOM: [ { tag: 'h1', attrs: { level: 1 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, { tag: 'h2', attrs: { level: 2 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, { tag: 'h3', attrs: { level: 3 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, { tag: 'h4', attrs: { level: 4 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, { tag: 'h5', attrs: { level: 5 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, { tag: 'h6', attrs: { level: 6 }, getAttrs(dom) { return getHeadingAttrs(dom); }, }, ], toDOM(node) { const dom = toParagraphDOM(node); if (dom instanceof Array) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (dom as any)[0] = `h${node.attrs.level || 1}`; // [0] is readonly so cast away to any } return dom; }, }, // :: NodeSpec A code listing. Disallows marks or non-text inline // nodes by default. Represented as a `
` element with a
    // `` element inside of it.
    code_block: {
        content: 'inline*',
        marks: '_',
        group: 'block',
        code: true,
        defining: true,
        parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
        toDOM() {
            return preDOM;
        },
    },

    equation: {
        inline: true,
        attrs: {
            fieldKey: { default: '' },
        },
        group: 'inline',
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
            return ['div', { ...node.attrs, ...attrs }];
        },
    },

    // :: NodeSpec The text node.
    text: {
        group: 'inline',
    },

    dashComment: {
        attrs: {
            docId: { default: '' },
            reflow: { default: true },
        },
        inline: true,
        group: 'inline',
        toDOM(node) {
            const attrs = { style: `width: 40px` };
            return ['span', { ...node.attrs, ...attrs }, '←'];
        },
    },

    summary: {
        inline: true,
        attrs: {
            visibility: { default: false },
            text: { default: undefined },
            textslice: { default: undefined },
        },
        group: 'inline',
        toDOM(node) {
            const attrs = { style: `width: 40px` };
            return ['span', { ...node.attrs, ...attrs }];
        },
    },

    // :: NodeSpec An inline image (``) node. Supports `src`,
    // `alt`, and `href` attributes. The latter two default to the empty
    // string.
    image: {
        inline: true,
        attrs: {
            src: {},
            agnostic: { default: null },
            width: { default: 100 },
            alt: { default: null },
            title: { default: null },
            float: { default: 'left' },
            docId: { default: '' },
        },
        group: 'inline',
        draggable: true,
        parseDOM: [
            {
                tag: 'img[src]',
                getAttrs: dom => {
                    return {
                        src: dom.getAttribute('src'),
                        title: dom.getAttribute('title'),
                        alt: dom.getAttribute('alt'),
                        width: Math.min(100, Number(dom.getAttribute('width'))),
                    };
                },
            },
        ],
        // TODO if we don't define toDom, dragging the image crashes. Why?
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}` };
            return ['img', { ...node.attrs, ...attrs }];
        },
    },

    dashDoc: {
        inline: true,
        attrs: {
            width: { default: 200 },
            height: { default: 100 },
            title: { default: null },
            float: { default: 'right' },
            hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
            fieldKey: { default: '' },
            docId: { default: '' },
            embedding: { default: '' },
        },
        group: 'inline',
        draggable: false,
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
            return ['div', { ...node.attrs, ...attrs }];
        },
    },

    dashField: {
        inline: true,
        attrs: {
            fieldKey: { default: '' },
            docId: { default: '' },
            hideKey: { default: false },
            hideValue: { default: false },
            editable: { default: true },
        },
        leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as FieldType),
        group: 'inline',
        draggable: false,
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
            return ['div', { ...node.attrs, ...attrs }];
        },
    },

    paintButton: {
        inline: true,
        attrs: {},
        group: 'inline',
        draggable: false,
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
            return ['div', { ...node.attrs, ...attrs }];
        },
    },

    video: {
        inline: true,
        attrs: {
            src: {},
            width: { default: '100px' },
            alt: { default: null },
            title: { default: null },
        },
        group: 'inline',
        draggable: true,
        parseDOM: [
            {
                tag: 'video[src]',
                getAttrs: dom => {
                    return {
                        src: dom.getAttribute('src'),
                        title: dom.getAttribute('title'),
                        alt: dom.getAttribute('alt'),
                        width: Math.min(100, Number(dom.getAttribute('width'))),
                    };
                },
            },
        ],
        toDOM(node) {
            const attrs = { style: `width: ${node.attrs.width}` };
            return ['video', { ...node.attrs, ...attrs }];
        },
    },

    // :: NodeSpec A hard line break, represented in the DOM as `
`. hard_break: { inline: true, group: 'inline', marks: '_', selectable: false, parseDOM: [{ tag: 'br' }], toDOM() { return brDOM; }, }, ordered_list: { ...orderedList, content: 'list_item+', group: 'block', marks: '_', attrs: { bulletStyle: { default: 0 }, mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet", visibility: { default: true }, indent: { default: undefined }, }, parseDOM: [ { tag: 'ul', getAttrs: dom => { return { bulletStyle: dom.getAttribute('data-bulletStyle'), mapStyle: dom.getAttribute('data-mapStyle'), fontColor: dom.style.color, fontSize: dom.style.fontSize, fontFamily: dom.style.fontFamily, indent: dom.style.marginLeft, }; }, }, { tag: 'ol', getAttrs: dom => { return { bulletStyle: dom.getAttribute('data-bulletStyle'), mapStyle: dom.getAttribute('data-mapStyle'), fontColor: dom.style.color, fontSize: dom.style.fontSize, fontFamily: dom.style.fontFamily, indent: dom.style.marginLeft, }; }, }, ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ 'ul', { 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, style: `${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ]; } return node.attrs.visibility ? [ 'ol', { class: `${map}-ol`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, style: `list-style: none; ${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ] : ['ol', { class: `${map}-ol`, style: `list-style: none;` }]; }, }, list_item: { ...listItem, attrs: { bulletStyle: { default: 0 }, mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet" visibility: { default: true }, }, marks: '_', content: '(paragraph|audiotag)+ | ((paragraph|audiotag)+ ordered_list)', parseDOM: [ { tag: 'li', getAttrs: dom => { return { mapStyle: dom.getAttribute('data-mapStyle'), bulletStyle: dom.getAttribute('data-bulletStyle') }; }, }, ], toDOM(node: Node) { const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', { class: `${map}`, style: `${fhigh} ${fsize} ${ffam} ${fcol} `, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, node.attrs.visibility ? 0 : [ 'span', { style: `${fhigh} ${fsize} ${ffam} ${fcol} position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre`, }, `${node.firstChild?.textContent}...`, ], ]; }, }, };