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}...`, ], ]; }, }, };