From 32084b2889c2aadd7fbb95346fec54c0582cea7e Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Wed, 11 Dec 2024 08:40:53 -0500 Subject: [PATCH 1/6] WIP --- .DS_Store | Bin 6148 -> 6148 bytes package-lock.json | 123 ++-------------------- package.json | 3 +- src/data/test-milestone.js | 9 ++ src/render/model/xml.js | 208 +++++++++++++++++++++++-------------- 5 files changed, 152 insertions(+), 191 deletions(-) create mode 100644 src/data/test-milestone.js diff --git a/.DS_Store b/.DS_Store index 62b60b8019d5e3654ab3367b9f7d308a337aa636..cdc8eda77d0d917ed493938a3994ae7aca486ea1 100644 GIT binary patch delta 80 zcmZoMXfc=|#>AjHu~2w2AG3@g69WV=0x2LCVAw3p{Dx(-1M^{~&Fmcf96+U;4Vk|) YPv#d<B)qF;R?_gMop8V`8F+G!u{k0xUqx$iN`Lz>tzuUR;orlb^IPaV29t zNSu`+iJ_DslOYEo2Nd0rRFIQdTw-8wjgg6&g_Vt+gPnt$BQ`iAzdX1kv81%vDX}OT z#0$yK&q;!@6O+O+Q_JH8M4a>UN)j{kQj5SEGE-84N@Bt@^HTE5o$^cbQi{QP>w_UO z9Go1S@d6Un)y5VEItnJnmbE$x)s{wJwpnd0Cx@uAzI9N1c1~_yeizVjK)?v}4jAx4 zX&BWFWFUDdr8qe!3Ft(SawOrh;G(>oe5Sngi5l`7Gw-rYHV|Ri%nkG*Fa$RyerKM{ VFQUi_3O$9%h9c6N14Onk0|3OBS8@OV diff --git a/package-lock.json b/package-lock.json index ebbffd34..179d5ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,8 @@ "semver": "^7.6.0", "uuid": "^8.1.0", "w3c-xmlserializer": "^2.0.0", - "xml-formatter": "^2.5.0" + "xml-formatter": "^2.5.0", + "xpath": "^0.0.34" }, "devDependencies": { "@babel/core": "^7.24.6", @@ -5723,20 +5724,6 @@ "url": "https://github.com/sponsors/rsimon" } }, - "node_modules/@recogito/recogito-client-core/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@recogito/recogito-client-core/node_modules/react-autosize-textarea": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz", @@ -5751,36 +5738,11 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@recogito/recogito-client-core/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, "node_modules/@recogito/recogito-client-core/node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, - "node_modules/@recogito/recogito-client-core/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/@redux-saga/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", @@ -6478,16 +6440,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "17.0.25", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.25.tgz", - "integrity": "sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==", - "optional": true, - "peer": true, - "dependencies": { - "@types/react": "^17" - } - }, "node_modules/@types/react-redux": { "version": "7.1.33", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", @@ -7324,20 +7276,6 @@ "resolved": "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz", "integrity": "sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw==" }, - "node_modules/annotorious-openseadragon/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/annotorious-openseadragon/node_modules/react-autosize-textarea": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz", @@ -7352,21 +7290,6 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/annotorious-openseadragon/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, "node_modules/annotorious-openseadragon/node_modules/react-select": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.2.0.tgz", @@ -7386,16 +7309,6 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, - "node_modules/annotorious-openseadragon/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -21005,15 +20918,6 @@ } } }, - "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-router": { "version": "6.23.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", @@ -24170,7 +24074,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=10" }, @@ -24273,19 +24177,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/typescript-compare": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", @@ -25932,6 +25823,14 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 857b0757..f8ff5e5a 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ "semver": "^7.6.0", "uuid": "^8.1.0", "w3c-xmlserializer": "^2.0.0", - "xml-formatter": "^2.5.0" + "xml-formatter": "^2.5.0", + "xpath": "^0.0.34" }, "eslintConfig": { "extends": "react-app" diff --git a/src/data/test-milestone.js b/src/data/test-milestone.js new file mode 100644 index 00000000..b606d3e5 --- /dev/null +++ b/src/data/test-milestone.js @@ -0,0 +1,9 @@ +export const data = [ + { + id: '83cd79d8-73fb-4217-b28a-f2b43f863b31', + path: '/TEI[1]/text[1]/front[1]/titlePage[1]/docAuthor[1]::60 /TEI[1]/text[1]/front[1]/titlePage[1]/docAuthor[1]::74', + author: 'Admin', + annotation: "Person" + } +] + diff --git a/src/render/model/xml.js b/src/render/model/xml.js index 8b71f187..7ebba985 100644 --- a/src/render/model/xml.js +++ b/src/render/model/xml.js @@ -1,4 +1,7 @@ import { DOMSerializer, Fragment } from "prosemirror-model" +import xpath from 'xpath' +import serialize from "w3c-xmlserializer" +import { data } from '../../data/test-milestone' // These elements are processed in XSLT by the xsl:strip-space command. This list is from xml/tei/odd/stripspace.xsl.model, TEI v4.1.0 const xmlStripSpaceNames = "TEI abstract additional address adminInfo altGrp altIdentifier alternate analytic annotation annotationBlock app appInfo application arc argument att attDef attList availability back biblFull biblStruct bicond binding bindingDesc body broadcast cRefPattern calendar calendarDesc castGroup castList category certainty char charDecl charProp choice cit classDecl classSpec classes climate cond constraintSpec content correction correspAction correspContext correspDesc custodialHist dataRef dataSpec datatype decoDesc dimensions div div1 div2 div3 div4 div5 div6 div7 divGen docTitle eLeaf eTree editionStmt editorialDecl elementSpec encodingDesc entry epigraph epilogue equipment event exemplum fDecl fLib facsimile figure fileDesc floatingText forest front fs fsConstraints fsDecl fsdDecl fvLib gap gi glyph graph graphic group handDesc handNotes history hom hyphenation iNode if imprint incident index interpGrp interpretation join joinGrp keywords kinesic langKnowledge langUsage layoutDesc leaf lg linkGrp list listAnnotation listApp listBibl listChange listEvent listForest listNym listObject listOrg listPerson listPlace listPrefixDef listRef listRelation listTranspose listWit location locusGrp macroSpec media metDecl model modelGrp modelSequence moduleRef moduleSpec monogr msContents msDesc msFrag msIdentifier msItem msItemStruct msPart namespace node normalization notatedMusic notesStmt nym object objectDesc objectIdentifier org paramList paramSpec particDesc performance person personGrp persona physDesc place population postscript precision prefixDef profileDesc projectDesc prologue publicationStmt punctuation quotation rdgGrp recordHist recording recordingStmt refsDecl relatedItem relation remarks respStmt respons revisionDesc root row samplingDecl schemaRef schemaSpec scriptDesc scriptStmt seal sealDesc segmentation sequence seriesStmt set setting settingDesc sourceDesc sourceDoc sp spGrp space spanGrp specGrp specList standOff state stdVals styleDefDecl subst substJoin superEntry supportDesc surface surfaceGrp table tagsDecl taxonomy teiCorpus teiHeader terrain text textClass textDesc timeline titlePage titleStmt trait transcriptionDesc transpose tree triangle typeDesc unitDecl unitDef vAlt vColl vDefault vLabel vMerge vNot vRange valItem valList vocal".split(' ') @@ -7,53 +10,102 @@ export function parseText(textEl, teiDocument, teiSchema, subDocName) { // make the TEIDocument visible to the node spec parser for access to sub docs teiSchema.teiDocuments.push(teiDocument) stripSpaces(textEl) - parseInterNodes(textEl,teiSchema,teiDocument.xmlDom) + console.log(textEl) + parseInterNodes(textEl, teiSchema, teiDocument.xmlDom) + + // TODO: Test loading milestones + if (subDocName === 'text') { + data.forEach(d => { + const pathsRaw = d.path.split(' ') + const path1 = pathsRaw[0].split('::'); + const path2 = pathsRaw[1].split('::') + insertMilestone(teiDocument.xmlDom, [path1[0], path1[1]], [path2[0], path2[1]], d.id) + }) + } const domParser = subDocName !== 'text' ? teiSchema.docNodeParsers[subDocName] : teiSchema.domParser const doc = domParser.parse(textEl) teiSchema.teiDocuments.pop() return doc } +function insertMilestone(xmlDom, start, end, id) { + const select = xpath.useNamespaces({ + teiXML: "http://www.tei-c.org/ns/1.0", + }); + const targetNodeStart = select(start[0].replaceAll('/', '/teiXML:'), xmlDom); + const targetNodeEnd = select(end[0].replaceAll('/', '/teiXML:'), xmlDom); + if (targetNodeStart[0] && targetNodeEnd[0] && start[0] === end[0]) { + let serializedXml = serialize(targetNodeStart[0]); + console.log('Target Start: ', serializedXml) + const normalized = serializedXml.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') + const newNodeStr = normalized.slice(0, parseInt(start[1]) - 1) + + `` + + normalized.slice(parseInt(start[1]) - 1, parseInt(end[1])) + + `` + + normalized.slice(parseInt(end[1])) + console.log('Normalized: ', newNodeStr) + const parser = new DOMParser(); + const newNode = parser.parseFromString(newNodeStr, "text/html") + targetNodeStart[0].replaceWith(newNode[0]) + } else if (targetNodeStart[0] && targetNodeEnd[0]) { + let serializedXmlStart = serialize(targetNodeStart[0]); + console.log('Target Start: ', serializedXmlStart) + let serializedXmlEnd = serialize(targetNodeEnd[0]); + console.log('Target End: ', serializedXmlEnd) + const normalizedStart = serializedXmlStart.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') + const newNodeStrStart = normalizedStart.slice(0, parseInt(start[1]) - 1) + `` + normalizedStart.slice(parseInt(start[1]) - 1) + console.log('Normalized Start: ', newNodeStrStart) + const normalizedEnd = serializedXmlEnd.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') + const newNodeStrEnd = normalizedEnd.slice(0, parseInt(end[1]) - 1) + `` + normalizedEnd.slice(parseInt(end[1]) - 1) + console.log('Normalized End: ', newNodeStrEnd) + const parser = new DOMParser(); + const newNodeStart = parser.parseFromString(newNodeStrStart, "text/html") + targetNodeStart[0].replaceWith(newNodeStart[0]) + const newNodeEnd = parser.parseFromString(newNodeStrEnd, "text/html") + targetNodeEnd[0].replaceWith(newNodeEnd[0]) + } +} + export function serializeText(doc, teiDocument, teiSchema) { const { resourceType, xmlDom } = teiDocument teiSchema.teiMode = true let textEl, domFragment - if( resourceType === 'header' ) { + if (resourceType === 'header') { domFragment = proseMirrorToDOM(doc.content, teiDocument, teiSchema, 'header') - textEl = xmlDom.getElementsByTagName('teiHeader')[0] - } else if( resourceType === 'text' ) { + textEl = xmlDom.getElementsByTagName('teiHeader')[0] + } else if (resourceType === 'text') { domFragment = proseMirrorToDOM(doc.content, teiDocument, teiSchema) - textEl = xmlDom.getElementsByTagName('text')[0] - } else if( resourceType === 'standOff' ) { + textEl = xmlDom.getElementsByTagName('text')[0] + } else if (resourceType === 'standOff') { domFragment = proseMirrorToDOM(doc.content, teiDocument, teiSchema) - textEl = xmlDom.getElementsByTagName('standOff')[0] - } else if( resourceType === 'sourceDoc' ) { + textEl = xmlDom.getElementsByTagName('standOff')[0] + } else if (resourceType === 'sourceDoc') { domFragment = proseMirrorToDOM(doc.content, teiDocument, teiSchema) - textEl = xmlDom.getElementsByTagName('sourceDoc')[0] + textEl = xmlDom.getElementsByTagName('sourceDoc')[0] } // take the body of the document from prosemirror and reunite it with // the rest of the xml document, then serialize to string var div = document.createElement('div') - div.appendChild( domFragment.cloneNode(true) ) - textEl.innerHTML = htmlToXML(div.innerHTML,teiSchema.elements,teiSchema.attrs) + div.appendChild(domFragment.cloneNode(true)) + textEl.innerHTML = htmlToXML(div.innerHTML, teiSchema.elements, teiSchema.attrs) const fileContents = new XMLSerializer().serializeToString(teiDocument.xmlDom); teiSchema.teiMode = false - + return fileContents } -export function proseMirrorToDOM( content, teiDocument, teiSchema, subDocName ) { +export function proseMirrorToDOM(content, teiDocument, teiSchema, subDocName) { const { inter } = teiSchema.elementGroups // make the TEIDocument visible to the serialize for access to sub docs teiSchema.teiDocuments.push(teiDocument) const schema = subDocName ? teiSchema.docNodeSchemas[subDocName] : teiSchema.schema - const domSerializer = DOMSerializer.fromSchema( schema ) + const domSerializer = DOMSerializer.fromSchema(schema) const domFragment = domSerializer.serializeFragment(content) // remove all text node channels TODO: what is max i? - for( let i=0; i<99; i++ ) { + for (let i = 0; i < 99; i++) { removeNodes(domFragment, `textNode${i}`) removeNodes(domFragment, `globalNode${i}`) } @@ -70,67 +122,67 @@ export function getTextNodeName(content) { return textNodeName } -export function findNoteNode( doc, noteID ) { +export function findNoteNode(doc, noteID) { let noteNode, notePos - doc.descendants( (node,pos) => { - if( node.attrs['__id__'] === noteID ) { + doc.descendants((node, pos) => { + if (node.attrs['__id__'] === noteID) { noteNode = node notePos = pos } - if( noteNode ) return false + if (noteNode) return false }) return { noteNode, notePos } } export function synthNameToElementName(nodeName) { - if( nodeName.includes('textNode') || nodeName.includes('globalNode') ) return null - return nodeName.endsWith('X') ? nodeName.slice(0,-1) : nodeName.startsWith('mark') ? nodeName.slice('mark'.length) : nodeName + if (nodeName.includes('textNode') || nodeName.includes('globalNode')) return null + return nodeName.endsWith('X') ? nodeName.slice(0, -1) : nodeName.startsWith('mark') ? nodeName.slice('mark'.length) : nodeName } // Internodes are a set of elements that can be processed as either nodes or marks, depending on their // location in the document structure. They have to be determined before parsing with ProseMirror. -function parseInterNodes(textEl,teiSchema,xmlDom) { +function parseInterNodes(textEl, teiSchema, xmlDom) { const { elements, elementGroups } = teiSchema const { hard, soft, inter, asides } = elementGroups - const nodes = [...hard,...soft] + const nodes = [...hard, ...soft] const markPrefix = 'mark' // asides which can't have textNode direct children - const hardAsides = asides.filter( aside => { + const hardAsides = asides.filter(aside => { return !elements[`${aside}X`].content.includes('textNode') }) // Node names are uppercase, so these are uppercased. Also, need to test against // both aside node name and the 'X' variant used in sub documents - const interMarkSet = [ ...hard.map(i => i.toUpperCase()), ...hardAsides.map(i => i.toUpperCase()), ...hardAsides.map(i => `${i.toUpperCase()}X`) ] + const interMarkSet = [...hard.map(i => i.toUpperCase()), ...hardAsides.map(i => i.toUpperCase()), ...hardAsides.map(i => `${i.toUpperCase()}X`)] function isMark(markEl) { let parentNodeName = markEl.parentNode.nodeName.toUpperCase() // is its parent a node that precludes it from being a mark? - if( interMarkSet.includes(parentNodeName) ) return false + if (interMarkSet.includes(parentNodeName)) return false // are there any node descendants of markEl? - for( const node of nodes ) { + for (const node of nodes) { const nodeEls = markEl.querySelectorAll(node) - if( nodeEls.length > 0 ) return false + if (nodeEls.length > 0) return false } return true } // pre-parse inter nodes, separating would be marks from nodes - for( const xmlTag of inter ) { + for (const xmlTag of inter) { const markEls = textEl.querySelectorAll(xmlTag) - for( let i=0; i < markEls.length; i++ ) { + for (let i = 0; i < markEls.length; i++) { const markEl = markEls[i] // if this is a mark.. rename to interMark tag - if( isMark(markEl) ) { + if (isMark(markEl)) { const elName = `${markPrefix}${xmlTag}` const interEl = xmlDom.createElement(elName) interEl.innerHTML = markEl.innerHTML - cloneAttributes(interEl,markEl) - markEl.parentNode.replaceChild(interEl,markEl) + cloneAttributes(interEl, markEl) + markEl.parentNode.replaceChild(interEl, markEl) } } } @@ -139,29 +191,29 @@ function parseInterNodes(textEl,teiSchema,xmlDom) { // Remove whitespace from children of xmlStripSpaceNames elements, // as per TEI Guidelines section 1.3.1.1.6 function stripSpaces(textEl) { - for( const xmlStripSpaceName of xmlStripSpaceNames ) { + for (const xmlStripSpaceName of xmlStripSpaceNames) { const stripEls = textEl.querySelectorAll(xmlStripSpaceName) - for( let i=0; i < stripEls.length; i++ ) { + for (let i = 0; i < stripEls.length; i++) { const stripEl = stripEls[i] - for( let j=0; j < stripEl.childNodes.length; j++ ) { + for (let j = 0; j < stripEl.childNodes.length; j++) { const child = stripEl.childNodes[j] - if( child.nodeName === '#text' ) { + if (child.nodeName === '#text') { stripEl.removeChild(child) } } } - } + } } // remove temporary nodes from the DOM and replace each with its children -function removeNodes(documentFragment,nodeName) { +function removeNodes(documentFragment, nodeName) { const nodes = documentFragment.querySelectorAll(nodeName) - for( let i=0; i < nodes.length; i++ ) { + for (let i = 0; i < nodes.length; i++) { const node = nodes[i] const items = [] - for( let j=0; j < node.childNodes.length; j++ ) { + for (let j = 0; j < node.childNodes.length; j++) { items.push(node.childNodes.item(j)) - } + } node.before(...items) node.remove() } @@ -169,88 +221,88 @@ function removeNodes(documentFragment,nodeName) { // convert all intermarks back to their XML node names function renameInterMarks(inter, documentFragment, xmlDom) { - for( const interMark of inter ) { + for (const interMark of inter) { const interMarkName = `mark${interMark}` const markEls = documentFragment.querySelectorAll(interMarkName) - for( let i=0; i < markEls.length; i++ ) { + for (let i = 0; i < markEls.length; i++) { const markEl = markEls[i] const interEl = xmlDom.createElement(interMark) interEl.innerHTML = markEl.innerHTML - cloneAttributes(interEl,markEl) - markEl.parentNode.replaceChild(interEl,markEl) + cloneAttributes(interEl, markEl) + markEl.parentNode.replaceChild(interEl, markEl) } } } // Apply various transformations to move from HTML -> XML -export function htmlToXML(html,elements,attrs) { +export function htmlToXML(html, elements, attrs) { // This entitiy is not valid XML - const nextHTML = html.replaceAll(' ',' ') + const nextHTML = html.replaceAll(' ', ' ') // this has to be done here because HTML is case insenitive, while XML is not. - return renameCamelCase(nextHTML,elements,attrs) + return renameCamelCase(nextHTML, elements, attrs) } -export function addTextNodes(state, dispatch=null) { +export function addTextNodes(state, dispatch = null) { const { tr, schema, doc } = state // if an element could have a textnode, but is instead empty, add a textnode to it - doc.descendants((node,pos) => { + doc.descendants((node, pos) => { const contentExp = node.type.spec.content - if( node.childCount === 0 && contentExp && contentExp.includes('textNode') ) { + if (node.childCount === 0 && contentExp && contentExp.includes('textNode')) { const textNodeName = getTextNodeName(contentExp) const textNodeType = schema.nodes[textNodeName] - const insertPos = tr.mapping.map(pos+1) - tr.setMeta('addToHistory',false) - tr.insert( insertPos, textNodeType.create() ) + const insertPos = tr.mapping.map(pos + 1) + tr.setMeta('addToHistory', false) + tr.insert(insertPos, textNodeType.create()) } return true }) - if( dispatch ) { + if (dispatch) { dispatch(tr) } else { - const {state: nextState} = state.applyTransaction(tr) + const { state: nextState } = state.applyTransaction(tr) return nextState.doc } } // take content fragment and replace any text nodes in there with text node type -export function replaceTextNodes( textNodeType, fragment ) { +export function replaceTextNodes(textNodeType, fragment) { let siblings = [] - for( let i=0; i < fragment.childCount; i++ ) { + for (let i = 0; i < fragment.childCount; i++) { const sibling = fragment.child(i) - if( sibling.type.name.includes('textNode') && sibling.type.name !== textNodeType.name ) { + if (sibling.type.name.includes('textNode') && sibling.type.name !== textNodeType.name) { const textNodeContent = sibling.content - for( let i=0; i < textNodeContent.childCount; i++ ) { + for (let i = 0; i < textNodeContent.childCount; i++) { const node = textNodeContent.child(i) - for( const mark of node.marks ) { - if( !textNodeType.allowsMarkType(mark.type) ) { + for (const mark of node.marks) { + if (!textNodeType.allowsMarkType(mark.type)) { // if any marks are not allowed for this textNode return null } } } - const nextSib = textNodeType.create(sibling.attr, sibling.content ) - siblings.push( nextSib ) + const nextSib = textNodeType.create(sibling.attr, sibling.content) + siblings.push(nextSib) } else { - siblings.push( sibling ) + siblings.push(sibling) } } // return new content fragment - return Fragment.from(siblings) + return Fragment.from(siblings) } // Repair camel cased attrs that React munged -function renameCamelCase(html,elements,attrs) { +function renameCamelCase(html, elements, attrs) { let htmlBuffer = html - for( const attr of Object.values(attrs) ) { - if( attr.ident ) { + for (const attr of Object.values(attrs)) { + if (attr.ident) { htmlBuffer = renameAttrs(htmlBuffer, attr.ident.toLowerCase(), attr.ident) } } - for( const el of Object.values(elements) ) { - if( el.name ) { + for (const el of Object.values(elements)) { + if (el.name) { htmlBuffer = renameEls(htmlBuffer, el.name.toLowerCase(), el.name) } } @@ -258,18 +310,18 @@ function renameCamelCase(html,elements,attrs) { } function renameAttrs(htmlFragment, oldAttrName, newAttrName) { - const regex = new RegExp(`${oldAttrName}=`,'g') - return htmlFragment.replace(regex,`${newAttrName}=`) + const regex = new RegExp(`${oldAttrName}=`, 'g') + return htmlFragment.replace(regex, `${newAttrName}=`) } function renameEls(htmlFragment, oldElName, newElName) { // capture any attributes - const regexOpen = new RegExp(`<${oldElName}(\\s+[^>]*)*>`,'g') - const regexClose = new RegExp(``,'g') - return htmlFragment.replace(regexOpen,(match,attrs) => `<${newElName}${attrs ? attrs : ''}>`).replace(regexClose,``) + const regexOpen = new RegExp(`<${oldElName}(\\s+[^>]*)*>`, 'g') + const regexClose = new RegExp(``, 'g') + return htmlFragment.replace(regexOpen, (match, attrs) => `<${newElName}${attrs ? attrs : ''}>`).replace(regexClose, ``) } // copy all the attributes from one element to another function cloneAttributes(target, source) { - [...source.attributes].forEach( attr => { target.setAttribute(attr.nodeName ,attr.nodeValue) }) + [...source.attributes].forEach(attr => { target.setAttribute(attr.nodeName, attr.nodeValue) }) } \ No newline at end of file From ca6aadfdde1309f4d944171ecdde95d14a609898 Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Wed, 18 Dec 2024 11:26:21 -0500 Subject: [PATCH 2/6] WIP --- .DS_Store | Bin 6148 -> 6148 bytes package-lock.json | 458 +++++---- src/data/test-milestone.js | 6 +- .../main-window/tei-editor/TEIEditor.jsx | 894 ++++++++++-------- src/render/model/TEIDocument.js | 163 ++-- src/render/model/TEISchema.js | 129 +-- src/render/model/xml.js | 142 ++- 7 files changed, 954 insertions(+), 838 deletions(-) diff --git a/.DS_Store b/.DS_Store index cdc8eda77d0d917ed493938a3994ae7aca486ea1..a67cca33446cc293bafd4a1efd03645a8596d3a6 100644 GIT binary patch delta 355 zcmZoMXfc=|#>B)qF;R?_gMop8V`8F+G!u{k0?a_n$iN`Lz>tzuUR;orlb^IPaV29t zNSu`+iJ_DslOYEo2Nd0rRFIQdTw-8wjgg6&g_Vt+gPnt$BQ`iAzdX1kv81%vDX}OT z#0$yK&q;!@6O+O+Q_JH8M4a>UN)j{kQj5SEGE-84N@Bt@^HTE5o$^cbQi{QP>w_UO z9Go1S@d6Un)y5VEItnJnmbE$x)s{wJwpnd0Cx@uAzI9N1c1~_yeizVjK)?v}4jAx4 zX&BWFWFUDdr8qe!3Ft(SawOrh;G(>oe73xFpfKac#2+k^4MbQra|68y48o0x-!f=^+xp@{V60Ff=s0Pn_DumAu6 delta 111 zcmZoMXfc=|#>AjHu~68Ak%57MnIWH{l%XW0I5{UNKR*W~1O`k{8YBS30t`_3&4SF| pST-v#Z(`ca&cV+CGzlp3oq009h$1IQZ30N&WD_3c%`qY?m;r^h6wd$v diff --git a/package-lock.json b/package-lock.json index 179d5ba5..ff970314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -479,9 +479,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } @@ -4089,82 +4089,30 @@ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -4172,11 +4120,12 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -4199,7 +4148,8 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -6852,9 +6802,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==" }, "node_modules/@vercel/webpack-asset-relocator-loader": { "version": "1.7.3", @@ -9376,11 +9326,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.24.2" }, "funding": { "type": "opencollective", @@ -9429,9 +9379,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -10032,11 +9982,11 @@ "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -11506,60 +11456,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -11847,21 +11743,6 @@ "eslint": "^7.5.0 || ^8.0.0" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -11972,47 +11853,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -12026,9 +11866,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dependencies": { "estraverse": "^5.1.0" }, @@ -12406,17 +12246,6 @@ "pend": "~1.2.0" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -12696,23 +12525,10 @@ "flat": "cli.js" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==" }, "node_modules/flexsearch": { "version": "0.7.43", @@ -13971,9 +13787,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } @@ -17747,9 +17563,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -21020,6 +20836,36 @@ } } }, + "node_modules/react-scripts/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/react-scripts/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/react-scripts/node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -21046,6 +20892,103 @@ "node": ">=10" } }, + "node_modules/react-scripts/node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/react-scripts/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/react-scripts/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/react-scripts/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/react-scripts/node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -21062,6 +21005,33 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/react-scripts/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/react-scripts/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-scripts/node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -21087,6 +21057,17 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/react-scripts/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-scroll": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.8.3.tgz", @@ -22106,9 +22087,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -22167,11 +22148,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -24028,9 +24004,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/src/data/test-milestone.js b/src/data/test-milestone.js index b606d3e5..1a026f47 100644 --- a/src/data/test-milestone.js +++ b/src/data/test-milestone.js @@ -1,8 +1,8 @@ export const data = [ { - id: '83cd79d8-73fb-4217-b28a-f2b43f863b31', - path: '/TEI[1]/text[1]/front[1]/titlePage[1]/docAuthor[1]::60 /TEI[1]/text[1]/front[1]/titlePage[1]/docAuthor[1]::74', - author: 'Admin', + id: "83cd79d8-73fb-4217-b28a-f2b43f863b31", + path: "//text[@xml:id='text-1']/body[1]/div[1]/div[1]/lg[2]/l[1]::0 //text[@xml:id='text-1']/body[1]/div[1]/div[1]/lg[3]/l[4]::42", // My English Letter + author: "Admin", annotation: "Person" } ] diff --git a/src/render/components/main-window/tei-editor/TEIEditor.jsx b/src/render/components/main-window/tei-editor/TEIEditor.jsx index 7acd88ee..4b3fa1a9 100644 --- a/src/render/components/main-window/tei-editor/TEIEditor.jsx +++ b/src/render/components/main-window/tei-editor/TEIEditor.jsx @@ -1,424 +1,512 @@ -import React, { Component } from 'react' -import {EditorView} from "prosemirror-view" +import React, { Component } from "react"; +import { EditorView } from "prosemirror-view"; import { debounce } from "debounce"; -import { HotKeys } from 'react-hotkeys' - -// import applyDevTools from "prosemirror-dev-tools"; - -import ProseMirrorComponent from "../../common/ProseMirrorComponent" -import EditorGutter from "./EditorGutter" -import StructurePalette from "./StructurePalette" -import ParameterDrawer from './ParameterDrawer' -import EditorToolbar from './EditorToolbar' -import ThumbnailMargin from './ThumbnailMargin' -import ReadOnlyToolbar from './ReadOnlyToolbar' -import TitleBar from '../TitleBar' -import NotePopup from './NotePopup' -import { transformPastedHTMLHandler,transformPastedHandler, createClipboardSerializer } from "../../../model/cut-and-paste" -import { navigateFromTreeToEditor, getSelectedElements, broadcastZoneLinks, navigateFromEditorToTree, getEditorCommands, arrowNavToNote } from '../../../model/editor-navigation' -import { findNoteNode } from '../../../model/xml' -import { canConfigAdmin } from '../../../model/permissions' -import { getConfigStatus } from '../../../model/faircopy-config' -import { getHotKeyConfig } from "../../../model/editor-keybindings" - -const resizeRefreshRate = 100 +import { HotKeys } from "react-hotkeys"; + +import applyDevTools from "prosemirror-dev-tools"; + +import ProseMirrorComponent from "../../common/ProseMirrorComponent"; +import EditorGutter from "./EditorGutter"; +import StructurePalette from "./StructurePalette"; +import ParameterDrawer from "./ParameterDrawer"; +import EditorToolbar from "./EditorToolbar"; +import ThumbnailMargin from "./ThumbnailMargin"; +import ReadOnlyToolbar from "./ReadOnlyToolbar"; +import TitleBar from "../TitleBar"; +import NotePopup from "./NotePopup"; +import { + transformPastedHTMLHandler, + transformPastedHandler, + createClipboardSerializer, +} from "../../../model/cut-and-paste"; +import { + navigateFromTreeToEditor, + getSelectedElements, + broadcastZoneLinks, + navigateFromEditorToTree, + getEditorCommands, + arrowNavToNote, +} from "../../../model/editor-navigation"; +import { findNoteNode } from "../../../model/xml"; +import { canConfigAdmin } from "../../../model/permissions"; +import { getConfigStatus } from "../../../model/faircopy-config"; +import { getHotKeyConfig } from "../../../model/editor-keybindings"; + +const resizeRefreshRate = 100; export default class TEIEditor extends Component { - - constructor() { - super() - this.state = { - noteID: null, - notePopupAnchorEl: null, - elementMenuOptions: null, - paletteWindowOpen: false, - drawerPinned: false, - currentSubmenuID: 0 - } - this.drawerRef = null - } - - componentDidMount() { - this.resizeListener = debounce(this.onResize,resizeRefreshRate) - window.addEventListener("resize", this.resizeListener ) - window.onbeforeunload = this.onBeforeUnload - // bit of a hack to make the gutter settle into the right shape on initial load - setTimeout( this.onResize, 1000 ) + constructor() { + super(); + this.state = { + noteID: null, + notePopupAnchorEl: null, + elementMenuOptions: null, + paletteWindowOpen: false, + drawerPinned: false, + currentSubmenuID: 0, + }; + this.drawerRef = null; + } + + componentDidMount() { + this.resizeListener = debounce(this.onResize, resizeRefreshRate); + window.addEventListener("resize", this.resizeListener); + window.onbeforeunload = this.onBeforeUnload; + // bit of a hack to make the gutter settle into the right shape on initial load + setTimeout(this.onResize, 1000); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.resizeListener); + } + + onResize = () => { + const { teiDocument } = this.props; + teiDocument.gutterMarkCacheDirty = true; + teiDocument.refreshView(); + }; + + onBeforeUnload = (e) => { + const { teiDocument } = this.props; + const { changedSinceLastSave } = teiDocument; + const { exitAnyway } = this.state; + + if (!exitAnyway && changedSinceLastSave) { + // TODO send callback + // this.setState({ ...this.state, alertDialogMode: 'close'}) + e.returnValue = false; } + }; - componentWillUnmount() { - window.removeEventListener("resize", this.resizeListener ) - } + // Since the editor isn't a react component, some hotkeys are handled by editor directly + onEditorKeyDown = (editorView, e) => { + const { teiDocument } = this.props; + const selection = editorView ? editorView.state.selection : null; + const key = e.key; - onResize = () => { - const { teiDocument } = this.props - teiDocument.gutterMarkCacheDirty = true - teiDocument.refreshView() + if (key === "ArrowLeft") { + arrowNavToNote(this.openNotePopup, teiDocument, -1); + return; } - onBeforeUnload = (e) => { - const { teiDocument } = this.props - const { changedSinceLastSave } = teiDocument - const { exitAnyway } = this.state - - if( !exitAnyway && changedSinceLastSave ) { - // TODO send callback - // this.setState({ ...this.state, alertDialogMode: 'close'}) - e.returnValue = false - } - } - - // Since the editor isn't a react component, some hotkeys are handled by editor directly - onEditorKeyDown = (editorView,e) => { - const { teiDocument } = this.props - const selection = (editorView) ? editorView.state.selection : null - const key = e.key - - if( key === 'ArrowLeft' ) { - arrowNavToNote( this.openNotePopup, teiDocument, -1 ) - return - } - - if( key === 'ArrowRight') { - arrowNavToNote( this.openNotePopup, teiDocument, 1 ) - return - } - - if( selection && selection.node ) { - return (key === "Backspace" || key === "Delete") ? false : true - } + if (key === "ArrowRight") { + arrowNavToNote(this.openNotePopup, teiDocument, 1); + return; } - createEditorView = (element) => { - const { teiDocument } = this.props - const { teiSchema } = teiDocument.fairCopyProject - - if( teiDocument.editorView ) return; - this.clipboardSerializer = createClipboardSerializer(teiSchema,teiDocument) - - const editorView = new EditorView( - element, - { - dispatchTransaction: this.dispatchTransaction, - state: teiDocument.initialState, - handleClickOn: this.onClickOn, - handleKeyDown: this.onEditorKeyDown, - transformPastedHTML: transformPastedHTMLHandler(teiSchema,teiDocument), - transformPasted: transformPastedHandler(teiSchema,teiDocument), - editable: () => { return teiDocument.isEditable() }, - clipboardSerializer: this.clipboardSerializer - } - ) - // uncomment to use ProseMirror dev tools - // if( process.env['NODE_ENV'] === 'development' ) applyDevTools(editorView) - teiDocument.finalizeEditorView(editorView) + if (selection && selection.node) { + return key === "Backspace" || key === "Delete" ? false : true; } - - dispatchTransaction = (transaction) => { - const { noteID } = this.state - const { teiDocument, onAlertMessage, onErrorCountChange, onResetSearch } = this.props - const { editorView } = teiDocument - - if( editorView ) { - // display error messages generated by this transaction - const alertMessage = transaction.getMeta('alertMessage') - if( alertMessage ) { - onAlertMessage(alertMessage) - } - - // clear search if doc changed - if( transaction.docChanged ) { - transaction.setMeta('searchResults', -1) - transaction.setMeta('searchQuery', null) - transaction.setMeta('selectionIndex', 0) - onResetSearch() - } - - // update document state - teiDocument.onUpdate(transaction) - - // get next selection state - const selectedElements = getSelectedElements(teiDocument,noteID) - - // opening parameter drawer - if( teiDocument.selectedElements.length === 0 && selectedElements.length > 0 ) { - setTimeout( () => { - const { tr } = editorView.state - tr.scrollIntoView() - editorView.dispatch(tr) - }, 100 ) - } - - // closing parameter drawer, update system flags - if( teiDocument.selectedElements.length > 0 && selectedElements.length === 0 ) { - // TODO can I debounce this? - setTimeout( () => { - const { tr } = editorView.state - teiDocument.updateSystemFlags(tr,onErrorCountChange) - editorView.dispatch(tr) - }, 100 ) - } - - // update selection state - teiDocument.selectedElements = selectedElements - - // maintain note position - const nextNotePopupAnchorEl = this.maintainNoteAnchor() - const nextNoteID = nextNotePopupAnchorEl ? noteID : null - - broadcastZoneLinks(teiDocument) - this.setState({...this.state, noteID: nextNoteID, notePopupAnchorEl: nextNotePopupAnchorEl }) - } + }; + + createEditorView = (element) => { + const { teiDocument } = this.props; + const { teiSchema } = teiDocument.fairCopyProject; + + if (teiDocument.editorView) return; + this.clipboardSerializer = createClipboardSerializer( + teiSchema, + teiDocument + ); + + const editorView = new EditorView(element, { + dispatchTransaction: this.dispatchTransaction, + state: teiDocument.initialState, + handleClickOn: this.onClickOn, + handleKeyDown: this.onEditorKeyDown, + transformPastedHTML: transformPastedHTMLHandler(teiSchema, teiDocument), + transformPasted: transformPastedHandler(teiSchema, teiDocument), + editable: () => { + return teiDocument.isEditable(); + }, + clipboardSerializer: this.clipboardSerializer, + }); + // uncomment to use ProseMirror dev tools + if (process.env["NODE_ENV"] === "development") applyDevTools(editorView); + teiDocument.finalizeEditorView(editorView); + }; + + dispatchTransaction = (transaction) => { + const { noteID } = this.state; + const { teiDocument, onAlertMessage, onErrorCountChange, onResetSearch } = + this.props; + const { editorView } = teiDocument; + + if (editorView) { + // display error messages generated by this transaction + const alertMessage = transaction.getMeta("alertMessage"); + if (alertMessage) { + onAlertMessage(alertMessage); + } + + // clear search if doc changed + if (transaction.docChanged) { + transaction.setMeta("searchResults", -1); + transaction.setMeta("searchQuery", null); + transaction.setMeta("selectionIndex", 0); + onResetSearch(); + } + + // update document state + teiDocument.onUpdate(transaction); + + // get next selection state + const selectedElements = getSelectedElements(teiDocument, noteID); + + // opening parameter drawer + if ( + teiDocument.selectedElements.length === 0 && + selectedElements.length > 0 + ) { + setTimeout(() => { + const { tr } = editorView.state; + tr.scrollIntoView(); + editorView.dispatch(tr); + }, 100); + } + + // closing parameter drawer, update system flags + if ( + teiDocument.selectedElements.length > 0 && + selectedElements.length === 0 + ) { + // TODO can I debounce this? + setTimeout(() => { + const { tr } = editorView.state; + teiDocument.updateSystemFlags(tr, onErrorCountChange); + editorView.dispatch(tr); + }, 100); + } + + // update selection state + teiDocument.selectedElements = selectedElements; + + // maintain note position + const nextNotePopupAnchorEl = this.maintainNoteAnchor(); + const nextNoteID = nextNotePopupAnchorEl ? noteID : null; + + broadcastZoneLinks(teiDocument); + this.setState({ + ...this.state, + noteID: nextNoteID, + notePopupAnchorEl: nextNotePopupAnchorEl, + }); } - - // editing a note's attributes changes the DOM, invalidating notePopupAnchorEl - // this function keeps it attached to the right part of the DOM - maintainNoteAnchor() { - const {teiDocument} = this.props - const {editorView} = teiDocument - const {noteID, notePopupAnchorEl} = this.state - if( notePopupAnchorEl ) { - const { doc } = editorView.state - const { notePos } = findNoteNode( doc, noteID ) - if( notePos !== null ) { - const domPos = editorView.domAtPos(notePos) - return domPos.node.childNodes[domPos.offset] - } else { - // anchor element was probably deleted - return null - } - } - return notePopupAnchorEl + }; + + // editing a note's attributes changes the DOM, invalidating notePopupAnchorEl + // this function keeps it attached to the right part of the DOM + maintainNoteAnchor() { + const { teiDocument } = this.props; + const { editorView } = teiDocument; + const { noteID, notePopupAnchorEl } = this.state; + if (notePopupAnchorEl) { + const { doc } = editorView.state; + const { notePos } = findNoteNode(doc, noteID); + if (notePos !== null) { + const domPos = editorView.domAtPos(notePos); + return domPos.node.childNodes[domPos.offset]; + } else { + // anchor element was probably deleted + return null; + } } - - onClickOn = ( editorView, pos, node, nodePos, event, direct ) => { - if( !direct ) return - - const { teiDocument } = this.props - const { asides } = teiDocument.fairCopyProject.teiSchema.elementGroups - const { currentTreeNode } = teiDocument - - if( asides.includes(node.type.name) ) { - const { noteID } = this.state - const nextID = node.attrs['__id__'] - if( noteID !== nextID ) { - if( noteID !== null ) { - this.closeNotePopup() - } else { - this.openNotePopup(nextID, event.target) - } - } + return notePopupAnchorEl; + } + + onClickOn = (editorView, pos, node, nodePos, event, direct) => { + if (!direct) return; + + const { teiDocument } = this.props; + const { asides } = teiDocument.fairCopyProject.teiSchema.elementGroups; + const { currentTreeNode } = teiDocument; + + if (asides.includes(node.type.name)) { + const { noteID } = this.state; + const nextID = node.attrs["__id__"]; + if (noteID !== nextID) { + if (noteID !== null) { + this.closeNotePopup(); } else { - this.closeNotePopup() - } - - // if the document is read only, clicking on the body deselects the editor gutter - if( !teiDocument.isEditable() && currentTreeNode.editorGutterPos !== null ) { - teiDocument.currentTreeNode = { editorGutterPos: null, editorGutterPath: null, treeID: "main" } - teiDocument.refreshView() - } - } - - onTogglePalette = () => { - const { paletteWindowOpen } = this.state - this.setState({...this.state, paletteWindowOpen: !paletteWindowOpen}) - } - - onDrawerPinToggle = () => { - const { drawerPinned } = this.state - this.setState({...this.state, drawerPinned: !drawerPinned}) - } - - openNotePopup = (noteID, notePopupAnchorEl) => { - this.setState({...this.state, noteID, notePopupAnchorEl }) - } - - closeNotePopup = () => { - const { noteID, notePopupAnchorEl } = this.state - if( noteID !== null || notePopupAnchorEl !== null ) { - this.setState({...this.state, noteID: null, notePopupAnchorEl: null }) + this.openNotePopup(nextID, event.target); } + } + } else { + this.closeNotePopup(); } - getMainEditorHotKeyConfig() { - const { teiDocument, onToggleSearchBar } = this.props - - // get the base hotkey config - const { keyMap, handlers } = getHotKeyConfig( teiDocument, getEditorCommands( teiDocument, this.onTogglePalette, this.onOpenElementMenu, this.clipboardSerializer, onToggleSearchBar ) ) - - keyMap.hopToTree = 'shift+tab' - handlers.hopToTree = () => { - const { editorView } = teiDocument - const { treeID } = teiDocument.currentTreeNode - const { editorGutterPos } = teiDocument.currentTreeNode - - if( editorGutterPos === null ) { - const { nextPos, nextPath } = navigateFromEditorToTree( editorView ) - this.onChangePos(nextPos, nextPath, treeID) - } - } - - return { keyMap, handlers } - } - - onOpenElementMenu = (elementMenuOptions ) => { - this.setState({...this.state, elementMenuOptions }) - } - - onCloseElementMenu = () => { - this.setState({...this.state, elementMenuOptions: null }) + // if the document is read only, clicking on the body deselects the editor gutter + if (!teiDocument.isEditable() && currentTreeNode.editorGutterPos !== null) { + teiDocument.currentTreeNode = { + editorGutterPos: null, + editorGutterPath: null, + treeID: "main", + }; + teiDocument.refreshView(); } - - onChangePos = (editorGutterPos, editorGutterPath, treeID ) => { - const { noteID } = this.state - const { teiDocument } = this.props - const editorView = teiDocument.getActiveView() - const { tr } = editorView.state - if( treeID === "main" && noteID ) { - // close the note window - this.closeNotePopup() - } else { - teiDocument.currentTreeNode = { editorGutterPos, editorGutterPath, treeID } - tr.setMeta( 'highlightEnabled', editorGutterPos === null ) - editorView.dispatch(tr) - } + }; + + onTogglePalette = () => { + const { paletteWindowOpen } = this.state; + this.setState({ ...this.state, paletteWindowOpen: !paletteWindowOpen }); + }; + + onDrawerPinToggle = () => { + const { drawerPinned } = this.state; + this.setState({ ...this.state, drawerPinned: !drawerPinned }); + }; + + openNotePopup = (noteID, notePopupAnchorEl) => { + this.setState({ ...this.state, noteID, notePopupAnchorEl }); + }; + + closeNotePopup = () => { + const { noteID, notePopupAnchorEl } = this.state; + if (noteID !== null || notePopupAnchorEl !== null) { + this.setState({ ...this.state, noteID: null, notePopupAnchorEl: null }); } - - render() { - const { teiDocument, hidden, onSave, onDragElement, onAlertMessage, onToggleSearchBar, onEditResource, onProjectSettings, onResourceAction, leftPaneWidth, currentView } = this.props - const { noteID, notePopupAnchorEl, elementMenuOptions, currentSubmenuID, drawerPinned, paletteWindowOpen } = this.state - const { fairCopyProject, parentEntry, resourceEntry } = teiDocument - const { isLoggedIn, configLastAction, userID, permissions, remote } = fairCopyProject - const readOnly = !teiDocument.isEditable() - - const onClickBody = () => { - const { editorView, currentTreeNode } = teiDocument - if( editorView && !editorView.hasFocus() && currentTreeNode.editorGutterPos === null ) { - editorView.focus() - } - } - - const onFocus = () => { - const { currentTreeNode, editorView } = teiDocument - const { editorGutterPos } = currentTreeNode - if( editorGutterPos !== null ) { - teiDocument.currentTreeNode = { editorGutterPos: null, editorGutterPath: null, treeID: "main" } - navigateFromTreeToEditor( editorView, editorGutterPos ) - } - } - - const onJumpToDrawer = () => { - if( this.drawerRef ) { - this.drawerRef.focus() - } - } - - const { selectedElements } = teiDocument - const drawerHeight = drawerPinned || selectedElements.length > 0 ? 300 : 50 - const drawerWidthCSS = `calc(100vw - 30px - ${leftPaneWidth}px)` - const editorHeight = drawerPinned || selectedElements.length > 0 ? 530 : 180 - - const editorHeightCSS = `calc(100% - ${editorHeight}px)` - const editorWidthCSS = `calc(100vw - 10px - ${leftPaneWidth}px)` - const editorStyle = { minWidth: editorWidthCSS, maxHeight: editorHeightCSS } - const style = hidden ? { display: 'none' } : {} - - const canConfig = canConfigAdmin(permissions) - const lockStatus = getConfigStatus( configLastAction, userID ) - const canEditConfig = !remote || (canConfig && lockStatus === 'checked_out') - - const { keyMap, handlers } = this.getMainEditorHotKeyConfig() - - return ( -
- -
- { !hidden && - - } - { !hidden && readOnly ? - : - - } -
- { !hidden && } - - { !hidden && } -
- { !hidden && { this.drawerRef = el}} - drawerPinned={drawerPinned} - onDrawerPinToggle={this.onDrawerPinToggle} - noteID={noteID} - height={drawerHeight} - width={drawerWidthCSS} - readOnly={readOnly} - canEditConfig={canEditConfig} - /> } -
-
- { !hidden && } - { paletteWindowOpen && !readOnly && { this.setState( {...this.state, currentSubmenuID} )}} - onClose={()=>{ this.setState( {...this.state, paletteWindowOpen: false} )}} - > - } -
- ) + }; + + getMainEditorHotKeyConfig() { + const { teiDocument, onToggleSearchBar } = this.props; + + // get the base hotkey config + const { keyMap, handlers } = getHotKeyConfig( + teiDocument, + getEditorCommands( + teiDocument, + this.onTogglePalette, + this.onOpenElementMenu, + this.clipboardSerializer, + onToggleSearchBar + ) + ); + + keyMap.hopToTree = "shift+tab"; + handlers.hopToTree = () => { + const { editorView } = teiDocument; + const { treeID } = teiDocument.currentTreeNode; + const { editorGutterPos } = teiDocument.currentTreeNode; + + if (editorGutterPos === null) { + const { nextPos, nextPath } = navigateFromEditorToTree(editorView); + this.onChangePos(nextPos, nextPath, treeID); + } + }; + + return { keyMap, handlers }; + } + + onOpenElementMenu = (elementMenuOptions) => { + this.setState({ ...this.state, elementMenuOptions }); + }; + + onCloseElementMenu = () => { + this.setState({ ...this.state, elementMenuOptions: null }); + }; + + onChangePos = (editorGutterPos, editorGutterPath, treeID) => { + const { noteID } = this.state; + const { teiDocument } = this.props; + const editorView = teiDocument.getActiveView(); + const { tr } = editorView.state; + if (treeID === "main" && noteID) { + // close the note window + this.closeNotePopup(); + } else { + teiDocument.currentTreeNode = { + editorGutterPos, + editorGutterPath, + treeID, + }; + tr.setMeta("highlightEnabled", editorGutterPos === null); + editorView.dispatch(tr); } + }; + + render() { + const { + teiDocument, + hidden, + onSave, + onDragElement, + onAlertMessage, + onToggleSearchBar, + onEditResource, + onProjectSettings, + onResourceAction, + leftPaneWidth, + currentView, + } = this.props; + const { + noteID, + notePopupAnchorEl, + elementMenuOptions, + currentSubmenuID, + drawerPinned, + paletteWindowOpen, + } = this.state; + const { fairCopyProject, parentEntry, resourceEntry } = teiDocument; + const { isLoggedIn, configLastAction, userID, permissions, remote } = + fairCopyProject; + const readOnly = !teiDocument.isEditable(); + + const onClickBody = () => { + const { editorView, currentTreeNode } = teiDocument; + if ( + editorView && + !editorView.hasFocus() && + currentTreeNode.editorGutterPos === null + ) { + editorView.focus(); + } + }; + + const onFocus = () => { + const { currentTreeNode, editorView } = teiDocument; + const { editorGutterPos } = currentTreeNode; + if (editorGutterPos !== null) { + teiDocument.currentTreeNode = { + editorGutterPos: null, + editorGutterPath: null, + treeID: "main", + }; + navigateFromTreeToEditor(editorView, editorGutterPos); + } + }; + + const onJumpToDrawer = () => { + if (this.drawerRef) { + this.drawerRef.focus(); + } + }; + + const { selectedElements } = teiDocument; + const drawerHeight = drawerPinned || selectedElements.length > 0 ? 300 : 50; + const drawerWidthCSS = `calc(100vw - 30px - ${leftPaneWidth}px)`; + const editorHeight = + drawerPinned || selectedElements.length > 0 ? 530 : 180; + + const editorHeightCSS = `calc(100% - ${editorHeight}px)`; + const editorWidthCSS = `calc(100vw - 10px - ${leftPaneWidth}px)`; + const editorStyle = { + minWidth: editorWidthCSS, + maxHeight: editorHeightCSS, + }; + const style = hidden ? { display: "none" } : {}; + + const canConfig = canConfigAdmin(permissions); + const lockStatus = getConfigStatus(configLastAction, userID); + const canEditConfig = + !remote || (canConfig && lockStatus === "checked_out"); + + const { keyMap, handlers } = this.getMainEditorHotKeyConfig(); + + return ( +
+ +
+ {!hidden && ( + + )} + {!hidden && readOnly ? ( + + ) : ( + + )} +
+ {!hidden && ( + + )} + + {!hidden && } +
+ {!hidden && ( + { + this.drawerRef = el; + }} + drawerPinned={drawerPinned} + onDrawerPinToggle={this.onDrawerPinToggle} + noteID={noteID} + height={drawerHeight} + width={drawerWidthCSS} + readOnly={readOnly} + canEditConfig={canEditConfig} + /> + )} +
+
+ {!hidden && ( + + )} + {paletteWindowOpen && !readOnly && ( + { + this.setState({ ...this.state, currentSubmenuID }); + }} + onClose={() => { + this.setState({ ...this.state, paletteWindowOpen: false }); + }} + > + )} +
+ ); + } } diff --git a/src/render/model/TEIDocument.js b/src/render/model/TEIDocument.js index 77e4ac5f..f744fbe5 100644 --- a/src/render/model/TEIDocument.js +++ b/src/render/model/TEIDocument.js @@ -1,17 +1,17 @@ -import {EditorState, TextSelection } from "prosemirror-state" -import {keymap} from "prosemirror-keymap" -import {history} from "prosemirror-history" -import {baseKeymap} from "./basekeymap" -import {highlighter} from "./highlighter" -import {searchHighlighter} from "./search-highlighter" -import {dropCursor} from "prosemirror-dropcursor" -import {gapCursor} from "prosemirror-gapcursor" +import { EditorState, TextSelection } from "prosemirror-state" +import { keymap } from "prosemirror-keymap" +import { history } from "prosemirror-history" +import { baseKeymap } from "./basekeymap" +import { highlighter } from "./highlighter" +import { searchHighlighter } from "./search-highlighter" +import { dropCursor } from "prosemirror-dropcursor" +import { gapCursor } from "prosemirror-gapcursor" import { v4 as uuidv4 } from 'uuid' -import {teiHeaderTemplate, teiTextTemplate, teiStandOffTemplate, teiSourceDocTemplate } from "./tei-template" -import {parseText, proseMirrorToDOM, serializeText, addTextNodes} from "./xml" -import {applySystemFlags} from "./system-flags" -import {mapResource} from "./id-map" +import { teiHeaderTemplate, teiTextTemplate, teiStandOffTemplate, teiSourceDocTemplate } from "./tei-template" +import { parseText, proseMirrorToDOM, serializeText, addTextNodes, addAnnotations } from "./xml" +import { applySystemFlags } from "./system-flags" +import { mapResource } from "./id-map" import { generateGutterMarks } from './gutter-marks' const fairCopy = window.fairCopy @@ -21,7 +21,7 @@ const gutterTopReadOnly = 115 export default class TEIDocument { - constructor( resourceEntry, parentEntry, fairCopyProject, teiSchema=null ) { + constructor(resourceEntry, parentEntry, fairCopyProject, teiSchema = null) { this.subDocs = {} this.subDocCounter = 0 this.errors = [] @@ -49,30 +49,30 @@ export default class TEIDocument { this.changedSinceLastSave = false } - onResourceUpdated = ( eventType, resource ) => { - if( eventType === 'resourceEntryUpdated' ) { + onResourceUpdated = (eventType, resource) => { + if (eventType === 'resourceEntryUpdated') { const { id: targetResourceID } = resource - if( this.resourceID === targetResourceID ) { + if (this.resourceID === targetResourceID) { this.resourceEntry = resource - this.resourceID = this.resourceEntry.id - this.resourceType = this.resourceEntry.type + this.resourceID = this.resourceEntry.id + this.resourceType = this.resourceEntry.type } - if( this.parentEntry && this.parentEntry.id === targetResourceID ) { + if (this.parentEntry && this.parentEntry.id === targetResourceID) { this.parentEntry = resource } } // TODO right now this only works for FacsDocument, make it work here too // else if( eventType === 'resourceContentUpdated' ) { - // load updated content if we are in read only mode - // if( !this.isEditable() ) { - // const { resourceContent } = resource - // this.load(resourceContent) - // } + // load updated content if we are in read only mode + // if( !this.isEditable() ) { + // const { resourceContent } = resource + // this.load(resourceContent) + // } // } } isEditable() { - return this.fairCopyProject.isEditable( this.resourceEntry ) + return this.fairCopyProject.isEditable(this.resourceEntry) } getTEISchema() { @@ -83,10 +83,10 @@ export default class TEIDocument { // load blank XML template const parser = new DOMParser(); const teiTemplate = this.resourceType === 'text' ? teiTextTemplate : this.resourceType === 'standOff' ? teiStandOffTemplate : this.resourceType === 'sourceDoc' ? teiSourceDocTemplate : teiHeaderTemplate('') - this.xmlDom = parser.parseFromString(teiTemplate, "text/xml"); + this.xmlDom = parser.parseFromString(teiTemplate, "text/xml"); const doc = this.createEmptyDocument(document) - - return EditorState.create({ + + return EditorState.create({ doc, plugins: this.plugins }) } @@ -102,73 +102,73 @@ export default class TEIDocument { noteX.innerHTML = node.innerHTML let noteDoc = document.createElement(`${name}Doc`) noteDoc.append(noteX) - const subDoc = parseText(noteDoc,this,teiSchema,name) + const subDoc = parseText(noteDoc, this, teiSchema, name) this.subDocs[noteID] = JSON.stringify(subDoc.toJSON()); } - serializeSubDocument( subDocID, name, attrs) { + serializeSubDocument(subDocID, name, attrs) { const teiSchema = this.getTEISchema() - const noteJSON = JSON.parse( this.subDocs[subDocID] ) + const noteJSON = JSON.parse(this.subDocs[subDocID]) const subDoc = teiSchema.docNodeSchemas[name].nodeFromJSON(noteJSON); // get the content of noteDoc, which is the only child of doc const noteDocFragment = subDoc.firstChild.content const domFragment = proseMirrorToDOM(noteDocFragment, this, teiSchema, name) let note = document.createElement(name) - note.appendChild( domFragment.cloneNode(true) ) - for( const attrKey of Object.keys(attrs)) { + note.appendChild(domFragment.cloneNode(true)) + for (const attrKey of Object.keys(attrs)) { const attrVal = attrs[attrKey] - note.setAttribute(attrKey,attrVal) + note.setAttribute(attrKey, attrVal) } return note } - hasID = (targetID) => { + hasID = (targetID) => { const localID = this.parentEntry ? this.parentEntry.localID : this.resourceEntry.localID - return this.fairCopyProject.hasID(targetID, localID) + return this.fairCopyProject.hasID(targetID, localID) } // called by dispatch transaction for every change to doc state onUpdate(transaction) { const { idMap } = this.fairCopyProject - if( this.isEditable() ) { - const resourceMap = mapResource( this.resourceEntry, transaction.doc ) + if (this.isEditable()) { + const resourceMap = mapResource(this.resourceEntry, transaction.doc) // update the ID Map - idMap.setResourceMap(resourceMap,this.resourceEntry.localID, this.parentEntry?.localID) + idMap.setResourceMap(resourceMap, this.resourceEntry.localID, this.parentEntry?.localID) // note unsaved state this.changedSinceLastSave = this.changedSinceLastSave || transaction.docChanged } - + // update editor state const nextEditorState = this.editorView.state.apply(transaction) this.editorView.updateState(nextEditorState) } setExpandedGutter(expanded) { - if( this.expandedGutter !== expanded ) this.gutterMarkCacheDirty = true + if (this.expandedGutter !== expanded) this.gutterMarkCacheDirty = true this.expandedGutter = expanded } getNoteGutterMarks(gutterTop) { const { docNodes } = this.fairCopyProject.teiSchema.elementGroups - return generateGutterMarks( this.noteEditorView, this.expandedGutter, docNodes, gutterTop ) + return generateGutterMarks(this.noteEditorView, this.expandedGutter, docNodes, gutterTop) } getGutterMarks() { // regenerate gutter marks if the document structure has changed const { docNodes } = this.fairCopyProject.teiSchema.elementGroups const gutterTop = this.isEditable() ? gutterTopEditable : gutterTopReadOnly - return generateGutterMarks( this.editorView, this.expandedGutter, docNodes, gutterTop ) + return generateGutterMarks(this.editorView, this.expandedGutter, docNodes, gutterTop) } - updateSystemFlags(transaction,onErrorCountChange) { + updateSystemFlags(transaction, onErrorCountChange) { const { idMap, teiSchema, fairCopyConfig } = this.fairCopyProject const relativeParentID = this.getRelativeParentID() - const nextErrors = applySystemFlags(teiSchema,idMap,fairCopyConfig,relativeParentID,transaction) - if( this.errors.length !== nextErrors.length ) { + const nextErrors = applySystemFlags(teiSchema, idMap, fairCopyConfig, relativeParentID, transaction) + if (this.errors.length !== nextErrors.length) { this.errors = nextErrors onErrorCountChange() - } + } } getRelativeParentID() { @@ -179,18 +179,19 @@ export default class TEIDocument { this.editorView = editorView const { tr, doc } = editorView.state tr.setSelection(TextSelection.create(doc, 0)) - this.updateSystemFlags(tr,() => {}) + this.updateSystemFlags(tr, () => { }) editorView.dispatch(tr); addTextNodes(editorView.state, editorView.dispatch) editorView.focus() + addAnnotations(editorView.state, editorView.dispatch) this.changedSinceLastSave = false } refreshView = () => { // dispatch a blank transaction to force a display update of the subcomponents - if( this.editorView ) { + if (this.editorView) { const { tr } = this.editorView.state - this.editorView.dispatch(tr) + this.editorView.dispatch(tr) } } @@ -202,8 +203,8 @@ export default class TEIDocument { return doc } - openSubDocument( subDocID ) { - const subDocJSON = JSON.parse( this.subDocs[subDocID] ) + openSubDocument(subDocID) { + const subDocJSON = JSON.parse(this.subDocs[subDocID]) const teiSchema = this.getTEISchema() const doc = teiSchema.schema.nodeFromJSON(subDocJSON); return EditorState.create({ @@ -213,30 +214,30 @@ export default class TEIDocument { }) } - createSubDocument = (documentDOM,docType) => { + createSubDocument = (documentDOM, docType) => { const teiSchema = this.getTEISchema() let noteDoc = documentDOM.createElement(`${docType}Doc`) const docX = documentDOM.createElement(`${docType}X`) - const {defaultNodes} = teiSchema.elements[docType] - if( defaultNodes ) { - for( const defaultNodeName of defaultNodes ) { + const { defaultNodes } = teiSchema.elements[docType] + if (defaultNodes) { + for (const defaultNodeName of defaultNodes) { const defaultNode = documentDOM.createElement(defaultNodeName) docX.append(defaultNode) } } - noteDoc.append(docX) - const subDoc = parseText(noteDoc,this,teiSchema,docType) + noteDoc.append(docX) + const subDoc = parseText(noteDoc, this, teiSchema, docType) const subDocID = this.issueSubDocumentID() this.subDocs[subDocID] = JSON.stringify(subDoc.toJSON()) return subDocID } - getVocab(elementName,attrName) { + getVocab(elementName, attrName) { const { fairCopyConfig } = this.fairCopyProject const vocabID = fairCopyConfig.elements[elementName].attrState[attrName].vocabID - const vocab = ( vocabID && fairCopyConfig.vocabs[vocabID]) ? fairCopyConfig.vocabs[vocabID] : [] - return { vocabID, vocab } + const vocab = (vocabID && fairCopyConfig.vocabs[vocabID]) ? fairCopyConfig.vocabs[vocabID] : [] + return { vocabID, vocab } } getActiveView() { @@ -248,32 +249,32 @@ export default class TEIDocument { return this.noteEditorView ? 'note' : 'main' } - load( text ) { + load(text) { const parser = new DOMParser(); const teiSchema = this.getTEISchema() this.xmlDom = parser.parseFromString(text, "text/xml"); - + let doc - if( this.resourceType === 'header' ) { - const textEl = this.xmlDom.getElementsByTagName('teiHeader')[0] - doc = parseText(textEl,this,teiSchema,'header') - - } else if( this.resourceType === 'text' ) { - const textEl = this.xmlDom.getElementsByTagName('text')[0] - doc = parseText(textEl,this,teiSchema,'text') - } else if( this.resourceType === 'standOff') { - const textEl = this.xmlDom.getElementsByTagName('standOff')[0] - doc = parseText(textEl,this,teiSchema,'standOff') - } else if(this.resourceType === 'sourceDoc') { - const textEl = this.xmlDom.getElementsByTagName('sourceDoc')[0] - doc = parseText(textEl,this,teiSchema,'sourceDoc') + if (this.resourceType === 'header') { + const textEl = this.xmlDom.getElementsByTagName('teiHeader')[0] + doc = parseText(textEl, this, teiSchema, 'header') + + } else if (this.resourceType === 'text') { + const textEl = this.xmlDom.getElementsByTagName('text')[0] + doc = parseText(textEl, this, teiSchema, 'text') + } else if (this.resourceType === 'standOff') { + const textEl = this.xmlDom.getElementsByTagName('standOff')[0] + doc = parseText(textEl, this, teiSchema, 'standOff') + } else if (this.resourceType === 'sourceDoc') { + const textEl = this.xmlDom.getElementsByTagName('sourceDoc')[0] + doc = parseText(textEl, this, teiSchema, 'sourceDoc') } else { throw new Error("Attempted to load unknown document type.") } - + this.changedSinceLastSave = false - this.initialState = EditorState.create({ - doc, plugins: this.plugins + this.initialState = EditorState.create({ + doc, plugins: this.plugins }) this.loading = false } @@ -296,13 +297,13 @@ export default class TEIDocument { save() { const editorState = this.editorView.state const teiSchema = this.getTEISchema() - + const fileContents = serializeText(editorState.doc, this, teiSchema) const messageID = uuidv4() fairCopy.ipcSend('requestSave', messageID, this.resourceID, fileContents) this.lastMessageID = messageID - + this.changedSinceLastSave = false } } \ No newline at end of file diff --git a/src/render/model/TEISchema.js b/src/render/model/TEISchema.js index 1e65c823..53579f7a 100644 --- a/src/render/model/TEISchema.js +++ b/src/render/model/TEISchema.js @@ -1,4 +1,4 @@ -import {Schema, MarkType} from "prosemirror-model" +import { Schema, MarkType } from "prosemirror-model" import { DOMParser as PMDOMParser } from "prosemirror-model" import { createValidationSet } from "./element-validators" import { synthNameToElementName } from "./xml" @@ -6,17 +6,17 @@ import { synthNameToElementName } from "./xml" const pmTypeToMenu = { "node": ['structure'], "mark": ['mark'], - "inline-node": [ 'structure', 'inline' ] + "inline-node": ['structure', 'inline'] } const elementTypeToPmTypes = { 'docNodes': [], - 'embed':[], - 'exclude':[], + 'embed': [], + 'exclude': [], 'hard': ['node'], 'soft': ['node'], 'marks': ['mark'], - 'inter': ['node','mark'], + 'inter': ['node', 'mark'], 'inlines': ['inline-node'], 'asides': ['inline-node'] } @@ -63,7 +63,7 @@ export default class TEISchema { this.schema = new Schema(schemaSpec) this.domParser = PMDOMParser.fromSchema(this.schema) this.createSubDocSchemas(schemaSpec) - this.validationSet = createValidationSet(this.elements,this.schema) + this.validationSet = createValidationSet(this.elements, this.schema) } // Each subdoc type needs its own schema that has the docNode as its top level node and a parser that uses that schema @@ -71,12 +71,12 @@ export default class TEISchema { this.docNodeSchemas = {} this.docNodeParsers = {} const { docNodes } = this.elementGroups - for( const docNode of docNodes ) { - if( docNode !== 'doc' ) { + for (const docNode of docNodes) { + if (docNode !== 'doc') { const docNodeSchemaSpec = { ...schemaSpec, topNode: docNode } - const asideName = docNode.slice(0,-3) // Slice off 'Doc' to get node name + const asideName = docNode.slice(0, -3) // Slice off 'Doc' to get node name this.docNodeSchemas[asideName] = new Schema(docNodeSchemaSpec) - this.docNodeParsers[asideName] = PMDOMParser.fromSchema(this.docNodeSchemas[asideName]) + this.docNodeParsers[asideName] = PMDOMParser.fromSchema(this.docNodeSchemas[asideName]) } } } @@ -96,22 +96,22 @@ export default class TEISchema { inline: true } } - + const marks = {} - for( const element of teiSimple.elements ) { + for (const element of teiSimple.elements) { const { pmType, name, marks: markContent, content, group, isolating, icon } = element const validAttrs = element.validAttrs ? element.validAttrs : [] - if( pmType === 'mark' || pmType === 'node') { + if (pmType === 'mark' || pmType === 'node') { const phraseLvl = (pmType === 'mark') const elSpec = this.createElementSpec({ name, attrs: validAttrs, content, group, markContent, phraseLvl, isolating }) - if( pmType === 'mark' ) { + if (pmType === 'mark') { marks[name] = elSpec } else { nodes[name] = elSpec } - } else if( pmType === 'inline-node' ) { - if( teiSimple.elementGroups.asides.includes(name) ) { + } else if (pmType === 'inline-node') { + if (teiSimple.elementGroups.asides.includes(name)) { nodes[name] = this.createAsideSpec(name, icon, validAttrs, content, group) } else { nodes[name] = this.createAtomSpec(name, icon, validAttrs, group) @@ -119,30 +119,37 @@ export default class TEISchema { } else { console.log('unrecognized pmType') } - elements[name] = element + elements[name] = element + } + + // This is a test of adding the annotations to the document + marks['__annoMark__'] = { + toDOM() { return ["__annoMark__", 0] }, + parseDOM: [{ tag: "__annoMark__" }], + attrs: { '__id__': { hidden: true } } } return { schemaSpec: { nodes, marks }, elements, attrs, elementGroups: teiSimple.elementGroups, modules: teiSimple.modules } } - filterOutBlanks( attrObj ) { + filterOutBlanks(attrObj) { // don't save blank attrs const attrs = {} - for( const key of Object.keys(attrObj) ) { + for (const key of Object.keys(attrObj)) { const value = attrObj[key] - if( value && value.length > 0 ) { + if (value && value.length > 0) { attrs[key] = value } } return attrs } - filterInternal( attrObj ) { + filterInternal(attrObj) { // don't save error flags const attrs = {} - for( const key of Object.keys(attrObj) ) { + for (const key of Object.keys(attrObj)) { const value = attrObj[key] - if( !systemAttributes.includes(key)) { + if (!systemAttributes.includes(key)) { attrs[key] = value } } @@ -162,18 +169,18 @@ export default class TEISchema { { tag: name, getAttrs: this.getAttrParser(elSpec.attrs) - } + } ], toDOM: (el) => { - if( this.teiMode ) { + if (this.teiMode) { let attrs = this.filterOutBlanks(el.attrs) attrs = this.filterInternal(attrs) - return [name,attrs,0] + return [name, attrs, 0] } else { const displayAttrs = { ...el.attrs, phraseLvl } - return [`tei-${name}`,displayAttrs,0] + return [`tei-${name}`, displayAttrs, 0] } - } + } } } @@ -194,26 +201,26 @@ export default class TEISchema { getAttrs: (domNode) => { const attrParser = this.getAttrParser(validAttrs) const existingID = domNode.getAttribute('__id__') - const teiDocument = this.teiDocuments[this.teiDocuments.length-1] + const teiDocument = this.teiDocuments[this.teiDocuments.length - 1] const noteID = existingID ? existingID : teiDocument.issueSubDocumentID() teiDocument.parseSubDocument(domNode, name, noteID) return { ...attrParser(domNode), __id__: noteID } }, } ], - toDOM: (node) => { - if( this.teiMode ) { + toDOM: (node) => { + if (this.teiMode) { let attrs = this.filterOutBlanks(node.attrs) const subDocID = attrs['__id__'] attrs = this.filterInternal(attrs) - const teiDocument = this.teiDocuments[this.teiDocuments.length-1] - return teiDocument.serializeSubDocument( subDocID, name, attrs) + const teiDocument = this.teiDocuments[this.teiDocuments.length - 1] + return teiDocument.serializeSubDocument(subDocID, name, attrs) } else { const noteAttrs = { ...node.attrs, class: `fa-solid fa-xs ${icon} inline-node` } - return [`tei-${name}`,noteAttrs] + return [`tei-${name}`, noteAttrs] } } - } + } } createAtomSpec(name, icon, validAttrs, group) { @@ -230,21 +237,21 @@ export default class TEISchema { getAttrs: this.getAttrParser(validAttrs) }], toDOM: (node) => { - if( this.teiMode ) { + if (this.teiMode) { let attrs = this.filterOutBlanks(node.attrs) attrs = this.filterInternal(attrs) - return [name,attrs] + return [name, attrs] } else { const atomAttrs = { ...node.attrs, class: `fa-solid ${icon} inline-node` } - return [`tei-${name}`,atomAttrs] + return [`tei-${name}`, atomAttrs] } - } + } } } - getPMAttrSpec( validAttrs ) { + getPMAttrSpec(validAttrs) { let attrs = {} - for( const attr of validAttrs ) { + for (const attr of validAttrs) { attrs[attr] = { default: '' } } attrs['__error__'] = { default: false } @@ -255,32 +262,32 @@ export default class TEISchema { } // if there is a definition of this attr specific to this element, use that, otherwise use the global def. - getAttrSpec( attrID, elementID ) { + getAttrSpec(attrID, elementID) { const elementName = synthNameToElementName(elementID) - if( !elementName ) return this.attrs[attrID] + if (!elementName) return this.attrs[attrID] const elementSpec = this.elements[elementName] return elementSpec.derivedAttrs.includes(attrID) ? this.attrs[`${attrID}-${elementName}`] : this.attrs[attrID] } - getAttrParser( validAttrs ) { + getAttrParser(validAttrs) { return (dom) => { let parsedAttrs = {} - for( const attr of validAttrs ) { + for (const attr of validAttrs) { parsedAttrs[attr] = dom.getAttribute(attr) } // all ids should be in xml namespace const bareID = parsedAttrs['id'] - if( bareID ) { + if (bareID) { parsedAttrs['xml:id'] = bareID parsedAttrs['id'] = null } - return parsedAttrs + return parsedAttrs } } getElementType(elementID) { - for( const groupID of Object.keys(this.elementGroups) ) { - if( this.elementGroups[groupID].includes(elementID) ) { + for (const groupID of Object.keys(this.elementGroups)) { + if (this.elementGroups[groupID].includes(elementID)) { return groupID } } @@ -295,27 +302,27 @@ export default class TEISchema { return elementTypeToPmTypes[this.getElementType(elementID)] } - validElementMenu(elementMenu,elementID) { + validElementMenu(elementMenu, elementID) { const elementType = this.getElementType(elementID) const pmTypes = elementTypeToPmTypes[elementType] - const menus = pmTypes.map( pmType => pmTypeToMenu[pmType] ).flat() + const menus = pmTypes.map(pmType => pmTypeToMenu[pmType]).flat() return menus.includes(elementMenu) - } + } pmNodeToElementName(pmNode) { - const {name} = pmNode.type - - if( systemElements.includes(name) ) return null + const { name } = pmNode.type + + if (systemElements.includes(name)) return null - if( pmNode.type instanceof MarkType ) { - if( !this.elements[name].synth ) return name - if( name.startsWith(markPrefix) ) { + if (pmNode.type instanceof MarkType) { + if (!this.elements[name].synth) return name + if (name.startsWith(markPrefix)) { return name.slice(markPrefix.length) } else { return null } } else { - if( !this.elements[name].synth ) { + if (!this.elements[name].synth) { return name } else { return null @@ -329,8 +336,8 @@ export function getElementIcon(elementID, elements) { return elementSpec ? `fa-solid ${elementSpec.icon}` : null } -export function getElementTypeIcon( elementType ) { - if( elementType === 'mark' ) { +export function getElementTypeIcon(elementType) { + if (elementType === 'mark') { return "fa-solid fa-marker" } else { return "fa-solid fa-stamp" diff --git a/src/render/model/xml.js b/src/render/model/xml.js index 7ebba985..0db9e459 100644 --- a/src/render/model/xml.js +++ b/src/render/model/xml.js @@ -2,6 +2,8 @@ import { DOMSerializer, Fragment } from "prosemirror-model" import xpath from 'xpath' import serialize from "w3c-xmlserializer" import { data } from '../../data/test-milestone' +import { RELEASE_TYPES } from "semver" +import { find } from "../../../webpack.rules" // These elements are processed in XSLT by the xsl:strip-space command. This list is from xml/tei/odd/stripspace.xsl.model, TEI v4.1.0 const xmlStripSpaceNames = "TEI abstract additional address adminInfo altGrp altIdentifier alternate analytic annotation annotationBlock app appInfo application arc argument att attDef attList availability back biblFull biblStruct bicond binding bindingDesc body broadcast cRefPattern calendar calendarDesc castGroup castList category certainty char charDecl charProp choice cit classDecl classSpec classes climate cond constraintSpec content correction correspAction correspContext correspDesc custodialHist dataRef dataSpec datatype decoDesc dimensions div div1 div2 div3 div4 div5 div6 div7 divGen docTitle eLeaf eTree editionStmt editorialDecl elementSpec encodingDesc entry epigraph epilogue equipment event exemplum fDecl fLib facsimile figure fileDesc floatingText forest front fs fsConstraints fsDecl fsdDecl fvLib gap gi glyph graph graphic group handDesc handNotes history hom hyphenation iNode if imprint incident index interpGrp interpretation join joinGrp keywords kinesic langKnowledge langUsage layoutDesc leaf lg linkGrp list listAnnotation listApp listBibl listChange listEvent listForest listNym listObject listOrg listPerson listPlace listPrefixDef listRef listRelation listTranspose listWit location locusGrp macroSpec media metDecl model modelGrp modelSequence moduleRef moduleSpec monogr msContents msDesc msFrag msIdentifier msItem msItemStruct msPart namespace node normalization notatedMusic notesStmt nym object objectDesc objectIdentifier org paramList paramSpec particDesc performance person personGrp persona physDesc place population postscript precision prefixDef profileDesc projectDesc prologue publicationStmt punctuation quotation rdgGrp recordHist recording recordingStmt refsDecl relatedItem relation remarks respStmt respons revisionDesc root row samplingDecl schemaRef schemaSpec scriptDesc scriptStmt seal sealDesc segmentation sequence seriesStmt set setting settingDesc sourceDesc sourceDoc sp spGrp space spanGrp specGrp specList standOff state stdVals styleDefDecl subst substJoin superEntry supportDesc surface surfaceGrp table tagsDecl taxonomy teiCorpus teiHeader terrain text textClass textDesc timeline titlePage titleStmt trait transcriptionDesc transpose tree triangle typeDesc unitDecl unitDef vAlt vColl vDefault vLabel vMerge vNot vRange valItem valList vocal".split(' ') @@ -10,62 +12,13 @@ export function parseText(textEl, teiDocument, teiSchema, subDocName) { // make the TEIDocument visible to the node spec parser for access to sub docs teiSchema.teiDocuments.push(teiDocument) stripSpaces(textEl) - console.log(textEl) parseInterNodes(textEl, teiSchema, teiDocument.xmlDom) - - // TODO: Test loading milestones - if (subDocName === 'text') { - data.forEach(d => { - const pathsRaw = d.path.split(' ') - const path1 = pathsRaw[0].split('::'); - const path2 = pathsRaw[1].split('::') - insertMilestone(teiDocument.xmlDom, [path1[0], path1[1]], [path2[0], path2[1]], d.id) - }) - } const domParser = subDocName !== 'text' ? teiSchema.docNodeParsers[subDocName] : teiSchema.domParser const doc = domParser.parse(textEl) teiSchema.teiDocuments.pop() return doc } -function insertMilestone(xmlDom, start, end, id) { - const select = xpath.useNamespaces({ - teiXML: "http://www.tei-c.org/ns/1.0", - }); - const targetNodeStart = select(start[0].replaceAll('/', '/teiXML:'), xmlDom); - const targetNodeEnd = select(end[0].replaceAll('/', '/teiXML:'), xmlDom); - if (targetNodeStart[0] && targetNodeEnd[0] && start[0] === end[0]) { - let serializedXml = serialize(targetNodeStart[0]); - console.log('Target Start: ', serializedXml) - const normalized = serializedXml.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') - const newNodeStr = normalized.slice(0, parseInt(start[1]) - 1) + - `` + - normalized.slice(parseInt(start[1]) - 1, parseInt(end[1])) + - `` + - normalized.slice(parseInt(end[1])) - console.log('Normalized: ', newNodeStr) - const parser = new DOMParser(); - const newNode = parser.parseFromString(newNodeStr, "text/html") - targetNodeStart[0].replaceWith(newNode[0]) - } else if (targetNodeStart[0] && targetNodeEnd[0]) { - let serializedXmlStart = serialize(targetNodeStart[0]); - console.log('Target Start: ', serializedXmlStart) - let serializedXmlEnd = serialize(targetNodeEnd[0]); - console.log('Target End: ', serializedXmlEnd) - const normalizedStart = serializedXmlStart.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') - const newNodeStrStart = normalizedStart.slice(0, parseInt(start[1]) - 1) + `` + normalizedStart.slice(parseInt(start[1]) - 1) - console.log('Normalized Start: ', newNodeStrStart) - const normalizedEnd = serializedXmlEnd.replaceAll(' xmlns="http://www.tei-c.org/ns/1.0"', '') - const newNodeStrEnd = normalizedEnd.slice(0, parseInt(end[1]) - 1) + `` + normalizedEnd.slice(parseInt(end[1]) - 1) - console.log('Normalized End: ', newNodeStrEnd) - const parser = new DOMParser(); - const newNodeStart = parser.parseFromString(newNodeStrStart, "text/html") - targetNodeStart[0].replaceWith(newNodeStart[0]) - const newNodeEnd = parser.parseFromString(newNodeStrEnd, "text/html") - targetNodeEnd[0].replaceWith(newNodeEnd[0]) - } -} - export function serializeText(doc, teiDocument, teiSchema) { const { resourceType, xmlDom } = teiDocument teiSchema.teiMode = true @@ -266,6 +219,97 @@ export function addTextNodes(state, dispatch = null) { } } +export function addAnnotations(state, dispatch = null) { + function getNodeMap(path) { + const arr = path.split(' ') + const start = arr[0] + const end = arr[1] + const ret = { start: [], end: [] } + + for (let i = 0; i < 2; i++) { + let str, input; + if (i === 0) { + str = start; + input = ret.start; + } else { + str = end; + input = ret.end; + } + + // Always assume that first character is / + let index = 1 + let next = str.substring(index).indexOf('/') + while (next !== -1) { + if (str.substring(index, index + 1) === '/') { + index += 1 + } + + next = str.substring(index).indexOf('/') + if (next === -1) { + const parse = str.substring(index).split('['); + const id = parse[1].split(']') + const offset = id[1].split("::") + input.push({ node: parse[0], id: id[0], offset: offset[1] }) + } else { + const parse = str.substring(index, index + next).split('['); + const id = parse[1].split(']') + input.push({ node: parse[0], id: id[0] }) + } + + index = index + next + 1; + } + } + + return ret; + } + + function findNode(startNode, path, index, findCount, curOffset) { + const idCount = parseInt(path[index].id) === NaN ? -1 : parseInt(path[index].id); + const idMatch = idCount < 0 ? path[index].id : '' + + for (let i = 0; i < startNode.childCount; i++) { + const node = startNode.child(i); + //console.log('idCount: ', idCount, ', idMatch: ', idMatch, ', Find Count: ', findCount) + //console.log('Node Name: ', node.type.name, ', Path Name: ', path[index].node) + curOffset += node.nodeSize + if (node.type.name === path[index].node) { + if ((idCount < 0 && node.attrs['@xml:id'] === idMatch) || (idCount > -1 && findCount === idCount)) { + index++ + // console.log('Found!') + if (index < path.length) { + // console.log('Finding -> Node Size: ', node.nodeSize, ', Current Offset: ', curOffset - node.nodeSize) + return findNode(node, path, index, 1, curOffset - node.nodeSize) + } else { + // console.log('Found! -> Node Size: ', node.nodeSize, ', Current Offset: ', curOffset - node.nodeSize) + // Account for the TextNode by adding 2 + return { node, offset: curOffset - node.nodeSize + 2 } + } + } + + findCount++ + } + } + } + + const { doc } = state + + data.forEach(d => { + const paths = getNodeMap(d.path) + + // We want to start at the children of text node as that + // should always be present and should be the first nodes in the document + const startIndex = paths.start.findIndex(p => p.node === 'text') + 1; + const endIndex = paths.end.findIndex(p => p.node === 'text') + 1; + const startNode = findNode(doc, paths.start, startIndex, 1, 0); + const endNode = findNode(doc, paths.end, endIndex, 1, 0); + let position = 0 + doc.nodesBetween(startNode.offset + parseInt(paths.start[paths.start.length - 1].offset), endNode.offset + parseInt(paths.end[paths.end.length - 1].offset), function (node, pos, parent, index) { + position = pos + }) + console.log('Text Annotated: ', doc.textBetween(startNode.offset + parseInt(paths.start[paths.start.length - 1].offset), position + parseInt(paths.end[paths.end.length - 1].offset))) + }) +} + // take content fragment and replace any text nodes in there with text node type export function replaceTextNodes(textNodeType, fragment) { let siblings = [] From 876fc07b9577ca42fc60cdb28b347ef94aa19660 Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Mon, 23 Dec 2024 15:56:53 -0500 Subject: [PATCH 3/6] Working version with standOff parsing --- .prettierrc | 17 + .vscode/settings.json | 5 + package.json | 2 +- src/data/test-milestone.js | 6 + src/main-process/FairCopySession.js | 308 +-- src/main-process/ProjectStore.js | 258 +- .../components/main-window/MainWindow.jsx | 2375 ++++++++++------- .../main-window/tei-editor/TEIEditor.jsx | 4 +- src/render/css/TEIElementStyles.css | 117 +- src/render/model/TEIDocument.js | 6 +- src/render/model/TEISchema.js | 3 +- src/render/model/commands.js | 186 +- src/render/model/xml.js | 41 +- src/render/workers/parse-standoff.js | 39 + src/render/workers/project-archive-worker.js | 244 +- src/render/workers/remote-project-worker.js | 101 +- 16 files changed, 2142 insertions(+), 1570 deletions(-) create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 src/render/workers/parse-standoff.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d495dc20 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "jsxSingleQuote": true, + "overrides": [ + { + "files": [ + "**/*.mjs" + ], + "options": { + "singleQuote": true + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a5aabc7b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/package.json b/package.json index f8ff5e5a..2bac4d11 100644 --- a/package.json +++ b/package.json @@ -89,4 +89,4 @@ "eslintConfig": { "extends": "react-app" } -} +} \ No newline at end of file diff --git a/src/data/test-milestone.js b/src/data/test-milestone.js index 1a026f47..b1ecf1c6 100644 --- a/src/data/test-milestone.js +++ b/src/data/test-milestone.js @@ -4,6 +4,12 @@ export const data = [ path: "//text[@xml:id='text-1']/body[1]/div[1]/div[1]/lg[2]/l[1]::0 //text[@xml:id='text-1']/body[1]/div[1]/div[1]/lg[3]/l[4]::42", // My English Letter author: "Admin", annotation: "Person" + }, + { + id: "uid-4b2ab81c-04c1-44a3-bc21-fd4d8e071545", + path: "//text[@xml:id='text-1']/body[1]/div[1]/p[2]::3 //text[@xml:id='text-1']/body[1]/div[1]/p[2]::21", // My English Letter + author: "Admin", + annotation: "Person" } ] diff --git a/src/main-process/FairCopySession.js b/src/main-process/FairCopySession.js index 6287d708..428f421a 100644 --- a/src/main-process/FairCopySession.js +++ b/src/main-process/FairCopySession.js @@ -9,18 +9,18 @@ const initialRowsPerPage = 50 class FairCopySession { - constructor( fairCopyApplication, targetFile ) { + constructor(fairCopyApplication, targetFile) { this.fairCopyApplication = fairCopyApplication this.projectStore = new ProjectStore(fairCopyApplication) - this.projectStore.openProject(targetFile, this.onProjectOpened ) + this.projectStore.openProject(targetFile, this.onProjectOpened) this.remote = false this.projectOpen = true this.resourceViews = { currentView: 'home', - remote: { + remote: { indexParentID: null, parentEntry: null, - currentPage: 1, + currentPage: 1, rowsPerPage: initialRowsPerPage, totalRows: 0, nameFilter: null, @@ -31,13 +31,13 @@ class FairCopySession { home: { indexParentID: null, parentEntry: null, - currentPage: 1, + currentPage: 1, rowsPerPage: initialRowsPerPage, totalRows: 0, nameFilter: null, orderBy: 'name', order: 'ascending', - loading: true + loading: true } } } @@ -49,15 +49,15 @@ class FairCopySession { // id map authority tracks ids across processes and server this.idMapAuthority = createIDMapAuthority(this.remote, idMap, (idMapData) => { - this.fairCopyApplication.sendToAllWindows('IDMapUpdated', { idMapData } ) + this.fairCopyApplication.sendToAllWindows('IDMapUpdated', { idMapData }) }) - this.fairCopyApplication.sendToMainWindow('projectOpened', projectData ) + this.fairCopyApplication.sendToMainWindow('projectOpened', projectData) // init remote project if this is one - if( this.remote ) { + if (this.remote) { const { userID, serverURL, projectID } = manifestData - this.remoteProject = new RemoteProject(this, userID, serverURL, projectID ) + this.remoteProject = new RemoteProject(this, userID, serverURL, projectID) } this.requestResourceView() @@ -66,7 +66,7 @@ class FairCopySession { closeProject() { this.projectStore.close() - if( this.remoteProject ) this.remoteProject.close() + if (this.remoteProject) this.remoteProject.close() this.projectOpen = false } @@ -74,73 +74,73 @@ class FairCopySession { this.remoteProject.open() this.requestResourceView() } - + openImageResource(url) { this.projectStore.openImageResource(url) } setResourceMap(resourceMap, localID, parentID) { - this.idMapAuthority.setResourceMap( resourceMap, localID, parentID ) + this.idMapAuthority.setResourceMap(resourceMap, localID, parentID) } - addResource(resourceEntry,resourceData,resourceMap) { + addResource(resourceEntry, resourceData, resourceMap) { let idMap = null - if( resourceMap ) { + if (resourceMap) { const { localID } = resourceEntry - if( resourceEntry.parentResource ) { + if (resourceEntry.parentResource) { const { localID: parentID } = this.idMapAuthority.getLocalIDs(resourceEntry.parentResource) - idMap = this.idMapAuthority.addResource(localID,parentID,resourceMap) + idMap = this.idMapAuthority.addResource(localID, parentID, resourceMap) } else { - idMap = this.idMapAuthority.addResource(localID,null,resourceMap) + idMap = this.idMapAuthority.addResource(localID, null, resourceMap) } - - this.idMapAuthority.sendIDMapUpdate() + + this.idMapAuthority.sendIDMapUpdate() } - this.projectStore.addResource(resourceEntry,resourceData,idMap) + this.projectStore.addResource(resourceEntry, resourceData, idMap) } - replaceTEIDocument( resources ) { + replaceTEIDocument(resources) { const { resources: manifestResources } = this.projectStore.manifestData - const teiDocResource = resources.find( r => r.resourceEntry.type == 'teidoc' ) - const teiDocLocalID = teiDocResource.resourceEntry.localID - const existingTEIDoc = Object.values(manifestResources).find( r => r.localID == teiDocLocalID && r.type == 'teidoc' ) - const existingResourceMap = this.idMapAuthority.getResourceMapByLocalID(teiDocLocalID,null) + const teiDocResource = resources.find(r => r.resourceEntry.type == 'teidoc') + const teiDocLocalID = teiDocResource.resourceEntry.localID + const existingTEIDoc = Object.values(manifestResources).find(r => r.localID == teiDocLocalID && r.type == 'teidoc') + const existingResourceMap = this.idMapAuthority.getResourceMapByLocalID(teiDocLocalID, null) - if( existingTEIDoc ) { - for( const resource of resources ) { - if( resource.resourceEntry.type !== 'teidoc') { - if( !this.replaceResource(resource,existingTEIDoc) ) { + if (existingTEIDoc) { + for (const resource of resources) { + if (resource.resourceEntry.type !== 'teidoc') { + if (!this.replaceResource(resource, existingTEIDoc)) { // don't continue if any sub resources fail return } } else { // acknowledge that we got the tei doc - if(this.projectStore.importInProgress) { + if (this.projectStore.importInProgress) { this.projectStore.importContinue() } } } const doomedIDs = [] - for( const childLocalID of Object.keys(existingResourceMap.ids) ) { - const newerVersion = resources.find( r => r.resourceEntry.localID == childLocalID ) - if( !newerVersion ) { + for (const childLocalID of Object.keys(existingResourceMap.ids)) { + const newerVersion = resources.find(r => r.resourceEntry.localID == childLocalID) + if (!newerVersion) { // if there wasn't a newer version of this resource, remove it - doomedIDs.push( existingResourceMap.ids[childLocalID].resourceID ) + doomedIDs.push(existingResourceMap.ids[childLocalID].resourceID) } } - if( doomedIDs.length > 0 ) this.removeResources(doomedIDs) + if (doomedIDs.length > 0) this.removeResources(doomedIDs) } else { - if( !existingResourceMap ) { + if (!existingResourceMap) { // add this teidoc and its resources as a new doc - for( const resource of resources ) { - const { resourceEntry, content, resourceMap } = resource - this.addResource(resourceEntry, content, resourceMap ) + for (const resource of resources) { + const { resourceEntry, content, resourceMap } = resource + this.addResource(resourceEntry, content, resourceMap) } } else { - if(this.projectStore.importInProgress) { + if (this.projectStore.importInProgress) { this.projectStore.importError(`${teiDocLocalID} is not checked out and could not be replaced.`) - this.projectStore.importContinue() + this.projectStore.importContinue() } } } @@ -153,35 +153,35 @@ class FairCopySession { // is there an existing resource with this id and parent? also set parentResource let existingLocalResource = null - if( parentEntry ) { - existingLocalResource = Object.values(resources).find( r => r.localID == localID && parentEntry.id == r.parentResource ) + if (parentEntry) { + existingLocalResource = Object.values(resources).find(r => r.localID == localID && parentEntry.id == r.parentResource) resourceEntry.parentResource = parentEntry.id } else { - existingLocalResource = Object.values(resources).find( r => r.localID == localID && r.parentResource == null) + existingLocalResource = Object.values(resources).find(r => r.localID == localID && r.parentResource == null) resourceEntry.parentResource = null } - if( existingLocalResource ) { + if (existingLocalResource) { // save over top of the existing resource - resourceEntry.id = existingLocalResource.id + resourceEntry.id = existingLocalResource.id resourceMap.resourceID = existingLocalResource.id const parentLocalID = parentEntry ? parentEntry.localID : null this.setResourceMap(resourceMap, localID, parentLocalID) - this.saveResource(resourceEntry.id,content,false) + this.saveResource(resourceEntry.id, content, false) - if(this.projectStore.importInProgress) { + if (this.projectStore.importInProgress) { this.projectStore.importContinue() } } else { // otherwise, does it exist in the idMap? const parentID = parentEntry ? parentEntry.localID : null - const existingResourceMap = this.idMapAuthority.getResourceMapByLocalID(localID,parentID) + const existingResourceMap = this.idMapAuthority.getResourceMapByLocalID(localID, parentID) // if not, just add this resource - if( !existingResourceMap ) { - if( parentEntry ) resourceEntry.parentResource = parentEntry.id + if (!existingResourceMap) { + if (parentEntry) resourceEntry.parentResource = parentEntry.id this.addResource(resourceEntry, content, resourceMap) } else { - if(this.projectStore.importInProgress) { + if (this.projectStore.importInProgress) { this.projectStore.importError(`${localID} is not checked out and could not be replaced.`) this.projectStore.importContinue() return false @@ -194,18 +194,18 @@ class FairCopySession { removeResources(resourceIDs) { const { resources } = this.projectStore.manifestData // use this map to generate unique ID list - const doomedIDMap = {} + const doomedIDMap = {} - for( const resourceID of resourceIDs ) { + for (const resourceID of resourceIDs) { const resourceEntry = resources[resourceID] - if( resourceEntry ) { - if( resourceEntry.type === 'teidoc') { + if (resourceEntry) { + if (resourceEntry.type === 'teidoc') { // also delete any checked out children - for( const localResource of Object.values(resources) ) { + for (const localResource of Object.values(resources)) { const { parentResource } = localResource - if( localResource.type !== 'image' && parentResource === resourceEntry.id ) { - doomedIDMap[localResource.id] = true - } + if (localResource.type !== 'image' && parentResource === resourceEntry.id) { + doomedIDMap[localResource.id] = true + } } } doomedIDMap[resourceEntry.id] = true @@ -216,7 +216,7 @@ class FairCopySession { const doomedIDs = Object.keys(doomedIDMap) const idMap = this.idMapAuthority.removeResources(doomedIDs) - this.projectStore.removeResources(doomedIDs,idMap) + this.projectStore.removeResources(doomedIDs, idMap) this.requestResourceView() } @@ -224,40 +224,40 @@ class FairCopySession { const { resources } = this.projectStore.manifestData const recoveredIDMap = {} - for( const resourceID of resourceIDs ) { + for (const resourceID of resourceIDs) { const resourceEntry = resources[resourceID] - if( resourceEntry && resourceEntry.deleted ) { + if (resourceEntry && resourceEntry.deleted) { // also recover any checked out children - if( resourceEntry.type === 'teidoc') { - for( const localResource of Object.values(resources) ) { + if (resourceEntry.type === 'teidoc') { + for (const localResource of Object.values(resources)) { const { parentResource } = localResource - if( localResource.type !== 'image' && parentResource === resourceEntry.id ) { - recoveredIDMap[localResource.id] = true - } + if (localResource.type !== 'image' && parentResource === resourceEntry.id) { + recoveredIDMap[localResource.id] = true + } } } recoveredIDMap[resourceEntry.id] = true } else { log.info(`Error recovering resource entry: ${resourceID}`) - } + } } const recoveredIDs = Object.keys(recoveredIDMap) - if( recoveredIDs.length > 0 ) { + if (recoveredIDs.length > 0) { const idMap = this.idMapAuthority.recoverResources(recoveredIDs) - this.projectStore.recoverResources(recoveredIDs,idMap) - this.requestResourceView() + this.projectStore.recoverResources(recoveredIDs, idMap) + this.requestResourceView() } } updateResourceView(resourceViewRequest) { - if( resourceViewRequest ) { - const { currentView, indexParentID, parentEntry } = resourceViewRequest + if (resourceViewRequest) { + const { currentView, indexParentID, parentEntry } = resourceViewRequest const resourceView = this.resourceViews[currentView] const currentPage = resourceViewRequest.currentPage !== undefined ? resourceViewRequest.currentPage : resourceView.currentPage let nameFilter, order, orderBy // carry over name filter and order values if switching views - if( this.resourceViews.currentView !== currentView ) { + if (this.resourceViews.currentView !== currentView) { nameFilter = resourceViewRequest.nameFilter !== undefined ? resourceViewRequest.nameFilter : this.resourceViews[currentView].nameFilter order = resourceViewRequest.order !== undefined ? resourceViewRequest.order : this.resourceViews[currentView].order orderBy = resourceViewRequest.orderBy !== undefined ? resourceViewRequest.orderBy : this.resourceViews[currentView].orderBy @@ -267,29 +267,29 @@ class FairCopySession { orderBy = resourceViewRequest.orderBy !== undefined ? resourceViewRequest.orderBy : resourceView.orderBy } this.resourceViews[currentView] = { ...resourceView, indexParentID, parentEntry, nameFilter, order, orderBy, currentPage } - this.resourceViews.currentView = currentView + this.resourceViews.currentView = currentView } this.requestResourceView() } requestResourceView() { - const {currentView} = this.resourceViews + const { currentView } = this.resourceViews const resourceView = this.resourceViews[currentView] const { indexParentID, currentPage, rowsPerPage } = resourceView const { resources: localResources } = this.projectStore.manifestData - if( currentView === 'remote' ) { + if (currentView === 'remote') { // request resource data from server this.remoteProject.requestResourceView(resourceView) } else { // respond right away from project store resourceView.parentEntry = indexParentID ? localResources[indexParentID] : null const { nameFilter, order, orderBy } = resourceView - const sortedResources = Object.values(localResources).sort((a,b) => { + const sortedResources = Object.values(localResources).sort((a, b) => { const valueA = a[orderBy].toUpperCase() const valueB = b[orderBy].toUpperCase() - if( valueA === valueB ) return 0 - if( order == 'ascending' ) { + if (valueA === valueB) return 0 + if (order == 'ascending') { return valueA > valueB ? 1 : -1 } else { return valueA < valueB ? 1 : -1 @@ -297,35 +297,35 @@ class FairCopySession { }) let resourceIndex = [] - for( const localResource of sortedResources ) { + for (const localResource of sortedResources) { const { parentResource } = localResource - if( localResource.type !== 'image' ) { + if (localResource.type !== 'image') { // if this resource is a child of current parent OR // if the parent is not checked out, display it at top level - if( parentResource === indexParentID || - ( indexParentID === null && !localResources[parentResource] )) { - if( indexParentID ) { + if (parentResource === indexParentID || + (indexParentID === null && !localResources[parentResource])) { + if (indexParentID) { // filter doesn't act on teidoc views - resourceIndex.push(localResource) - } else if( !nameFilter || localResource.name.includes(nameFilter) ) { - resourceIndex.push(localResource) + resourceIndex.push(localResource) + } else if (!nameFilter || localResource.name.includes(nameFilter)) { + resourceIndex.push(localResource) } } - } + } } // don't let currentPage be > page count - let pageCount = Math.ceil(resourceIndex.length/rowsPerPage) + let pageCount = Math.ceil(resourceIndex.length / rowsPerPage) pageCount = pageCount === 0 ? 1 : pageCount const nextPage = currentPage > pageCount ? pageCount : currentPage - const start = rowsPerPage * (nextPage-1) + const start = rowsPerPage * (nextPage - 1) const end = start + rowsPerPage resourceView.totalRows = resourceIndex.length resourceView.currentPage = nextPage resourceView.loading = false this.resourceViews[currentView] = resourceView - resourceIndex = resourceIndex.slice(start,end) + resourceIndex = resourceIndex.slice(start, end) - this.fairCopyApplication.sendToAllWindows('resourceViewUpdate', { resourceViews: this.resourceViews, resourceIndex } ) + this.fairCopyApplication.sendToAllWindows('resourceViewUpdate', { resourceViews: this.resourceViews, resourceIndex }) } } @@ -334,82 +334,82 @@ class FairCopySession { const { indexParentID, rowsPerPage, currentPage, totalRows } = resourceView // if parent isn't in remote response, must be local parent - if( indexParentID !== null && !resourceView.parentEntry ) { + if (indexParentID !== null && !resourceView.parentEntry) { resourceView.parentEntry = localResources[indexParentID] } - const resourceIndex = remoteResources.map( resourceEntry => { + const resourceIndex = remoteResources.map(resourceEntry => { return localResources[resourceEntry.id] ? localResources[resourceEntry.id] : resourceEntry }) // don't let currentPage be > page count - let pageCount = Math.ceil(totalRows/rowsPerPage) + let pageCount = Math.ceil(totalRows / rowsPerPage) pageCount = pageCount === 0 ? 1 : pageCount resourceView.currentPage = currentPage > pageCount ? pageCount : currentPage - + this.resourceViews.remote = resourceView this.fairCopyApplication.sendToAllWindows('resourceViewUpdate', { resourceViews: this.resourceViews, resourceIndex }) } - + searchProject(searchQuery) { - this.projectStore.searchIndex.searchProject(searchQuery) + this.projectStore.searchIndex.searchProject(searchQuery) } saveResource(resourceID, resourceData, updatePreview) { const { resources } = this.projectStore.manifestData const resourceEntry = resources[resourceID] - if( resourceEntry ) { + if (resourceEntry) { const { localID, parentID } = this.idMapAuthority.getLocalIDs(resourceID) const idMap = this.idMapAuthority.commitResource(localID, parentID) - this.projectStore.saveResource(resourceEntry, resourceData, idMap) - if( updatePreview ) { + this.projectStore.saveResource(resourceEntry, resourceData, idMap) + if (updatePreview) { this.requestPreviewView({ resourceEntry }) - } - return true + } + return true } return false } updateResource(resourceEntry) { const { resources } = this.projectStore.manifestData - if( resources[resourceEntry.id] ) { + if (resources[resourceEntry.id]) { const currentLocalID = resources[resourceEntry.id].localID const currentParentID = resources[resourceEntry.id].parentResource resources[resourceEntry.id] = resourceEntry - let idMap = null - if( resourceEntry.parentResource !== currentParentID ) { - idMap = this.idMapAuthority.moveResourceMap( resourceEntry.localID, currentLocalID, resourceEntry.parentResource, currentParentID ) + let idMap = null + if (resourceEntry.parentResource !== currentParentID) { + idMap = this.idMapAuthority.moveResourceMap(resourceEntry.localID, currentLocalID, resourceEntry.parentResource, currentParentID) } - if( resourceEntry.localID !== currentLocalID ) { - if( resourceEntry.parentResource ) { + if (resourceEntry.localID !== currentLocalID) { + if (resourceEntry.parentResource) { const { localID: parentID } = this.idMapAuthority.getLocalIDs(resourceEntry.parentResource) - idMap = this.idMapAuthority.changeID( resourceEntry.localID, currentLocalID, parentID ) + idMap = this.idMapAuthority.changeID(resourceEntry.localID, currentLocalID, parentID) } else { - idMap = this.idMapAuthority.changeID( resourceEntry.localID, currentLocalID, null ) + idMap = this.idMapAuthority.changeID(resourceEntry.localID, currentLocalID, null) } } - if( idMap ) { + if (idMap) { this.projectStore.saveIDMap(idMap) - this.idMapAuthority.sendIDMapUpdate() + this.idMapAuthority.sendIDMapUpdate() } - this.projectStore.saveManifest() - this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry ) - this.requestResourceView() + this.projectStore.saveManifest() + this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry) + this.requestResourceView() } else { log.info(`Error updating resource entry: ${resourceEntry.id}`) } } abandonResourceMap(resourceID) { - const { localID, parentID } = this.idMapAuthority.getLocalIDs( resourceID ) - this.idMapAuthority.abandonResourceMap(localID,parentID) + const { localID, parentID } = this.idMapAuthority.getLocalIDs(resourceID) + this.idMapAuthority.abandonResourceMap(localID, parentID) this.idMapAuthority.sendIDMapUpdate() } - openResource(resourceID, xmlID=null ) { - if( this.remote && !this.projectStore.manifestData.resources[resourceID]) { + openResource(resourceID, xmlID = null) { + if (this.remote && !this.projectStore.manifestData.resources[resourceID]) { this.remoteProject.openResource(resourceID, xmlID) } else { this.projectStore.openResource(resourceID, xmlID) @@ -417,32 +417,42 @@ class FairCopySession { } resourceOpened(resourceEntry, parentEntry, resource, xmlID) { - if( this.remote && !parentEntry && resourceEntry.parentResource ) { + if (this.remote && !parentEntry && resourceEntry.parentResource) { // parent is remote and resourceEntry is local or checked out // go lookup the parent entry to complete this payload this.remoteProject.getParentEntry(resourceEntry, resource, xmlID) } else { - if( xmlID ) { + if (xmlID) { // at the moment, only image views use xmlID - this.openImageView( { xmlID, resourceEntry, parentEntry, resource } ) + this.openImageView({ xmlID, resourceEntry, parentEntry, resource }) } else { - this.fairCopyApplication.sendToAllWindows('resourceOpened', { resourceEntry, parentEntry, resource } ) - log.info(`opened resourceID: ${resourceEntry.id}`) + this.fairCopyApplication.sendToAllWindows('resourceOpened', { resourceEntry, parentEntry, resource }) + log.info(`opened resourceID: ${resourceEntry.id}`) } } } + + annotationDataReceived(data, parentResourceID) { + // For now this shouldn't work for remote projects + if (this.remote) { + // Do nothing + } else { + this.fairCopyApplication.sendToAllWindows('annotationDataReceived', { annotationData: data, parentResourceID }) + } + } + // parent found, send complete resourceOpened message parentFound(resourceEntry, parentEntry, resource, xmlID) { - if( xmlID ) { - this.openImageView( { xmlID, resourceEntry, parentEntry, resource } ) + if (xmlID) { + this.openImageView({ xmlID, resourceEntry, parentEntry, resource }) } else { - this.fairCopyApplication.sendToMainWindow('resourceOpened', { resourceEntry, parentEntry, resource } ) - log.info(`got parent, opened resourceID: ${resourceEntry.id}`) + this.fairCopyApplication.sendToMainWindow('resourceOpened', { resourceEntry, parentEntry, resource }) + log.info(`got parent, opened resourceID: ${resourceEntry.id}`) } } - openImageView( resourceData ) { + openImageView(resourceData) { const { resourceEntry } = resourceData const imageViewData = { ...resourceData } @@ -450,8 +460,8 @@ class FairCopySession { imageViewData.teiSchema = fs.readFileSync(`${baseDir}/tei-simple.json`).toString('utf-8') imageViewData.idMap = JSON.stringify(this.idMapAuthority.idMap) - // mix in remote project data if needed - if( this.remote ) { + // mix in remote project data if needed + if (this.remote) { const { userID, serverURL, permissions } = this.projectStore.manifestData imageViewData.userID = userID imageViewData.serverURL = serverURL @@ -459,12 +469,12 @@ class FairCopySession { imageViewData.permissions = permissions } const imageView = this.fairCopyApplication.imageViews[resourceEntry.id] - imageView.webContents.send('imageViewOpened', imageViewData ) + imageView.webContents.send('imageViewOpened', imageViewData) this.projectStore.sendLocalResources() } - importStart(paths,options) { - this.projectStore.importStart(paths,options) + importStart(paths, options) { + this.projectStore.importStart(paths, options) } importIIIFStart(importList) { @@ -484,7 +494,7 @@ class FairCopySession { const { resources } = this.projectStore.manifestData const committedResources = [] - const createCommitEntry = ( resourceEntry ) => { + const createCommitEntry = (resourceEntry) => { const { id, local, deleted, name, localID, parentResource: parentID, type } = resourceEntry const resourceMap = !deleted ? this.idMapAuthority.getResourceMap(id) : null const action = deleted ? 'destroy' : local ? 'create' : 'update' @@ -498,16 +508,16 @@ class FairCopySession { resourceType: type } } - + const homeParentID = this.resourceViews.home.indexParentID - for( const resourceID of checkInResources ) { + for (const resourceID of checkInResources) { const resourceEntry = resources[resourceID] // ignore resources that aren't in local manifest - if( resourceEntry ) { + if (resourceEntry) { const resourceCommitEntry = createCommitEntry(resourceEntry) committedResources.push(resourceCommitEntry) } - if( resourceID === homeParentID ) { + if (resourceID === homeParentID) { // if this got checked in, move to root this.resourceViews.home.indexParentID = null } @@ -531,12 +541,12 @@ class FairCopySession { checkOutConfig() { this.remoteProject.checkOutConfig() - } + } - exportFairCopyConfig(exportPath,fairCopyConfig) { - this.projectStore.exportFairCopyConfig(exportPath,fairCopyConfig) + exportFairCopyConfig(exportPath, fairCopyConfig) { + this.projectStore.exportFairCopyConfig(exportPath, fairCopyConfig) } - + updateProjectInfo(projectInfo) { this.projectStore.updateProjectInfo(projectInfo) // TODO if this is a remote project, send the latest name and description to server @@ -547,8 +557,8 @@ class FairCopySession { this.projectStore.requestPreviewView(previewData) } - requestExport(resourceEntries,path) { - this.projectStore.requestExport(resourceEntries,path) + requestExport(resourceEntries, path) { + this.projectStore.requestExport(resourceEntries, path) } requestEditionCrafterData(url) { diff --git a/src/main-process/ProjectStore.js b/src/main-process/ProjectStore.js index 68993968..4f64f460 100644 --- a/src/main-process/ProjectStore.js +++ b/src/main-process/ProjectStore.js @@ -8,6 +8,7 @@ const { SearchIndex } = require('./SearchIndex') const { WorkerWindow } = require('./WorkerWindow') const { EditionCrafterServer } = require('./EditionCrafterServer') + const manifestEntryName = 'faircopy-manifest.json' const configSettingsEntryName = 'config-settings.json' const idMapEntryName = 'id-map.json' @@ -22,15 +23,15 @@ class ProjectStore { this.editionCrafterServer = new EditionCrafterServer() } - initProjectArchiveWorker( baseDir, debug, projectFilePath ) { - this.projectArchiveWorker = new WorkerWindow( baseDir, debug, 'project-archive', (msg) => { + initProjectArchiveWorker(baseDir, debug, projectFilePath) { + this.projectArchiveWorker = new WorkerWindow(baseDir, debug, 'project-archive', (msg) => { const { messageType } = msg - switch( messageType ) { - case 'project-data': + switch (messageType) { + case 'project-data': { const { project } = msg - this.loadProject(project) + this.loadProject(project) } break case 'resource-data': @@ -38,20 +39,26 @@ class ProjectStore { const { resourceID, xmlID, resource } = msg const resourceEntry = this.manifestData.resources[resourceID] const parentEntry = this.manifestData.resources[resourceEntry.parentResource] - this.fairCopyApplication.fairCopySession.resourceOpened( resourceEntry, parentEntry, resource, xmlID ) + this.fairCopyApplication.fairCopySession.resourceOpened(resourceEntry, parentEntry, resource, xmlID) + } + break + case 'annotation-data': + { + const { data, parentResourceID } = msg + this.fairCopyApplication.fairCopySession.annotationDataReceived(data, parentResourceID) } break case 'index-resource': { const { resourceID, resource } = msg const resourceEntry = this.manifestData.resources[resourceID] - this.searchIndex.indexResource( resourceID, resourceEntry.type, resource ) + this.searchIndex.indexResource(resourceID, resourceEntry.type, resource) } break case 'exported-resource': { const { error, path } = msg - if( error ) { + if (error) { log.info(`Error exporting resources to: ${path} ${error}`) } else { log.info(`Exported resources to: ${path}`) @@ -61,19 +68,19 @@ class ProjectStore { case 'preview-resource': { const { previewData, ecData, error } = msg - if( error ) { + if (error) { log.info(`Error previewing resource: ${error}`) } else { const teiDocumentID = previewData.resourceEntry.localID - this.editionCrafterServer.addTEIDocument(teiDocumentID,ecData) - this.fairCopyApplication.createPreviewWindow(previewData).then(() => {}) + this.editionCrafterServer.addTEIDocument(teiDocumentID, ecData) + this.fairCopyApplication.createPreviewWindow(previewData).then(() => { }) } } - break + break case 'cache-file-name': { const { cacheFile } = msg - if( cacheFile ) this.fairCopyApplication.localFileProtocolCallback(decodeURIComponent(cacheFile)) + if (cacheFile) this.fairCopyApplication.localFileProtocolCallback(decodeURIComponent(cacheFile)) } break case 'check-in-results': @@ -85,37 +92,37 @@ class ProjectStore { case 'check-out-results': { const { resources, error } = msg - this.checkOutResults(resources,error) + this.checkOutResults(resources, error) } break default: throw new Error(`Unrecognized message type ${messageType} received from project archive: ${JSON.stringify(msg)}`) } }) - + return this.projectArchiveWorker.start({ projectFilePath, manifestEntryName, configSettingsEntryName, idMapEntryName }) } openProject(projectFilePath, onProjectOpened) { - const {baseDir} = this.fairCopyApplication + const { baseDir } = this.fairCopyApplication this.onProjectOpened = onProjectOpened const debug = !app.isPackaged - this.initProjectArchiveWorker( baseDir, debug, projectFilePath ).then(() => { + this.initProjectArchiveWorker(baseDir, debug, projectFilePath).then(() => { this.projectArchiveWorker.postMessage({ messageType: 'open' }) }) } loadProject(project) { const { baseDir, config: fairCopyAppConfig } = this.fairCopyApplication - - if( !project ) { + + if (!project) { log.error('Attempted to open invalid project file.') return } const teiSchema = fs.readFileSync(`${baseDir}/tei-simple.json`).toString('utf-8') const baseConfigJSON = fs.readFileSync(`${baseDir}/faircopy-config.json`).toString('utf-8') - if( !teiSchema || !baseConfigJSON ) { + if (!teiSchema || !baseConfigJSON) { log.info('Application data is missing or corrupted.') return } @@ -124,7 +131,7 @@ class ProjectStore { let { fairCopyManifest, fairCopyConfig, idMap, projectFilePath } = project - if( !fairCopyManifest || !fairCopyConfig || !idMap ) { + if (!fairCopyManifest || !fairCopyConfig || !idMap) { log.info('Project file is missing required entries.') return } @@ -133,7 +140,7 @@ class ProjectStore { this.manifestData = fairCopyManifest const currentVersion = this.fairCopyApplication.config.version - if( app.isPackaged && !compatibleProject(this.manifestData, currentVersion) ) { + if (app.isPackaged && !compatibleProject(this.manifestData, currentVersion)) { log.info('Project file is incompatible.') const incompatInfo = { projectFilePath, projectFileVersion: this.manifestData.generatedWith } this.fairCopyApplication.sendToMainWindow('projectIncompatible', incompatInfo) @@ -142,68 +149,68 @@ class ProjectStore { // migrate the manifestData to latest version this.manifestData = migrateManifestData(this.manifestData) - + // if elements changed in config, migrate project config - migrateConfig(this.manifestData.generatedWith,this.baseConfig,fairCopyConfig, fairCopyAppConfig) + migrateConfig(this.manifestData.generatedWith, this.baseConfig, fairCopyConfig, fairCopyAppConfig) this.migratedConfig = fairCopyConfig // clean up the idMap if there are no resources idMap = this.cleanUpIDMap(idMap) // apply any migrations to ID Map data - idMap = migrateIDMap(this.manifestData.generatedWith,idMap,this.manifestData.resources) + idMap = migrateIDMap(this.manifestData.generatedWith, idMap, this.manifestData.resources) // setup search index if local - if( !this.manifestData.remote ) { - this.searchIndex = new SearchIndex( teiSchema, this, (status) => { - this.fairCopyApplication.sendToMainWindow('searchSystemStatus', status ) - }) + if (!this.manifestData.remote) { + this.searchIndex = new SearchIndex(teiSchema, this, (status) => { + this.fairCopyApplication.sendToMainWindow('searchSystemStatus', status) + }) } const projectData = { projectFilePath, fairCopyManifest: this.manifestData, teiSchema, fairCopyConfig, baseConfig: this.baseConfig, idMap } - this.onProjectOpened( projectData ) + this.onProjectOpened(projectData) } // clean up the idMap if there are no resources cleanUpIDMap(idMap) { const { resources } = this.manifestData - if( Object.values(resources).length === 0 ) { + if (Object.values(resources).length === 0) { return "{}" } else { return idMap } } - - async importStart(paths,options) { + + async importStart(paths, options) { this.importRunning(true) const importList = [] - for( const path of paths ) { + for (const path of paths) { const stats = await stat(path) - if( stats.size <= maxImportFileSize ) { - const data = await readFile(path, { encoding: 'utf-8'} ) - importList.push({ path, data, options }) + if (stats.size <= maxImportFileSize) { + const data = await readFile(path, { encoding: 'utf-8' }) + importList.push({ path, data, options }) } else { - importList.push({ path, error: 'too-big'}) + importList.push({ path, error: 'too-big' }) } } const importData = { command: 'start', importList } - this.fairCopyApplication.sendToMainWindow('importData', importData ) + this.fairCopyApplication.sendToMainWindow('importData', importData) this.importContinue() } - importIIIFStart( importList ) { + importIIIFStart(importList) { this.importRunning(true) const importData = { command: 'start', importList } - this.fairCopyApplication.sendToMainWindow('importData', importData ) + this.fairCopyApplication.sendToMainWindow('importData', importData) this.importContinue() } importContinue() { - this.fairCopyApplication.sendToMainWindow('importData', { command: 'next' } ) + this.fairCopyApplication.sendToMainWindow('importData', { command: 'next' }) } importError(errorMessage) { - this.fairCopyApplication.sendToMainWindow('importData', { command: 'error', errorMessage } ) + this.fairCopyApplication.sendToMainWindow('importData', { command: 'error', errorMessage }) } importEnd() { @@ -216,10 +223,10 @@ class ProjectStore { this.importInProgress = running } - requestExport(resourceEntries,path) { + requestExport(resourceEntries, path) { const { resources: localEntries, remote, userID, serverURL, projectID } = this.manifestData const projectData = { localEntries, remote, userID, serverURL, projectID } - for( const resourceEntry of resourceEntries ) { + for (const resourceEntry of resourceEntries) { this.projectArchiveWorker.postMessage({ messageType: 'request-export', resourceEntry, projectData, path }) } } @@ -232,92 +239,92 @@ class ProjectStore { close() { this.projectArchiveWorker.postMessage({ messageType: 'close' }) - if( this.searchIndex ) this.searchIndex.close() + if (this.searchIndex) this.searchIndex.close() } saveResource(resourceEntry, resourceData, idMap) { const { id, type } = resourceEntry this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) this.projectArchiveWorker.postMessage({ messageType: 'write-resource', resourceID: id, data: resourceData }) - if( this.searchIndex ) this.searchIndex.indexResource(id, type, resourceData ) + if (this.searchIndex) this.searchIndex.indexResource(id, type, resourceData) this.save() } - - requestEditionCrafterData( url ) { + + requestEditionCrafterData(url) { return this.editionCrafterServer.processRequest(url) } - requestIndex( resourceID ) { + requestIndex(resourceID) { this.projectArchiveWorker.postMessage({ messageType: 'request-index', resourceID }) } - addResource( resourceEntry, resourceData, idMap ) { + addResource(resourceEntry, resourceData, idMap) { this.manifestData.resources[resourceEntry.id] = resourceEntry - if( resourceEntry.type === 'image' ) { + if (resourceEntry.type === 'image') { this.projectArchiveWorker.postMessage({ messageType: 'add-local-file', resourceID: resourceEntry.id, localFilePath: resourceData }) } else { - if( idMap ) { + if (idMap) { this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) } this.projectArchiveWorker.postMessage({ messageType: 'write-resource', resourceID: resourceEntry.id, data: resourceData }) - if( this.searchIndex ) this.searchIndex.indexResource(resourceEntry.id, resourceEntry.type, resourceData ) + if (this.searchIndex) this.searchIndex.indexResource(resourceEntry.id, resourceEntry.type, resourceData) } - if(this.importInProgress) { + if (this.importInProgress) { this.importContinue() } else { this.saveManifest() this.fairCopyApplication.fairCopySession.requestResourceView() } } - - removeResources(resourceIDs,idMap) { - for( const resourceID of resourceIDs ) { + + removeResources(resourceIDs, idMap) { + for (const resourceID of resourceIDs) { const resourceEntry = this.manifestData.resources[resourceID] - + // go through the manifest entries, if there are local images associated with this facs, delete them too - if( resourceEntry.type === 'facs' ) { - for( const entry of Object.values(this.manifestData.resources) ) { - const {id, type, localID} = entry - if( type === 'image' && localID.startsWith(resourceID) ) { - delete this.manifestData.resources[id] - this.projectArchiveWorker.postMessage({ messageType: 'remove-file', fileID: id }) + if (resourceEntry.type === 'facs') { + for (const entry of Object.values(this.manifestData.resources)) { + const { id, type, localID } = entry + if (type === 'image' && localID.startsWith(resourceID)) { + delete this.manifestData.resources[id] + this.projectArchiveWorker.postMessage({ messageType: 'remove-file', fileID: id }) log.info(`Removed image resource from project: ${id}`) } } } else { // remove associated search index - if( this.searchIndex ) this.searchIndex.removeIndex(resourceID) + if (this.searchIndex) this.searchIndex.removeIndex(resourceID) } - - if( resourceEntry.local ) { - delete this.manifestData.resources[resourceID] - if( resourceEntry.type !== 'teidoc' ) { + + if (resourceEntry.local) { + delete this.manifestData.resources[resourceID] + if (resourceEntry.type !== 'teidoc') { this.projectArchiveWorker.postMessage({ messageType: 'remove-file', fileID: resourceID }) } } else { resourceEntry.deleted = true - this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry ) + this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry) } - + log.info(`Removed resource from project: ${resourceID}`) } - if( idMap ) { - this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) + if (idMap) { + this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) } this.saveManifest() } - recoverResources( resourceIDs, idMap ) { - for( const resourceID of resourceIDs ) { - const resourceEntry = this.manifestData.resources[resourceID] + recoverResources(resourceIDs, idMap) { + for (const resourceID of resourceIDs) { + const resourceEntry = this.manifestData.resources[resourceID] - if( resourceEntry ) { + if (resourceEntry) { resourceEntry.deleted = false - this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry ) - } + this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry) + } } - this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) + this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) this.saveManifest() } @@ -333,15 +340,15 @@ class ProjectStore { this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) } - saveFairCopyConfig( fairCopyConfig, lastAction=null ) { + saveFairCopyConfig(fairCopyConfig, lastAction = null) { this.migratedConfig = null - if( lastAction ) { + if (lastAction) { this.manifestData.configLastAction = lastAction } const configData = JSON.stringify(fairCopyConfig) - if( configData && configData.length > 0 ) { + if (configData && configData.length > 0) { this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: configSettingsEntryName, data: configData }) - this.saveManifest() + this.saveManifest() } else { log.error(`Cannot save empty FairCopy Config.`) } @@ -349,20 +356,20 @@ class ProjectStore { save() { // if there was a migration that hasn't been saved yet, save it now - if( this.migratedConfig ) { - this.saveFairCopyConfig( this.migratedConfig ) + if (this.migratedConfig) { + this.saveFairCopyConfig(this.migratedConfig) } else { this.projectArchiveWorker.postMessage({ messageType: 'save' }) } } - exportFairCopyConfig( exportPath, fairCopyConfigJSONCompact ) { + exportFairCopyConfig(exportPath, fairCopyConfigJSONCompact) { try { const fairCopyConfig = JSON.parse(fairCopyConfigJSONCompact) const fairCopyConfigJSON = JSON.stringify(fairCopyConfig, null, '\t') - fs.writeFileSync(exportPath,fairCopyConfigJSON) + fs.writeFileSync(exportPath, fairCopyConfigJSON) log.info(`Exported project config to: ${exportPath}`) - } catch(e) { + } catch (e) { log.error(`Error exporting project config: ${e}`) } } @@ -370,7 +377,7 @@ class ProjectStore { updateProjectInfo(projectInfo) { const { name, description, permissions } = projectInfo this.manifestData.projectName = name - this.manifestData.description = description + this.manifestData.description = description this.manifestData.permissions = permissions this.saveManifest() } @@ -379,98 +386,101 @@ class ProjectStore { this.projectArchiveWorker.postMessage({ messageType: 'check-in', userID, serverURL, projectID, committedResources, message }) } - checkOut(userID, serverURL, projectID, resourceEntries ) { + checkOut(userID, serverURL, projectID, resourceEntries) { this.projectArchiveWorker.postMessage({ messageType: 'check-out', userID, serverURL, projectID, resourceEntries }) } openImageResource(requestURL) { - const resourceID = requestURL.replace(`local://`, '').replace('..','') + const resourceID = requestURL.replace(`local://`, '').replace('..', '') const resourceEntry = this.manifestData.resources[resourceID] - if( resourceEntry ) { + if (resourceEntry) { // create a file path to the temp dir const ext = getExtensionForMIMEType(resourceEntry.mimeType) const fileName = `${resourceID}.${ext}` this.projectArchiveWorker.postMessage({ messageType: 'cache-resource', resourceID, fileName }) - } + } } - openResource(resourceID,xmlID) { + openResource(resourceID, xmlID) { const resourceEntry = this.manifestData.resources[resourceID] - if( resourceEntry ) { + if (resourceEntry) { this.projectArchiveWorker.postMessage({ messageType: 'read-resource', resourceID, xmlID }) } } - checkOutResults(resources,error) { + checkOutResults(resources, error) { const checkOutStatus = [] - for( const resource of Object.values(resources) ) { + for (const resource of Object.values(resources)) { const { state, resourceEntry, parentEntry, content } = resource - if( state === 'success') { + if (state === 'success') { this.manifestData.resources[resourceEntry.id] = resourceEntry - this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry ) - if( parentEntry ) this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', parentEntry ) - this.fairCopyApplication.sendToAllWindows('resourceContentUpdated', { resourceID: resourceEntry.id, messageID: 'check-out-messsage', resourceContent: content }) - } + this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry) + if (parentEntry) this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', parentEntry) + this.fairCopyApplication.sendToAllWindows('resourceContentUpdated', { resourceID: resourceEntry.id, messageID: 'check-out-messsage', resourceContent: content }) + } checkOutStatus.push({ state, resourceEntry }) } const idMap = this.fairCopyApplication.fairCopySession.idMapAuthority.checkOut(resources) this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) - this.saveManifest() + this.saveManifest() - this.fairCopyApplication.sendToMainWindow('checkOutResults', checkOutStatus, error ) + this.fairCopyApplication.sendToMainWindow('checkOutResults', checkOutStatus, error) this.fairCopyApplication.fairCopySession.requestResourceView() } checkInResults(resourceStatus, error) { const { userID } = this.manifestData const resourceEntries = [] - for( const resourceID of Object.keys(resourceStatus) ) { + for (const resourceID of Object.keys(resourceStatus)) { const resourceEntry = this.manifestData.resources[resourceID] - if( !error ) { + if (!error) { // remove remote resources from project file and manifest, update all windows - this.projectArchiveWorker.postMessage({ messageType: 'remove-file', fileID: resourceID }) + this.projectArchiveWorker.postMessage({ messageType: 'remove-file', fileID: resourceID }) resourceEntry.local = false resourceEntry.lastAction = { action_type: 'check_in', user: { id: userID } } - this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry ) - delete this.manifestData.resources[resourceID] + this.fairCopyApplication.sendToAllWindows('resourceEntryUpdated', resourceEntry) + delete this.manifestData.resources[resourceID] } resourceEntries.push(resourceEntry) } - if( !error ) { - const idMap = this.fairCopyApplication.fairCopySession.idMapAuthority.checkIn(resourceEntries) - this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) + if (!error) { + const idMap = this.fairCopyApplication.fairCopySession.idMapAuthority.checkIn(resourceEntries) + this.projectArchiveWorker.postMessage({ messageType: 'write-file', fileID: idMapEntryName, data: idMap }) this.saveManifest() this.fairCopyApplication.fairCopySession.requestResourceView() } - this.fairCopyApplication.sendToMainWindow('checkInResults', {resourceEntries, resourceStatus, error} ) + this.fairCopyApplication.sendToMainWindow('checkInResults', { resourceEntries, resourceStatus, error }) } // send a list of the local resources to main window sendLocalResources() { - const resourceEntries = Object.values( this.manifestData.resources ) - + const resourceEntries = Object.values(this.manifestData.resources) const localResources = {} - for( const resourceEntry of resourceEntries ) { - if( resourceEntry.type !== 'image') { + for (const resourceEntry of resourceEntries) { + if (resourceEntry.type !== 'image') { localResources[resourceEntry.id] = resourceEntry } + + if (resourceEntry.type === 'standOff') { + this.projectArchiveWorker.postMessage({ messageType: 'parse-standoff', resourceID: resourceEntry.id, parentResourceID: resourceEntry.parentResource }) + } } - this.fairCopyApplication.sendToAllWindows('localResources', localResources ) + this.fairCopyApplication.sendToAllWindows('localResources', localResources) } } -function getExtensionForMIMEType( mimeType ) { - switch(mimeType) { +function getExtensionForMIMEType(mimeType) { + switch (mimeType) { case 'image/png': return 'png' case 'image/jpeg': return 'jpg' case 'image/gif': - return 'gif' + return 'gif' default: throw new Error(`Unknown MIMEType: ${mimeType}`) - } + } } exports.ProjectStore = ProjectStore diff --git a/src/render/components/main-window/MainWindow.jsx b/src/render/components/main-window/MainWindow.jsx index 53c0a373..70c3b01c 100644 --- a/src/render/components/main-window/MainWindow.jsx +++ b/src/render/components/main-window/MainWindow.jsx @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import { debounce } from "debounce"; +import { debounce } from 'debounce' import ProjectSidebar from './ProjectSidebar' import AlertDialog from './dialogs/AlertDialog' @@ -21,12 +21,16 @@ import ReleaseNotesDialog from './dialogs/ReleaseNotesDialog' import EditorDraggingElement from './tei-editor/EditorDraggingElement' import ImportTextsDialog from './dialogs/ImportTextsDialog' import ImportConsoleDialog from './dialogs/ImportConsoleDialog' -import { highlightSearchResults, scrollToSearchResult, searchResource } from '../../model/search' +import { + highlightSearchResults, + scrollToSearchResult, + searchResource, +} from '../../model/search' import SearchDialog from './dialogs/SearchDialog' import CheckInDialog from './dialogs/CheckInDialog' import CheckOutDialog from './dialogs/CheckOutDialog' import { bigRingSpinner } from '../common/ring-spinner' -import {SplitPaneView} from '../common/SplitPaneView' +import { SplitPaneView } from '../common/SplitPaneView' const fairCopy = window.fairCopy @@ -36,1034 +40,1467 @@ const resizeRefreshRate = 100 const resetSearchRate = 100 const initialRowsPerPage = 50 -const closePopUpState = { popupMenuOptions: null, popupMenuAnchorEl: null, popupMenuPlacement: null } +const closePopUpState = { + popupMenuOptions: null, + popupMenuAnchorEl: null, + popupMenuPlacement: null, +} export default class MainWindow extends Component { - - constructor() { - super() - - this.filterInitialState = { orderBy: 'name', order: 'ascending', nameFilter: null, rowsPerPage: initialRowsPerPage } - this.state = { - selectedResource: null, - openResources: {}, - resourceViews: { - currentView: 'home', - remote: { - indexParentID: null, - parentEntry: null, - totalRows: 0, - currentPage: 1, - ...this.filterInitialState, - loading: true - }, - home: { - indexParentID: null, - parentEntry: null, - totalRows: 0, - currentPage: 1, - ...this.filterInitialState, - loading: true - } - }, - allResourcesCheckmarked: false, - resourceCheckmarks: {}, - resourceIndex: [], - requestedResources: [], - localResources: [], - resourceBrowserOpen: true, - alertDialogMode: 'closed', - alertOptions: null, - exitOnClose: false, - editDialogMode: false, - addImagesMode: false, - releaseNotesMode: false, - loginMode: false, - draggingElementActive: false, - dragInfo: null, - editSurfaceInfoMode: false, - moveResourceMode: false, - checkInMode: false, - checkInResources: [], - editTEIDocDialogMode: false, - moveResources: null, - surfaceInfo: null, - popupMenuOptions: null, - popupMenuAnchorEl: null, - popupMenuPlacement: null, - alertMessage: null, - iiifDialogMode: false, - textImportDialogMode: false, - checkOutMode: false, - checkOutStatus: null, - checkOutError: null, - searchQuery: null, - searchResults: {}, - searchFilterOptions: { active: false, elementName: '', attrQs: []}, - searchSelectionIndex: 0, - searchFilterMode: false, - searchEnabled: false, - searchScope: 'file', - showSearchBar: false, - leftPaneWidth: initialLeftPaneWidth, - rightPaneWidth: initialRightPaneWidth - } + constructor() { + super() + + this.filterInitialState = { + orderBy: 'name', + order: 'ascending', + nameFilter: null, + rowsPerPage: initialRowsPerPage, } - - onResourceOpened = (event, resourceData) => { - const { fairCopyProject } = this.props - const { openResources, requestedResources } = this.state - const { resourceEntry, parentEntry, resource } = resourceData - - // if this is a resource we asked for, then open it. - // (resources can also be asked for by FacsDocument, maybe others in future) - if( requestedResources.includes( resourceEntry.id ) ) { - const nextRequestedResources = requestedResources.filter( r => r === resourceEntry.id ) - const doc = fairCopyProject.onResourceOpened(resourceEntry, parentEntry, resource) - if( doc ) { - const nextOpenResources = { ...openResources } - nextOpenResources[resourceEntry.id] = doc - this.setState( { - ...this.state, - openResources: nextOpenResources, - requestedResources: nextRequestedResources - }) - } else { - this.setState( { - ...this.state, - requestedResources: nextRequestedResources - }) - } - } + this.state = { + selectedResource: null, + openResources: {}, + resourceViews: { + currentView: 'home', + remote: { + indexParentID: null, + parentEntry: null, + totalRows: 0, + currentPage: 1, + ...this.filterInitialState, + loading: true, + }, + home: { + indexParentID: null, + parentEntry: null, + totalRows: 0, + currentPage: 1, + ...this.filterInitialState, + loading: true, + }, + }, + allResourcesCheckmarked: false, + resourceCheckmarks: {}, + resourceIndex: [], + requestedResources: [], + localResources: [], + annotationData: {}, + resourceBrowserOpen: true, + alertDialogMode: 'closed', + alertOptions: null, + exitOnClose: false, + editDialogMode: false, + addImagesMode: false, + releaseNotesMode: false, + loginMode: false, + draggingElementActive: false, + dragInfo: null, + editSurfaceInfoMode: false, + moveResourceMode: false, + checkInMode: false, + checkInResources: [], + editTEIDocDialogMode: false, + moveResources: null, + surfaceInfo: null, + popupMenuOptions: null, + popupMenuAnchorEl: null, + popupMenuPlacement: null, + alertMessage: null, + iiifDialogMode: false, + textImportDialogMode: false, + checkOutMode: false, + checkOutStatus: null, + checkOutError: null, + searchQuery: null, + searchResults: {}, + searchFilterOptions: { active: false, elementName: '', attrQs: [] }, + searchSelectionIndex: 0, + searchFilterMode: false, + searchEnabled: false, + searchScope: 'file', + showSearchBar: false, + leftPaneWidth: initialLeftPaneWidth, + rightPaneWidth: initialRightPaneWidth, } - - onRequestExitApp = () => this.requestExitApp() - onSearchSystemStatus = (event, status ) => { - if( status !== this.state.searchEnabled ) { - this.setState({...this.state, searchEnabled: status }) - } + } + + onResourceOpened = (event, resourceData) => { + const { fairCopyProject } = this.props + const { openResources, requestedResources } = this.state + const { resourceEntry, parentEntry, resource } = resourceData + + // if this is a resource we asked for, then open it. + // (resources can also be asked for by FacsDocument, maybe others in future) + if (requestedResources.includes(resourceEntry.id)) { + const nextRequestedResources = requestedResources.filter( + (r) => r === resourceEntry.id + ) + const doc = fairCopyProject.onResourceOpened( + resourceEntry, + parentEntry, + resource + ) + if (doc) { + const nextOpenResources = { ...openResources } + nextOpenResources[resourceEntry.id] = doc + this.setState({ + ...this.state, + openResources: nextOpenResources, + requestedResources: nextRequestedResources, + }) + } else { + this.setState({ + ...this.state, + requestedResources: nextRequestedResources, + }) + } } - - setResourceCheckmark = (resourceEntry, checked, stateUpdate=true) => { - const { resourceCheckmarks } = this.state - const nextCheckmarks = { ...resourceCheckmarks } + } + + onAnnotationDataReceived = (event, info) => { + const { annotationData: data, parentResourceID } = info + const annotationData = { ...this.state.annotationData } + annotationData[parentResourceID] = data + this.setState({ + ...this.state, + annotationData, + }) + } + + onRequestExitApp = () => this.requestExitApp() + onSearchSystemStatus = (event, status) => { + if (status !== this.state.searchEnabled) { + this.setState({ ...this.state, searchEnabled: status }) + } + } + + setResourceCheckmark = (resourceEntry, checked, stateUpdate = true) => { + const { resourceCheckmarks } = this.state + const nextCheckmarks = { ...resourceCheckmarks } + nextCheckmarks[resourceEntry.id] = checked ? resourceEntry : null + if (stateUpdate) + this.setState({ ...this.state, resourceCheckmarks: nextCheckmarks }) + return nextCheckmarks + } + + setAllCheckmarks = (checked, stateUpdate = true) => { + const { resourceCheckmarks, resourceIndex } = this.state + const nextCheckmarks = { ...resourceCheckmarks } + for (const resourceEntry of resourceIndex) { + if (resourceEntry.type !== 'header') { nextCheckmarks[resourceEntry.id] = checked ? resourceEntry : null - if(stateUpdate) this.setState({...this.state, resourceCheckmarks: nextCheckmarks }) - return nextCheckmarks + } } - - setAllCheckmarks = ( checked, stateUpdate=true ) => { - const { resourceCheckmarks, resourceIndex } = this.state - const nextCheckmarks = { ...resourceCheckmarks } - for( const resourceEntry of resourceIndex ) { - if( resourceEntry.type !== 'header' ) { - nextCheckmarks[resourceEntry.id] = checked ? resourceEntry : null - } - } - if(stateUpdate) this.setState({...this.state, resourceCheckmarks: nextCheckmarks, allResourcesCheckmarked: checked }) - else return { resourceCheckmarks: nextCheckmarks, allResourcesCheckmarked: checked } + if (stateUpdate) + this.setState({ + ...this.state, + resourceCheckmarks: nextCheckmarks, + allResourcesCheckmarked: checked, + }) + else + return { + resourceCheckmarks: nextCheckmarks, + allResourcesCheckmarked: checked, + } + } + + onResourceViewUpdate = (event, resourceData) => { + const { resourceViews } = this.state + const { currentView } = resourceViews + const resourceView = resourceViews[currentView] + const { + resourceViews: nextResourceViews, + resourceIndex: nextResourceIndex, + } = resourceData + const { currentView: nextCurrentView } = nextResourceViews + const nextResourceView = nextResourceViews[nextCurrentView] + + // if the indexParentID or the currentView changed, clear checkmarks + if ( + currentView !== nextCurrentView || + resourceView.indexParentID !== nextResourceView.indexParentID + ) { + const checkmarkState = this.setAllCheckmarks(false, false) + this.setState({ + ...this.state, + ...checkmarkState, + resourceViews: nextResourceViews, + resourceIndex: nextResourceIndex, + }) + } else { + this.setState({ + ...this.state, + resourceViews: nextResourceViews, + resourceIndex: nextResourceIndex, + }) } - - onResourceViewUpdate = (event, resourceData ) => { - const { resourceViews } = this.state - const { currentView } = resourceViews - const resourceView = resourceViews[currentView] - const { resourceViews: nextResourceViews, resourceIndex: nextResourceIndex } = resourceData - const { currentView: nextCurrentView } = nextResourceViews - const nextResourceView = nextResourceViews[nextCurrentView] - - // if the indexParentID or the currentView changed, clear checkmarks - if( currentView !== nextCurrentView || resourceView.indexParentID !== nextResourceView.indexParentID ) { - const checkmarkState = this.setAllCheckmarks(false,false) - this.setState({...this.state, ...checkmarkState, resourceViews: nextResourceViews, resourceIndex: nextResourceIndex }) - } else { - this.setState({...this.state, resourceViews: nextResourceViews, resourceIndex: nextResourceIndex }) - } + } + + onCheckOutResults = (event, checkOutStatus, checkOutError) => { + this.setState({ + ...this.state, + checkOutMode: true, + checkOutStatus, + checkOutError, + }) + } + + onResourceEntryUpdated = (e, resourceEntry) => { + const { fairCopyProject } = this.props + fairCopyProject.notifyListeners('resourceEntryUpdated', resourceEntry) + this.refreshWindow() + } + + onResourceContentUpdated = (e, resourceUpdate) => { + const { fairCopyProject } = this.props + fairCopyProject.notifyListeners('resourceContentUpdated', resourceUpdate) + this.refreshWindow() + } + + onUpdateProjectInfo = (e, projectInfo) => { + const { fairCopyProject } = this.props + fairCopyProject.updateProjectInfo(projectInfo) + } + + componentDidMount() { + fairCopy.ipcRegisterCallback('resourceOpened', this.onResourceOpened) + fairCopy.ipcRegisterCallback( + 'resourceViewUpdate', + this.onResourceViewUpdate + ) + fairCopy.ipcRegisterCallback('requestExitApp', this.onRequestExitApp) + fairCopy.ipcRegisterCallback( + 'searchSystemStatus', + this.onSearchSystemStatus + ) + fairCopy.ipcRegisterCallback('checkOutResults', this.onCheckOutResults) + fairCopy.ipcRegisterCallback( + 'resourceEntryUpdated', + this.onResourceEntryUpdated + ) + fairCopy.ipcRegisterCallback( + 'resourceContentUpdated', + this.onResourceContentUpdated + ) + fairCopy.ipcRegisterCallback('updateProjectInfo', this.onUpdateProjectInfo) + fairCopy.ipcRegisterCallback('localResources', this.onLocalResources) + fairCopy.ipcRegisterCallback( + 'annotationDataReceived', + this.onAnnotationDataReceived + ) + } + + componentWillUnmount() { + fairCopy.ipcRemoveListener('resourceOpened', this.onResourceOpened) + fairCopy.ipcRemoveListener('resourceViewUpdate', this.onResourceViewUpdate) + fairCopy.ipcRemoveListener('requestExitApp', this.onRequestExitApp) + fairCopy.ipcRemoveListener('searchSystemStatus', this.onSearchSystemStatus) + fairCopy.ipcRemoveListener('checkOutResults', this.onCheckOutResults) + fairCopy.ipcRemoveListener( + 'resourceEntryUpdated', + this.onResourceEntryUpdated + ) + fairCopy.ipcRemoveListener( + 'resourceContentUpdated', + this.onResourceContentUpdated + ) + fairCopy.ipcRemoveListener('updateProjectInfo', this.onUpdateProjectInfo) + fairCopy.ipcRemoveListener('localResources', this.onLocalResources) + } + + refreshWindow() { + const { selectedResource, openResources } = this.state + + if (selectedResource) { + const resource = openResources[selectedResource] + if (resource instanceof TEIDocument && resource.editorView) { + this.setState({ ...this.state }) + } } - - onCheckOutResults = (event, checkOutStatus, checkOutError ) => { - this.setState({ ...this.state, checkOutMode: true, checkOutStatus, checkOutError }) + } + + requestExitApp = () => { + const { openResources } = this.state + const resourceIDs = Object.keys(openResources) + if (resourceIDs.length > 0) { + this.closeResources(resourceIDs, true) + } else { + fairCopy.ipcSend('exitApp') } + } - onResourceEntryUpdated = (e, resourceEntry) => { - const { fairCopyProject } = this.props - fairCopyProject.notifyListeners( 'resourceEntryUpdated', resourceEntry) - this.refreshWindow() + isResourceOpen(resourceEntries) { + const { openResources } = this.state + for (const resourceEntry of resourceEntries) { + if (openResources[resourceEntry.id]) { + return true + } } - - onResourceContentUpdated = (e, resourceUpdate) => { - const { fairCopyProject } = this.props - fairCopyProject.notifyListeners( 'resourceContentUpdated', resourceUpdate) - this.refreshWindow() + return false + } + + selectTEIDoc(teiDocID) { + const { resourceViews, resourceIndex } = this.state + const { currentView } = resourceViews + const currentParentEntry = resourceViews[currentView].parentEntry + + let teiDocEntry + if (currentParentEntry && teiDocID === currentParentEntry.id) { + teiDocEntry = currentParentEntry + } else { + teiDocEntry = resourceIndex.find( + (resourceEntry) => + resourceEntry.id === teiDocID && resourceEntry.type === 'teidoc' + ) } - - onUpdateProjectInfo = ( e, projectInfo ) => { - const { fairCopyProject } = this.props - fairCopyProject.updateProjectInfo( projectInfo ) + if (!teiDocEntry) return false + + const indexParentID = teiDocEntry.id + const parentEntry = teiDocEntry + const currentPage = 1 + const resourceViewRequest = { + currentView, + indexParentID, + parentEntry, + currentPage, + ...this.filterInitialState, } - - componentDidMount() { - fairCopy.ipcRegisterCallback('resourceOpened', this.onResourceOpened ) - fairCopy.ipcRegisterCallback('resourceViewUpdate', this.onResourceViewUpdate ) - fairCopy.ipcRegisterCallback('requestExitApp', this.onRequestExitApp ) - fairCopy.ipcRegisterCallback('searchSystemStatus', this.onSearchSystemStatus ) - fairCopy.ipcRegisterCallback('checkOutResults', this.onCheckOutResults ) - fairCopy.ipcRegisterCallback('resourceEntryUpdated', this.onResourceEntryUpdated ) - fairCopy.ipcRegisterCallback('resourceContentUpdated', this.onResourceContentUpdated ) - fairCopy.ipcRegisterCallback('updateProjectInfo', this.onUpdateProjectInfo ) - fairCopy.ipcRegisterCallback('localResources', this.onLocalResources ) + fairCopy.ipcSend('requestResourceView', resourceViewRequest) + + const nextResourceViews = { ...resourceViews } + const currentResourceView = resourceViews[currentView] + nextResourceViews[currentView] = { + ...currentResourceView, + indexParentID, + parentEntry, + currentPage, + loading: true, } - componentWillUnmount() { - fairCopy.ipcRemoveListener('resourceOpened', this.onResourceOpened ) - fairCopy.ipcRemoveListener('resourceViewUpdate', this.onResourceViewUpdate ) - fairCopy.ipcRemoveListener('requestExitApp', this.onRequestExitApp ) - fairCopy.ipcRemoveListener('searchSystemStatus', this.onSearchSystemStatus ) - fairCopy.ipcRemoveListener('checkOutResults', this.onCheckOutResults ) - fairCopy.ipcRemoveListener('resourceEntryUpdated', this.onResourceEntryUpdated ) - fairCopy.ipcRemoveListener('resourceContentUpdated', this.onResourceContentUpdated ) - fairCopy.ipcRemoveListener('updateProjectInfo', this.onUpdateProjectInfo ) - fairCopy.ipcRemoveListener('localResources', this.onLocalResources ) + const nextResourceIndex = currentView === 'home' ? resourceIndex : [] + + const checkmarkState = this.setAllCheckmarks(false, false) + const nextState = { ...this.state, ...checkmarkState } + + this.setState({ + ...nextState, + selectedResource: null, + resourceBrowserOpen: true, + resourceViews: nextResourceViews, + resourceIndex: nextResourceIndex, + ...closePopUpState, + }) + return true + } + + selectResources(resourceIDs) { + const { fairCopyProject } = this.props + const { + openResources, + selectedResource, + requestedResources, + resourceIndex, + } = this.state + + let nextSelection = resourceIDs[0] + + if (this.selectTEIDoc(nextSelection)) { + return } - - refreshWindow() { - const { selectedResource, openResources } = this.state - - if( selectedResource ) { - const resource = openResources[selectedResource] - if( resource instanceof TEIDocument && resource.editorView ) { - this.setState({...this.state}) - } - } + let change = selectedResource !== nextSelection + const nextRequestedResources = [...requestedResources] + + for (const resourceID of resourceIDs) { + if (!openResources[resourceID]) { + nextRequestedResources.push(resourceID) + fairCopyProject.openResource(resourceID) + } } - requestExitApp = () => { - const { openResources } = this.state - const resourceIDs = Object.keys( openResources ) - if( resourceIDs.length > 0 ) { - this.closeResources( resourceIDs, true) - } else { - fairCopy.ipcSend('exitApp') - } + if (change) { + this.setState({ + ...this.state, + selectedResource: nextSelection, + resourceBrowserOpen: false, + requestedResources: nextRequestedResources, + currentSubmenuID: 0, + searchSelectionIndex: 0, + popupMenuOptions: null, + popupMenuAnchorEl: null, + popupMenuPlacement: null, + }) + const nextResource = openResources[nextSelection] + if (nextResource instanceof TEIDocument) { + const { searchQuery, searchResults } = this.state + this.refreshWhenReady(searchQuery, searchResults, false) + } + } else { + this.setState({ + ...this.state, + resourceBrowserOpen: false, + requestedResources: nextRequestedResources, + popupMenuOptions: null, + popupMenuAnchorEl: null, + popupMenuPlacement: null, + }) } - - isResourceOpen( resourceEntries ) { - const { openResources } = this.state - for( const resourceEntry of resourceEntries ) { - if( openResources[resourceEntry.id] ) { - return true - } + } + + closeResources = (resourceIDs, exitOnClose = false, promptSave = true) => { + const { fairCopyProject } = this.props + const { openResources, selectedResource, resourceBrowserOpen } = this.state + + if (promptSave) { + for (const resourceID of resourceIDs) { + const resource = openResources[resourceID] + if (resource && resource.changedSinceLastSave) { + const alertOptions = { + resource, + resourceIDs, + } + this.setState({ + ...this.state, + exitOnClose, + alertDialogMode: 'confirmSave', + alertOptions, + }) + return } - return false + } } - selectTEIDoc( teiDocID ) { - const {resourceViews, resourceIndex} = this.state - const {currentView} = resourceViews - const currentParentEntry = resourceViews[currentView].parentEntry - - let teiDocEntry - if( currentParentEntry && teiDocID === currentParentEntry.id ) { - teiDocEntry = currentParentEntry - } else { - teiDocEntry = resourceIndex.find(resourceEntry => resourceEntry.id === teiDocID && resourceEntry.type === 'teidoc') - } - if(!teiDocEntry) return false - - const indexParentID = teiDocEntry.id - const parentEntry = teiDocEntry - const currentPage = 1 - const resourceViewRequest = { currentView, indexParentID, parentEntry, currentPage, ...this.filterInitialState } - fairCopy.ipcSend('requestResourceView', resourceViewRequest ) - - const nextResourceViews = { ...resourceViews } - const currentResourceView = resourceViews[currentView] - nextResourceViews[currentView] = { ...currentResourceView, indexParentID, parentEntry, currentPage, loading: true } - - const nextResourceIndex = currentView === 'home' ? resourceIndex : [] - - const checkmarkState = this.setAllCheckmarks(false,false) - const nextState = { ...this.state, ...checkmarkState } - - this.setState({ - ...nextState, - selectedResource: null, - resourceBrowserOpen: true, - resourceViews: nextResourceViews, - resourceIndex: nextResourceIndex, - ...closePopUpState - }) - return true + let nextResourceArr = [] + for (const openResourceID of Object.keys(openResources)) { + if (!resourceIDs.find((id) => id === openResourceID)) { + // this id is not on the close list + nextResourceArr.push(openResources[openResourceID]) + } else { + // closing this resource + fairCopyProject.onResourceClosed(openResources[openResourceID]) + } } - selectResources(resourceIDs) { - const { fairCopyProject } = this.props - const { openResources, selectedResource, requestedResources, resourceIndex } = this.state - - let nextSelection = resourceIDs[0] + let nextSelection, + nextBrowserOpen, + nextResources = {} + if (nextResourceArr.length > 0) { + for (const resource of nextResourceArr) { + nextResources[resource.resourceID] = resource + } + nextSelection = resourceIDs.find((id) => id === selectedResource) + ? nextResourceArr[0].resourceID + : selectedResource + nextBrowserOpen = resourceBrowserOpen + } else { + nextSelection = null + nextBrowserOpen = true + } - if( this.selectTEIDoc(nextSelection) ) { - return + this.setState({ + ...this.state, + selectedResource: nextSelection, + openResources: nextResources, + resourceBrowserOpen: nextBrowserOpen, + alertDialogMode: 'closed', + alertOptions: null, + popupMenuOptions: null, + popupMenuAnchorEl: null, + popupMenuPlacement: null, + }) + + if (exitOnClose) { + fairCopy.ipcSend('exitApp') + } + } + + // a bit of a hack - need to refresh after it renders + refreshWhenReady(searchQuery, searchResults, openToSearchResult) { + setTimeout(() => { + const { selectedResource, openResources } = this.state + const resource = openResources[selectedResource] + if (resource && resource.getActiveView()) { + this.updateSearchResults(resource, searchQuery, searchResults) + resource.refreshView() + if (openToSearchResult) { + scrollToSearchResult(resource, 0) } - let change = (selectedResource !== nextSelection) - const nextRequestedResources = [ ...requestedResources ] - - for( const resourceID of resourceIDs ) { - if( !openResources[resourceID] ) { - nextRequestedResources.push(resourceID) - fairCopyProject.openResource(resourceID) - } + } else { + this.refreshWhenReady(searchQuery, searchResults, openToSearchResult) + } + }, 60) + } + + saveResources(resourceIDs) { + const { openResources, selectedResource } = this.state + for (const resourceID of resourceIDs) { + const resource = openResources[resourceID] + if (resource && resource.changedSinceLastSave) { + resource.save() + if (resource.resourceID === selectedResource) resource.refreshView() + } + } + } + + checkInResources(checkInResources) { + this.setState({ + ...this.state, + checkInMode: true, + checkInResources, + ...closePopUpState, + }) + } + + checkOutResources(resourceEntries) { + const { fairCopyProject } = this.props + const { userID, serverURL, projectID } = fairCopyProject + fairCopy.ipcSend('checkOut', userID, serverURL, projectID, resourceEntries) + } + + onLocalResources = (event, localResources) => { + this.setState({ ...this.state, localResources }) + } + + onOpenPopupMenu = ( + popupMenuOptions, + popupMenuAnchorEl, + popupMenuPlacement + ) => { + this.setState({ + ...this.state, + popupMenuOptions, + popupMenuAnchorEl, + popupMenuPlacement, + }) + } + + onClosePopupMenu = () => { + this.setState({ ...this.state, ...closePopUpState }) + } + + onLogin = () => { + this.setState({ ...this.state, loginMode: true }) + } + + onLoggedIn = () => { + const { resourceViews } = this.state + const nextResourceViews = { ...resourceViews } + nextResourceViews['remote'].loading = true + this.setState({ + ...this.state, + resourceViews: nextResourceViews, + loginMode: false, + }) + fairCopy.ipcSend('reopenProject') + } + + onEditResource = () => { + this.setState({ ...this.state, editDialogMode: true }) + } + + onImportResource = (importType) => { + if (importType === 'xml') { + this.setState({ ...this.state, textImportDialogMode: true }) + } else { + this.setState({ ...this.state, iiifDialogMode: true }) + } + } + + onAddImages = () => { + this.setState({ ...this.state, addImagesMode: true }) + } + + onAlertMessage = (message) => { + this.setState({ ...this.state, alertMessage: message, ...closePopUpState }) + } + + onEditSurfaceInfo = (surfaceInfo) => { + this.setState({ + ...this.state, + surfaceInfo: surfaceInfo, + editSurfaceInfoMode: true, + }) + } + + onDragElement = (elementID, clientOffset, startingPoint, dragTarget) => { + const dragInfo = { elementID, clientOffset, dragTarget, startingPoint } + this.setState({ ...this.state, draggingElementActive: true, dragInfo }) + } + + onResourceViewChange = (nextView) => { + const { resourceViews } = this.state + const { currentView } = resourceViews + const resourceView = resourceViews[currentView] + const { indexParentID, parentEntry } = resourceView + const resourceViewRequest = { + currentView, + indexParentID, + parentEntry, + ...nextView, + } + fairCopy.ipcSend('requestResourceView', resourceViewRequest) + const checkMarkState = this.setAllCheckmarks(false, false) + const nextResourceViews = { ...resourceViews } + nextResourceViews[currentView].loading = true + this.setState({ + ...this.state, + ...checkMarkState, + resourceViews: nextResourceViews, + }) + } + + onResourceAction = (actionID, resourceIDs, resourceEntries) => { + // all actions that use nextState clear the checkmarks + const checkmarkState = this.setAllCheckmarks(false, false) + const nextState = { ...this.state, ...checkmarkState } + + switch (actionID) { + case 'open': + this.selectResources(resourceIDs) + break + case 'open-search-result': + this.selectResources(resourceIDs, true) + break + case 'check-in': + { + // don't check in if there are unsaved files being committed + const { openResources } = this.state + for (const resourceID of resourceIDs) { + const openResource = openResources[resourceID] + if (openResource && openResource.changedSinceLastSave) { + this.onAlertMessage( + 'You must save all files that are being checked in.' + ) + return + } + } + this.setState({ + ...nextState, + checkInMode: true, + checkInResources: resourceIDs, + ...closePopUpState, + }) } - - if( change ) { + break + case 'check-out': + this.checkOutResources(resourceEntries) + this.setState({ ...nextState, ...closePopUpState }) + break + case 'close': + this.closeResources(resourceIDs) + break + case 'remote': + { + const { resourceViews } = this.state + if ( + resourceViews.currentView === 'home' && + !resourceViews.home.loading + ) { + const nextResourceViews = { ...resourceViews } + nextResourceViews.currentView = 'remote' + nextResourceViews.remote.loading = true + const { indexParentID, parentEntry, currentPage } = + resourceViews.remote + const resourceViewRequest = { + currentView: 'remote', + indexParentID, + parentEntry, + currentPage, + ...this.filterInitialState, + } + fairCopy.ipcSend('requestResourceView', resourceViewRequest) this.setState({ - ...this.state, - selectedResource: nextSelection, - resourceBrowserOpen: false, - requestedResources: nextRequestedResources, - currentSubmenuID: 0, - searchSelectionIndex: 0, - popupMenuOptions: null, - popupMenuAnchorEl: null, - popupMenuPlacement: null - + ...nextState, + selectedResource: null, + resourceBrowserOpen: true, + resourceViews: nextResourceViews, + resourceIndex: [], }) - const nextResource = openResources[nextSelection] - if( nextResource instanceof TEIDocument ) { - const { searchQuery, searchResults } = this.state - this.refreshWhenReady(searchQuery, searchResults, false) - } - } else { - this.setState( { - ...this.state, - resourceBrowserOpen: false, - requestedResources: nextRequestedResources, - popupMenuOptions: null, - popupMenuAnchorEl: null, - popupMenuPlacement: null - }) + } } - } - - closeResources = (resourceIDs,exitOnClose=false,promptSave=true) => { - const { fairCopyProject } = this.props - const { openResources, selectedResource, resourceBrowserOpen } = this.state - - if( promptSave ) { - for( const resourceID of resourceIDs ) { - const resource = openResources[resourceID] - if( resource && resource.changedSinceLastSave ) { - const alertOptions = { - resource, resourceIDs - } - this.setState({ ...this.state, exitOnClose, alertDialogMode: 'confirmSave', alertOptions }) - return - } - } - } - - let nextResourceArr = [] - for( const openResourceID of Object.keys(openResources) ) { - if( !resourceIDs.find(id => id === openResourceID) ) { - // this id is not on the close list - nextResourceArr.push(openResources[openResourceID]) - } else { - // closing this resource - fairCopyProject.onResourceClosed(openResources[openResourceID]) + break + case 'home': + { + const { resourceViews } = this.state + if ( + resourceViews.currentView === 'remote' && + !resourceViews.remote.loading + ) { + const { indexParentID, parentEntry, currentPage } = + resourceViews.home + const resourceViewRequest = { + currentView: 'home', + indexParentID, + parentEntry, + currentPage, + ...this.filterInitialState, } + fairCopy.ipcSend('requestResourceView', resourceViewRequest) + const nextResourceViews = { ...resourceViews } + nextResourceViews.currentView = 'home' + nextResourceViews.home.loading = true + this.setState({ + ...nextState, + selectedResource: null, + resourceBrowserOpen: true, + resourceViews: nextResourceViews, + }) + } } - - let nextSelection, nextBrowserOpen, nextResources = {} - if( nextResourceArr.length > 0 ) { - for( const resource of nextResourceArr ) { - nextResources[resource.resourceID] = resource - } - nextSelection = resourceIDs.find(id => id === selectedResource) ? nextResourceArr[0].resourceID : selectedResource - nextBrowserOpen = resourceBrowserOpen + break + case 'root': + { + const { resourceViews, resourceIndex } = this.state + const { currentView } = resourceViews + const currentResourceView = resourceViews[currentView] + const { currentPage } = currentResourceView + const resourceViewRequest = { + currentView, + indexParentID: null, + parentEntry: null, + currentPage, + } + fairCopy.ipcSend('requestResourceView', resourceViewRequest) + const nextResourceViews = { ...resourceViews } + const nextResourceIndex = currentView === 'home' ? resourceIndex : [] + nextResourceViews[currentView] = { + ...currentResourceView, + indexParentID: null, + parentEntry: null, + loading: true, + } + this.setState({ + ...nextState, + selectedResource: null, + resourceBrowserOpen: true, + resourceViews: nextResourceViews, + resourceIndex: nextResourceIndex, + }) + } + break + case 'move': + if (this.isResourceOpen(resourceEntries)) { + this.onAlertMessage( + 'You must close open editor windows before moving a resource.' + ) + } else if (resourceEntries.find((r) => r.type === 'teidoc')) { + this.onAlertMessage('Cannot move TEIDocument type resources.') } else { - nextSelection = null - nextBrowserOpen = true + const { fairCopyProject } = this.props + const onMove = (movingItems, parentEntry) => { + fairCopyProject.moveResources(movingItems, parentEntry) + } + const moveResourceProps = { + resourceType: 'teidoc', + allowRoot: true, + movingItems: resourceEntries, + onMove, + } + this.setState({ + ...nextState, + moveResourceMode: true, + moveResourceProps, + ...closePopUpState, + }) } - - this.setState( { - ...this.state, - selectedResource: nextSelection, - openResources: nextResources, - resourceBrowserOpen: nextBrowserOpen, - alertDialogMode: 'closed', - alertOptions: null, - popupMenuOptions: null, - popupMenuAnchorEl: null, - popupMenuPlacement: null - }) - - if( exitOnClose ) { - fairCopy.ipcSend('exitApp') - } - } - - // a bit of a hack - need to refresh after it renders - refreshWhenReady( searchQuery, searchResults, openToSearchResult ) { - setTimeout( () => { - const { selectedResource, openResources } = this.state - const resource = openResources[selectedResource] - if( resource && resource.getActiveView() ) { - this.updateSearchResults(resource, searchQuery, searchResults) - resource.refreshView() - if( openToSearchResult ) { - scrollToSearchResult( resource, 0 ) - } - } else { - this.refreshWhenReady(searchQuery,searchResults, openToSearchResult) - } - }, 60 ) - } - - saveResources(resourceIDs) { - const { openResources, selectedResource } = this.state - for( const resourceID of resourceIDs ) { - const resource = openResources[resourceID] - if( resource && resource.changedSinceLastSave ) { - resource.save() - if( resource.resourceID === selectedResource ) resource.refreshView() - } + break + case 'save': + this.saveResources(resourceIDs) + break + case 'delete': + { + const { fairCopyProject } = this.props + const { openResources } = this.state + const alertOptions = { resourceIDs, openResources } + if (fairCopyProject.areEditable(resourceEntries)) { + this.setState({ + ...nextState, + alertDialogMode: 'confirmDelete', + alertOptions, + ...closePopUpState, + }) + } else { + this.onAlertMessage( + 'To delete a resource, you must first check it out.' + ) + } } - } - - checkInResources(checkInResources) { - this.setState({...this.state, checkInMode: true, checkInResources, ...closePopUpState }) - } - - checkOutResources(resourceEntries) { - const { fairCopyProject } = this.props - const { userID, serverURL, projectID } = fairCopyProject - fairCopy.ipcSend('checkOut', userID, serverURL, projectID, resourceEntries ) - } - - onLocalResources = (event,localResources) => { - this.setState({...this.state, localResources }) - } - - onOpenPopupMenu = (popupMenuOptions, popupMenuAnchorEl, popupMenuPlacement ) => { - this.setState({...this.state, popupMenuOptions, popupMenuAnchorEl, popupMenuPlacement }) - } - - onClosePopupMenu = () => { - this.setState({...this.state, ...closePopUpState }) - } - - onLogin = () => { - this.setState({...this.state, loginMode: true }) - } - - onLoggedIn = () => { - const { resourceViews } = this.state - const nextResourceViews = { ...resourceViews } - nextResourceViews['remote'].loading = true - this.setState( {...this.state, resourceViews: nextResourceViews, loginMode: false} ) - fairCopy.ipcSend('reopenProject') - } - - onEditResource = () => { - this.setState({...this.state, editDialogMode: true }) - } - - onImportResource = (importType) => { - if( importType === 'xml' ) { - this.setState({...this.state, textImportDialogMode: true }) - } else { - this.setState({...this.state, iiifDialogMode: true }) + break + case 'recover': + { + const { fairCopyProject } = this.props + fairCopyProject.recoverResources(resourceIDs) + this.setState({ ...nextState, ...closePopUpState }) } + break + case 'export': + fairCopy.ipcSend('requestExport', resourceEntries) + this.setState({ ...nextState, ...closePopUpState }) + break + default: + console.error(`Unrecognized resource action id: ${actionID}`) + break } - - onAddImages = () => { - this.setState({...this.state, addImagesMode: true }) - } - - onAlertMessage = (message) => { - this.setState({...this.state, alertMessage: message, ...closePopUpState }) - } - - onEditSurfaceInfo = (surfaceInfo) => { - this.setState( {...this.state, surfaceInfo: surfaceInfo, editSurfaceInfoMode: true} ) + } + + onSearchResults = ( + searchQuery, + searchResults, + popupMenuOptions, + searchBarEl + ) => { + const { selectedResource, openResources } = this.state + + if (selectedResource) { + const resource = openResources[selectedResource] + this.updateSearchResults(resource, searchQuery, searchResults) } - - onDragElement = (elementID, clientOffset, startingPoint, dragTarget) => { - const dragInfo = { elementID, clientOffset, dragTarget, startingPoint } - this.setState( {...this.state, draggingElementActive: true, dragInfo }) + if (popupMenuOptions.length === 0) { + this.setState({ + ...this.state, + searchQuery, + searchResults, + searchSelectionIndex: 0, + ...closePopUpState, + }) + } else { + const popupMenuPlacement = { vertical: 'top', horizontal: 'left' } + this.setState({ + ...this.state, + searchQuery, + searchResults, + searchSelectionIndex: 0, + popupMenuOptions, + popupMenuAnchorEl: searchBarEl, + popupMenuPlacement, + }) } - - onResourceViewChange = (nextView) => { - const { resourceViews } = this.state - const { currentView } = resourceViews - const resourceView = resourceViews[currentView] - const { indexParentID, parentEntry } = resourceView - const resourceViewRequest = { currentView, indexParentID, parentEntry, ...nextView } - fairCopy.ipcSend('requestResourceView', resourceViewRequest ) - const checkMarkState = this.setAllCheckmarks(false,false) - const nextResourceViews = { ...resourceViews } - nextResourceViews[currentView].loading = true - this.setState({...this.state, ...checkMarkState, resourceViews: nextResourceViews }) + } + + onSearchFilter = () => { + this.setState({ ...this.state, searchFilterMode: true }) + } + + onResetSearch = () => { + const resetSearch = debounce(() => { + this.setState({ + ...this.state, + searchQuery: '', + searchResults: {}, + searchSelectionIndex: 0, + }) + }, resetSearchRate) + resetSearch() + } + + updateSearchFilter = (elementName, attrQs, active, open) => { + const { searchQuery, searchScope, selectedResource, openResources } = + this.state + const searchFilterOptions = { elementName, attrQs, active } + const query = searchQuery ? searchQuery.query : '' + const searchQ = { query, elementName, attrQs } + + // run the search based on the new filter settings + if (searchScope === 'project') { + this.setState({ + ...this.state, + searchQuery: searchQ, + searchFilterOptions, + searchFilterMode: open, + }) + fairCopy.ipcSend('searchProject', searchQ) + } else { + const currentResource = selectedResource + ? openResources[selectedResource] + : null + if (currentResource) { + const searchResults = searchResource(currentResource, searchQ) + this.setState({ + ...this.state, + searchQuery: searchQ, + searchFilterOptions, + searchFilterMode: open, + }) + this.updateSearchResults(currentResource, searchQuery, searchResults) + } } - - onResourceAction = (actionID, resourceIDs, resourceEntries) => { - // all actions that use nextState clear the checkmarks - const checkmarkState = this.setAllCheckmarks(false,false) - const nextState = { ...this.state, ...checkmarkState } - - switch(actionID) { - case 'open': - this.selectResources(resourceIDs) - break - case 'open-search-result': - this.selectResources(resourceIDs, true) - break - case 'check-in': - { - // don't check in if there are unsaved files being committed - const { openResources } = this.state - for( const resourceID of resourceIDs ) { - const openResource = openResources[resourceID] - if( openResource && openResource.changedSinceLastSave ) { - this.onAlertMessage("You must save all files that are being checked in.") - return - } - } - this.setState({...nextState, checkInMode: true, checkInResources: resourceIDs, ...closePopUpState }) - } - break - case 'check-out': - this.checkOutResources(resourceEntries) - this.setState({...nextState, ...closePopUpState }) - break - case 'close': - this.closeResources(resourceIDs) - break - case 'remote': - { - const {resourceViews} = this.state - if( resourceViews.currentView === 'home' && !resourceViews.home.loading ) { - const nextResourceViews = { ...resourceViews } - nextResourceViews.currentView = 'remote' - nextResourceViews.remote.loading = true - const { indexParentID, parentEntry, currentPage } = resourceViews.remote - const resourceViewRequest = { currentView: 'remote', indexParentID, parentEntry, currentPage, ...this.filterInitialState } - fairCopy.ipcSend('requestResourceView', resourceViewRequest ) - this.setState({...nextState, selectedResource: null, resourceBrowserOpen: true, resourceViews: nextResourceViews, resourceIndex: [] }) - } - } - break - case 'home': - { - const {resourceViews} = this.state - if( resourceViews.currentView === 'remote' && !resourceViews.remote.loading ) { - const { indexParentID, parentEntry, currentPage } = resourceViews.home - const resourceViewRequest = { currentView: 'home', indexParentID, parentEntry, currentPage, ...this.filterInitialState } - fairCopy.ipcSend('requestResourceView', resourceViewRequest ) - const nextResourceViews = { ...resourceViews } - nextResourceViews.currentView = 'home' - nextResourceViews.home.loading = true - this.setState({...nextState, selectedResource: null, resourceBrowserOpen: true, resourceViews: nextResourceViews }) - } - } - break - case 'root': - { - const {resourceViews, resourceIndex } = this.state - const {currentView} = resourceViews - const currentResourceView = resourceViews[currentView] - const { currentPage } = currentResourceView - const resourceViewRequest = { currentView, indexParentID: null, parentEntry: null, currentPage } - fairCopy.ipcSend('requestResourceView', resourceViewRequest ) - const nextResourceViews = { ...resourceViews } - const nextResourceIndex = currentView === 'home' ? resourceIndex : [] - nextResourceViews[currentView] = { ...currentResourceView, indexParentID: null, parentEntry: null, loading: true } - this.setState({...nextState, selectedResource: null, resourceBrowserOpen: true, resourceViews: nextResourceViews, resourceIndex: nextResourceIndex }) - } - break - case 'move': - if( this.isResourceOpen( resourceEntries ) ) { - this.onAlertMessage("You must close open editor windows before moving a resource.") - } else if( resourceEntries.find( r => r.type === 'teidoc' ) ) { - this.onAlertMessage("Cannot move TEIDocument type resources.") - } else { - const { fairCopyProject } = this.props - const onMove = (movingItems, parentEntry)=>{ fairCopyProject.moveResources( movingItems, parentEntry ) } - const moveResourceProps = { resourceType: 'teidoc', allowRoot: true, movingItems: resourceEntries, onMove } - this.setState( {...nextState, moveResourceMode: true, moveResourceProps, ...closePopUpState} ) - } - break - case 'save': - this.saveResources(resourceIDs) - break - case 'delete': - { - const { fairCopyProject } = this.props - const { openResources } = this.state - const alertOptions = { resourceIDs, openResources } - if( fairCopyProject.areEditable( resourceEntries ) ) { - this.setState({ ...nextState, alertDialogMode: 'confirmDelete', alertOptions, ...closePopUpState }) - } else { - this.onAlertMessage("To delete a resource, you must first check it out.") - } - } - break - case 'recover': - { - const { fairCopyProject } = this.props - fairCopyProject.recoverResources(resourceIDs) - this.setState({...nextState, ...closePopUpState }) - } - break - case 'export': - fairCopy.ipcSend('requestExport', resourceEntries) - this.setState({...nextState, ...closePopUpState }) - break - default: - console.error(`Unrecognized resource action id: ${actionID}`) - break - } + } + + updateSearchResults(resource, searchQuery, searchResults) { + const { resourceID } = resource + const resourceSearchResults = searchResults[resourceID] + ? searchResults[resourceID] + : -1 + highlightSearchResults(resource, searchQuery, resourceSearchResults) + } + + closeSearchBar() { + const { selectedResource, openResources } = this.state + if (selectedResource) { + const resource = openResources[selectedResource] + highlightSearchResults(resource, null, -1) + resource.getActiveView().focus() } - - onSearchResults = ( searchQuery, searchResults, popupMenuOptions, searchBarEl) => { - const { selectedResource, openResources } = this.state - - if( selectedResource ) { - const resource = openResources[selectedResource] - this.updateSearchResults(resource, searchQuery, searchResults) + this.setState({ + ...this.state, + showSearchBar: false, + searchQuery: '', + searchResults: {}, + searchSelectionIndex: 0, + ...closePopUpState, + }) + } + + toggleSearchScope() { + const { searchScope } = this.state + const nextScope = searchScope === 'file' ? 'project' : 'file' + this.setState({ ...this.state, searchScope: nextScope }) + } + + renderEditors() { + const { + openResources, + selectedResource, + leftPaneWidth, + resourceViews, + annotationData, + } = this.state + const { fairCopyProject, onProjectSettings } = this.props + const { currentView } = resourceViews + + const editors = [] + let visible = false + for (const resource of Object.values(openResources)) { + const hidden = selectedResource !== resource.resourceID + if (!hidden) visible = true + const key = `editor-${resource.resourceID}` + const { resourceEntry, parentEntry } = resource + + const onSave = () => { + this.onResourceAction('save', [resource.resourceID]) + } + const onConfirmDeleteImages = (alertOptions) => { + this.setState({ + ...this.state, + alertDialogMode: 'confirmDeleteImages', + alertOptions, + }) + } + const onMoveSurfaces = (facsDocument, surfaces, onMoved) => { + const onMove = (movingItems, parentEntry) => { + facsDocument.moveSurfaces(movingItems, parentEntry, onMoved) } - if( popupMenuOptions.length === 0 ) { - this.setState({...this.state, searchQuery, searchResults, searchSelectionIndex: 0, ...closePopUpState }) - } else { - const popupMenuPlacement = { vertical: 'top', horizontal: 'left' } - this.setState({...this.state, searchQuery, searchResults, searchSelectionIndex: 0, popupMenuOptions, popupMenuAnchorEl: searchBarEl, popupMenuPlacement }) + const moveResourceProps = { + resourceType: 'facs', + allowRoot: false, + movingItems: surfaces, + onMove, + onMoved, } - } - - onSearchFilter = () => { - this.setState({...this.state, searchFilterMode: true }) - } - - onResetSearch = () => { - const resetSearch = debounce(() => { - this.setState({...this.state, searchQuery: '', searchResults: {}, searchSelectionIndex: 0 }) - }, resetSearchRate) - resetSearch() - } - - updateSearchFilter = ( elementName, attrQs, active, open ) => { - const { searchQuery, searchScope, selectedResource, openResources } = this.state - const searchFilterOptions = { elementName, attrQs, active } - const query = searchQuery ? searchQuery.query : "" - const searchQ = { query, elementName, attrQs } - - // run the search based on the new filter settings - if( searchScope === 'project' ) { - this.setState({...this.state, searchQuery: searchQ, searchFilterOptions, searchFilterMode: open }) - fairCopy.ipcSend('searchProject', searchQ) + this.setState({ + ...this.state, + moveResourceMode: true, + moveResourceProps, + ...closePopUpState, + }) + } + const onToggleSearchBar = (showSearchBar) => { + if (!showSearchBar) { + this.closeSearchBar() } else { - const currentResource = selectedResource ? openResources[selectedResource] : null - if( currentResource ) { - const searchResults = searchResource( currentResource, searchQ ) - this.setState({...this.state, searchQuery: searchQ, searchFilterOptions, searchFilterMode: open }) - this.updateSearchResults(currentResource, searchQuery, searchResults) - } + this.setState({ ...this.state, showSearchBar: true }) } + } + + // bump state to update sidebar + const onErrorCountChange = () => { + this.setState({ ...this.state }) + } + + if (resource instanceof TEIDocument) { + editors.push( + + ) + } else { + editors.push( + + ) + } } - updateSearchResults(resource, searchQuery, searchResults) { - const { resourceID } = resource - const resourceSearchResults = searchResults[resourceID] ? searchResults[resourceID] : -1 - highlightSearchResults(resource, searchQuery, resourceSearchResults) - } - - closeSearchBar() { - const { selectedResource, openResources } = this.state - if( selectedResource ) { - const resource = openResources[selectedResource] - highlightSearchResults(resource, null, -1) - resource.getActiveView().focus() - } - this.setState({...this.state, showSearchBar: false, searchQuery: '', searchResults: {}, searchSelectionIndex: 0, ...closePopUpState }) + // no visible editors have been added to the list, render a spinner + if (selectedResource && !visible) { + editors.push(
{bigRingSpinner()}
) } - toggleSearchScope() { - const { searchScope } = this.state - const nextScope = searchScope === 'file' ? 'project' : 'file' - this.setState({...this.state, searchScope: nextScope }) + return editors + } + + renderContentPane() { + const { fairCopyProject } = this.props + const { + resourceBrowserOpen, + resourceViews, + rightPaneWidth, + resourceIndex, + allResourcesCheckmarked, + resourceCheckmarks, + } = this.state + const { currentView } = resourceViews + const resourceView = resourceViews[currentView] + const { parentEntry } = resourceView + + return ( +
+ {resourceBrowserOpen && ( + { + this.setState({ ...this.state, editTEIDocDialogMode: true }) + }} + onImportResource={this.onImportResource} + onLogin={this.onLogin} + teiDoc={parentEntry} + setResourceCheckmark={this.setResourceCheckmark} + setAllCheckmarks={this.setAllCheckmarks} + allResourcesCheckmarked={allResourcesCheckmarked} + resourceCheckmarks={resourceCheckmarks} + onResourceViewChange={this.onResourceViewChange} + currentView={currentView} + resourceView={resourceView} + resourceIndex={resourceIndex} + fairCopyProject={fairCopyProject} + panelWidth={rightPaneWidth} + > + )} + {this.renderEditors()} +
+ ) + } + + renderAlertDialog() { + const { fairCopyProject } = this.props + const { alertDialogMode, alertOptions, exitOnClose } = this.state + + const onCloseAlert = () => { + this.setState({ + ...this.state, + alertDialogMode: 'closed', + alertOptions: null, + }) } - renderEditors() { - const { openResources, selectedResource, leftPaneWidth, resourceViews } = this.state - const { fairCopyProject, onProjectSettings } = this.props - const {currentView} = resourceViews - - const editors = [] - let visible = false - for( const resource of Object.values(openResources) ) { - const hidden = selectedResource !== resource.resourceID - if( !hidden ) visible = true - const key = `editor-${resource.resourceID}` - const { resourceEntry, parentEntry } = resource - - const onSave = () => { this.onResourceAction('save',[resource.resourceID]) } - const onConfirmDeleteImages = ( alertOptions ) => { - this.setState({ ...this.state, alertDialogMode: 'confirmDeleteImages', alertOptions }) - } - const onMoveSurfaces = ( facsDocument, surfaces, onMoved ) => { - const onMove = (movingItems, parentEntry)=>{ facsDocument.moveSurfaces( movingItems, parentEntry, onMoved ) } - const moveResourceProps = { resourceType: 'facs', allowRoot: false, movingItems: surfaces, onMove, onMoved } - this.setState( {...this.state, moveResourceMode: true, moveResourceProps, ...closePopUpState} ) - } - const onToggleSearchBar = (showSearchBar) => { - if( !showSearchBar ) { - this.closeSearchBar() - } else { - this.setState({ ...this.state, showSearchBar: true }) - } - } - - // bump state to update sidebar - const onErrorCountChange = () => { this.setState({...this.state})} - - if( resource instanceof TEIDocument ) { - editors.push( - - ) - } else { - editors.push( - - ) - } - } - - // no visible editors have been added to the list, render a spinner - if( selectedResource && !visible ) { - editors.push(
{bigRingSpinner()}
) - } - - return editors + return ( + + ) + } + + renderDialogs() { + const { + editDialogMode, + searchFilterMode, + searchFilterOptions, + moveResourceProps, + checkInResources, + checkOutMode, + checkOutStatus, + checkOutError, + loginMode, + checkInMode, + addImagesMode, + releaseNotesMode, + dragInfo, + draggingElementActive, + moveResourceMode, + editTEIDocDialogMode, + openResources, + selectedResource, + localResources, + resourceViews, + } = this.state + + const { fairCopyProject, appConfig } = this.props + const { idMap, serverURL } = fairCopyProject + const resourceView = resourceViews[resourceViews.currentView] + const { indexParentID, parentEntry: teiDocEntry } = resourceView + + const selectedDoc = selectedResource + ? openResources[selectedResource] + : null + const resourceEntry = selectedDoc ? selectedDoc.resourceEntry : null + + const { + alertMessage, + editSurfaceInfoMode, + iiifDialogMode, + textImportDialogMode, + surfaceInfo, + } = this.state + const { popupMenuOptions, popupMenuAnchorEl, popupMenuPlacement } = + this.state + + const onSaveResource = (name, localID, type) => { + if (resourceEntry) { + fairCopyProject.updateResource({ + ...resourceEntry, + name, + localID, + type, + }) + } else { + fairCopyProject.newResource(name, localID, type, indexParentID) + } + this.setState({ ...this.state, editDialogMode: false }) } - renderContentPane() { - const { fairCopyProject } = this.props - const { resourceBrowserOpen, resourceViews, rightPaneWidth, resourceIndex, allResourcesCheckmarked, resourceCheckmarks } = this.state - const { currentView } = resourceViews - const resourceView = resourceViews[currentView] - const { parentEntry } = resourceView - - return ( -
- { resourceBrowserOpen && - { this.setState({ ...this.state, editTEIDocDialogMode: true }) }} - onImportResource={this.onImportResource} - onLogin={this.onLogin} - teiDoc={parentEntry} - setResourceCheckmark={this.setResourceCheckmark} - setAllCheckmarks={this.setAllCheckmarks} - allResourcesCheckmarked={allResourcesCheckmarked} - resourceCheckmarks={resourceCheckmarks} - onResourceViewChange={this.onResourceViewChange} - currentView={currentView} - resourceView={resourceView} - resourceIndex={resourceIndex} - fairCopyProject={fairCopyProject} - panelWidth={rightPaneWidth} - > } - { this.renderEditors() } -
- ) + const onSaveTEIDoc = (name, localID, type) => { + fairCopyProject.updateResource({ ...teiDocEntry, name, localID, type }) + this.setState({ ...this.state, editTEIDocDialogMode: false }) } - renderAlertDialog() { - const { fairCopyProject } = this.props - const { alertDialogMode, alertOptions, exitOnClose } = this.state - - const onCloseAlert = () => { - this.setState({ ...this.state, alertDialogMode: 'closed', alertOptions: null }) - } - - return ( - - ) + const onSaveSurfaceInfo = (surfaceInfo) => { + const facsDocument = openResources[surfaceInfo.resourceID] + facsDocument.updateSurfaceInfo(surfaceInfo) + this.setState({ + ...this.state, + surfaceInfo: null, + editSurfaceInfoMode: false, + }) } - renderDialogs() { - const { editDialogMode, searchFilterMode, searchFilterOptions, moveResourceProps, checkInResources, checkOutMode, checkOutStatus, checkOutError, loginMode, checkInMode, addImagesMode, releaseNotesMode, dragInfo, draggingElementActive, moveResourceMode, editTEIDocDialogMode, openResources, selectedResource, localResources, resourceViews } = this.state - - const { fairCopyProject, appConfig } = this.props - const { idMap, serverURL } = fairCopyProject - const resourceView = resourceViews[resourceViews.currentView] - const { indexParentID, parentEntry: teiDocEntry } = resourceView - - const selectedDoc = selectedResource ? openResources[selectedResource] : null - const resourceEntry = selectedDoc ? selectedDoc.resourceEntry : null - - const { alertMessage, editSurfaceInfoMode, iiifDialogMode, textImportDialogMode, surfaceInfo } = this.state - const { popupMenuOptions, popupMenuAnchorEl, popupMenuPlacement } = this.state - - const onSaveResource = (name,localID,type) => { - if( resourceEntry ) { - fairCopyProject.updateResource({ ...resourceEntry, name, localID, type }) - } else { - fairCopyProject.newResource( name, localID, type, indexParentID ) - } - this.setState( {...this.state, editDialogMode: false} ) - } - - const onSaveTEIDoc = (name,localID,type) => { - fairCopyProject.updateResource({ ...teiDocEntry, name, localID, type }) - this.setState( {...this.state, editTEIDocDialogMode: false} ) - } - - const onSaveSurfaceInfo = (surfaceInfo) => { - const facsDocument = openResources[surfaceInfo.resourceID] - facsDocument.updateSurfaceInfo(surfaceInfo) - this.setState( {...this.state, surfaceInfo: null, editSurfaceInfoMode: false} ) - } - - return ( -
- { this.renderAlertDialog() } - - { editDialogMode && { this.setState( {...this.state, editDialogMode: false} )}} - > } - { editTEIDocDialogMode && { this.setState( {...this.state, editTEIDocDialogMode: false} )}} - > } - { iiifDialogMode && { this.setState( {...this.state, iiifDialogMode: false} )}} - > } - { textImportDialogMode && < ImportTextsDialog - fairCopyProject={fairCopyProject} - parentResourceID={indexParentID} - onClose={()=>{ this.setState( {...this.state, textImportDialogMode: false} )}} - > } - { addImagesMode && { this.setState( {...this.state, addImagesMode: false} )}} - > } - { draggingElementActive && {}} - onDrop={()=>{ this.setState( {...this.state, dragInfo: null, draggingElementActive: false} )}} - > } - { moveResourceMode && { this.setState( {...this.state, moveResourceMode: false, moveResourceProps: null} )}} - > } - { popupMenuAnchorEl && } - { releaseNotesMode && { this.setState( { ...this.state, releaseNotesMode: false })}} - > } - { editSurfaceInfoMode && { this.setState( {...this.state, editSurfaceInfoMode: false, surfaceInfo: null} )}} - > } - { searchFilterMode && { this.setState( {...this.state, searchFilterMode: false} )}} - > } - { checkInMode && { this.setState( {...this.state, checkInMode: false} )}} - > } - { checkOutMode && { this.setState( {...this.state, checkOutMode: false, checkOutStatus: null, checkOutError: null} )}} - > } - { loginMode && { this.setState( {...this.state, loginMode: false} )}} - serverURL={serverURL} - onLoggedIn={this.onLoggedIn} - > } - { this.setState({...this.state, alertMessage: null})}} - > -
- ) + return ( +
+ {this.renderAlertDialog()} + + {editDialogMode && ( + { + this.setState({ ...this.state, editDialogMode: false }) + }} + > + )} + {editTEIDocDialogMode && ( + { + this.setState({ ...this.state, editTEIDocDialogMode: false }) + }} + > + )} + {iiifDialogMode && ( + { + this.setState({ ...this.state, iiifDialogMode: false }) + }} + > + )} + {textImportDialogMode && ( + { + this.setState({ ...this.state, textImportDialogMode: false }) + }} + > + )} + {addImagesMode && ( + { + this.setState({ ...this.state, addImagesMode: false }) + }} + > + )} + {draggingElementActive && ( + {}} + onDrop={() => { + this.setState({ + ...this.state, + dragInfo: null, + draggingElementActive: false, + }) + }} + > + )} + {moveResourceMode && ( + { + this.setState({ + ...this.state, + moveResourceMode: false, + moveResourceProps: null, + }) + }} + > + )} + {popupMenuAnchorEl && ( + + )} + {releaseNotesMode && ( + { + this.setState({ ...this.state, releaseNotesMode: false }) + }} + > + )} + {editSurfaceInfoMode && ( + { + this.setState({ + ...this.state, + editSurfaceInfoMode: false, + surfaceInfo: null, + }) + }} + > + )} + {searchFilterMode && ( + { + this.setState({ ...this.state, searchFilterMode: false }) + }} + > + )} + {checkInMode && ( + { + this.setState({ ...this.state, checkInMode: false }) + }} + > + )} + {checkOutMode && ( + { + this.setState({ + ...this.state, + checkOutMode: false, + checkOutStatus: null, + checkOutError: null, + }) + }} + > + )} + {loginMode && ( + { + this.setState({ ...this.state, loginMode: false }) + }} + serverURL={serverURL} + onLoggedIn={this.onLoggedIn} + > + )} + { + this.setState({ ...this.state, alertMessage: null }) + }} + > +
+ ) + } + + renderProjectSidebar() { + const { openResources, selectedResource, leftPaneWidth } = this.state + const { fairCopyProject, onProjectSettings } = this.props + + const onSelectResource = (resourceID) => { + this.onResourceAction('open', [resourceID]) } - renderProjectSidebar() { - const { openResources, selectedResource, leftPaneWidth } = this.state - const { fairCopyProject, onProjectSettings } = this.props - - const onSelectResource = ( resourceID ) => { - this.onResourceAction( 'open', [resourceID] ) - } - - const onCloseResource = ( resourceID ) => { - this.onResourceAction( 'close', [resourceID] ) - } - - return ( - - ) + const onCloseResource = (resourceID) => { + this.onResourceAction('close', [resourceID]) } - onKeyDown = ( event ) => { - const ctrlDown = event.ctrlKey - const commandDown = event.metaKey - const {key} = event - - if( ctrlDown || commandDown ) { - switch(key) { - case '/': - { - const { selectedResource, openResources } = this.state - const currentResource = selectedResource ? openResources[selectedResource] : null - if( currentResource instanceof TEIDocument ) { - const {expandedGutter} = currentResource - currentResource.setExpandedGutter(!expandedGutter) - currentResource.refreshView() - } - } - break - // TODO - // case ' ': - // const activeMenu = 'structure' - // this.onOpenElementMenu({ menuGroup: activeMenu, action: 'replace' }) - // break - case 's': - const { selectedResource } = this.state - if( selectedResource ) { - this.onResourceAction('save',[selectedResource]) - } - break - default: - break + return ( + + ) + } + + onKeyDown = (event) => { + const ctrlDown = event.ctrlKey + const commandDown = event.metaKey + const { key } = event + + if (ctrlDown || commandDown) { + switch (key) { + case '/': + { + const { selectedResource, openResources } = this.state + const currentResource = selectedResource + ? openResources[selectedResource] + : null + if (currentResource instanceof TEIDocument) { + const { expandedGutter } = currentResource + currentResource.setExpandedGutter(!expandedGutter) + currentResource.refreshView() } - } + } + break + // TODO + // case ' ': + // const activeMenu = 'structure' + // this.onOpenElementMenu({ menuGroup: activeMenu, action: 'replace' }) + // break + case 's': + const { selectedResource } = this.state + if (selectedResource) { + this.onResourceAction('save', [selectedResource]) + } + break + default: + break + } } - - render() { - const { appConfig, hidden } = this.props - const { searchEnabled, showSearchBar, searchScope, searchFilterOptions, selectedResource, openResources, searchSelectionIndex } = this.state - - const onDragSplitPane = debounce((leftPaneWidth, rightPaneWidth) => { - this.setState({...this.state, leftPaneWidth, rightPaneWidth }) - }, resizeRefreshRate) - - // TODO fix for search ( selectedResource && isIndexable(openResources[selectedResource].resourceType) ) - const currentResource = selectedResource ? openResources[selectedResource] : null - - // hide the interface (to suspend state) - const style = hidden ? { display: 'none' } : {} - - return ( -
-
- - { this.setState({...this.state, searchSelectionIndex })}} - onResourceAction={this.onResourceAction} - onToggleSearch={()=>{ this.toggleSearchScope() }} - onCloseSearch={()=>{ this.closeSearchBar() } } - onQuitAndInstall={()=>{ this.requestExitApp() }} - onDisplayNotes={()=>{ this.setState({ ...this.state, releaseNotesMode: true })}} - > -
- { this.renderDialogs() } -
- ) - } - + } + + render() { + const { appConfig, hidden } = this.props + const { + searchEnabled, + showSearchBar, + searchScope, + searchFilterOptions, + selectedResource, + openResources, + searchSelectionIndex, + } = this.state + + const onDragSplitPane = debounce((leftPaneWidth, rightPaneWidth) => { + this.setState({ ...this.state, leftPaneWidth, rightPaneWidth }) + }, resizeRefreshRate) + + // TODO fix for search ( selectedResource && isIndexable(openResources[selectedResource].resourceType) ) + const currentResource = selectedResource + ? openResources[selectedResource] + : null + + // hide the interface (to suspend state) + const style = hidden ? { display: 'none' } : {} + + return ( +
+
+ + { + this.setState({ ...this.state, searchSelectionIndex }) + }} + onResourceAction={this.onResourceAction} + onToggleSearch={() => { + this.toggleSearchScope() + }} + onCloseSearch={() => { + this.closeSearchBar() + }} + onQuitAndInstall={() => { + this.requestExitApp() + }} + onDisplayNotes={() => { + this.setState({ ...this.state, releaseNotesMode: true }) + }} + > +
+ {this.renderDialogs()} +
+ ) + } } diff --git a/src/render/components/main-window/tei-editor/TEIEditor.jsx b/src/render/components/main-window/tei-editor/TEIEditor.jsx index 4b3fa1a9..cf04ca2a 100644 --- a/src/render/components/main-window/tei-editor/TEIEditor.jsx +++ b/src/render/components/main-window/tei-editor/TEIEditor.jsx @@ -100,7 +100,7 @@ export default class TEIEditor extends Component { }; createEditorView = (element) => { - const { teiDocument } = this.props; + const { teiDocument, annotationData } = this.props; const { teiSchema } = teiDocument.fairCopyProject; if (teiDocument.editorView) return; @@ -123,7 +123,7 @@ export default class TEIEditor extends Component { }); // uncomment to use ProseMirror dev tools if (process.env["NODE_ENV"] === "development") applyDevTools(editorView); - teiDocument.finalizeEditorView(editorView); + teiDocument.finalizeEditorView(editorView, annotationData); }; dispatchTransaction = (transaction) => { diff --git a/src/render/css/TEIElementStyles.css b/src/render/css/TEIElementStyles.css index 68bcd801..16e220c7 100644 --- a/src/render/css/TEIElementStyles.css +++ b/src/render/css/TEIElementStyles.css @@ -1,100 +1,119 @@ - -[__rtl__ = "true" ] { - direction:rtl; - text-align:right; +[__rtl__="true"] { + direction: rtl; + text-align: right; } -[__ltr__ = "true" ] { - direction:ltr; - text-align:left; +[__ltr__="true"] { + direction: ltr; + text-align: left; } -[__error__ = "true"] { - border-bottom: dotted red; +[__error__="true"] { + border-bottom: dotted red; } - -[phraselvl = "false"] { - margin: 0px; - display: block; +[phraselvl="false"] { + margin: 0px; + display: block; } /* Leave blank space for structure elements that have no content */ -[phraselvl = "false"]:empty { - display: inline-block; - min-height: 30px; +[phraselvl="false"]:empty { + display: inline-block; + min-height: 30px; +} + +__annomark__ { + background-color: rgba(0, 0, 255, 0.1); } -tei-textnode0, tei-textnode1, tei-textnode2, tei-textnode3, tei-textnode4, tei-textnode5, tei-textnode6, tei-textnode7, tei-textnode8, tei-textnode9, tei-textnode10, -tei-textnode11, tei-textnode12, tei-textnode13, tei-textnode14, tei-textnode15, tei-textnode16, tei-textnode17, tei-textnode18, tei-textnode19, tei-textnode20 { - display: block; - padding-top: 2px; - padding-bottom: 2px; +tei-textnode0, +tei-textnode1, +tei-textnode2, +tei-textnode3, +tei-textnode4, +tei-textnode5, +tei-textnode6, +tei-textnode7, +tei-textnode8, +tei-textnode9, +tei-textnode10, +tei-textnode11, +tei-textnode12, +tei-textnode13, +tei-textnode14, +tei-textnode15, +tei-textnode16, +tei-textnode17, +tei-textnode18, +tei-textnode19, +tei-textnode20 { + display: block; + padding-top: 2px; + padding-bottom: 2px; } .inline-node { - margin-right: 3px; - margin-left: 3px; -} + margin-right: 3px; + margin-left: 3px; +} -tei-markdel, tei-del { - color: red; - text-decoration: line-through; +tei-markdel, +tei-del { + color: red; + text-decoration: line-through; } *[rend~="uppercase"] { - text-transform: uppercase; + text-transform: uppercase; } *[rend~="italic"] { - font-style: italic; + font-style: italic; } *[rend~="bold"] { - font-weight: bold; + font-weight: bold; } *[rend~="red"] { - color: red; + color: red; } *[rend~="green"] { - color: green; + color: green; } *[rend~="blue"] { - color: blue; + color: blue; } *[rend~="purple"] { - color: purple; + color: purple; } *[rend~="orange"] { - color: orange; + color: orange; } *[rend~="brown"] { - color: brown; + color: brown; } *[rend~="gray"] { - color: gray; + color: gray; } *[rend~="small"] { - font-size: small; + font-size: small; } *[rend~="medium"] { - font-size: large; + font-size: large; } *[rend~="large"] { - font-size: x-large; + font-size: x-large; } - *[rend~="underline"] { - text-decoration: underline; + text-decoration: underline; } *[rend~="line-through"] { - text-decoration: line-through; - } - + text-decoration: line-through; +} .ProseMirror { - font-family: Arial, Helvetica, sans-serif; - font-size: 14pt; - line-height: 1.5em; + font-family: Arial, Helvetica, sans-serif; + font-size: 14pt; + line-height: 1.5em; } - \ No newline at end of file diff --git a/src/render/model/TEIDocument.js b/src/render/model/TEIDocument.js index f744fbe5..bd4031a9 100644 --- a/src/render/model/TEIDocument.js +++ b/src/render/model/TEIDocument.js @@ -175,15 +175,17 @@ export default class TEIDocument { return this.parentEntry ? this.parentEntry.localID : this.resourceEntry.localID } - finalizeEditorView(editorView) { + finalizeEditorView(editorView, annotationData) { this.editorView = editorView const { tr, doc } = editorView.state tr.setSelection(TextSelection.create(doc, 0)) this.updateSystemFlags(tr, () => { }) editorView.dispatch(tr); addTextNodes(editorView.state, editorView.dispatch) + if (this.resourceType === 'text') { + addAnnotations(editorView.state, annotationData, editorView.dispatch) + } editorView.focus() - addAnnotations(editorView.state, editorView.dispatch) this.changedSinceLastSave = false } diff --git a/src/render/model/TEISchema.js b/src/render/model/TEISchema.js index 53579f7a..b91a4828 100644 --- a/src/render/model/TEISchema.js +++ b/src/render/model/TEISchema.js @@ -126,7 +126,8 @@ export default class TEISchema { marks['__annoMark__'] = { toDOM() { return ["__annoMark__", 0] }, parseDOM: [{ tag: "__annoMark__" }], - attrs: { '__id__': { hidden: true } } + attrs: { 'id': { hidden: true } }, + group: "model_addrPart model_paraPart model_phrase model_limitedPhrase model_pPart_data model_correspActionPart model_nameLike", } return { schemaSpec: { nodes, marks }, elements, attrs, elementGroups: teiSimple.elementGroups, modules: teiSimple.modules } diff --git a/src/render/model/commands.js b/src/render/model/commands.js index 69f3a586..8031cb90 100644 --- a/src/render/model/commands.js +++ b/src/render/model/commands.js @@ -4,35 +4,35 @@ import { createValidNode } from "./element-validators" export function markApplies(doc, ranges, type) { for (let i = 0; i < ranges.length; i++) { - let {$from, $to} = ranges[i] - let can = true + let { $from, $to } = ranges[i] + let can = true doc.nodesBetween($from.pos, $to.pos, node => { - if( node.type.name.includes('textNode') && !node.type.allowsMarkType(type) ) { + if (node.type.name.includes('textNode') && !node.type.allowsMarkType(type)) { can = false return false - } + } }) if (!can) return false } return true } -export function addMark(markType,attrs) { - return function(state, dispatch) { - let {empty, $cursor, ranges} = state.selection +export function addMark(markType, attrs) { + return function (state, dispatch) { + let { empty, $cursor, ranges } = state.selection if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType)) { - dispatch( state.tr.setMeta('alertMessage', `Cannot apply mark at this location.`) ) + dispatch(state.tr.setMeta('alertMessage', `Cannot apply mark at this location.`)) return false - } + } if (dispatch) { if ($cursor) { dispatch(state.tr.addStoredMark(markType.create(null))) } else { - const {tr} = state + const { tr } = state for (let i = 0; i < ranges.length; i++) { - const {$from, $to} = ranges[i] + const { $from, $to } = ranges[i] const markParams = generateMarks(state.doc, markType, attrs, $from, $to) - for( const markParam of markParams ) { + for (const markParam of markParams) { tr.addMark(...markParam) } } @@ -43,25 +43,25 @@ export function addMark(markType,attrs) { } } -function combineAttrs( requestedAttrs, existingAttrs ) { +function combineAttrs(requestedAttrs, existingAttrs) { const nextAttrs = { ...existingAttrs } - for( const key of Object.keys(requestedAttrs) ) { + for (const key of Object.keys(requestedAttrs)) { const nextVal = requestedAttrs[key] - if( nextAttrs[key] ) { + if (nextAttrs[key]) { const vals = nextAttrs[key].split(' ') - if( !vals.includes(nextVal) ) { + if (!vals.includes(nextVal)) { nextAttrs[key] = `${nextAttrs[key]} ${nextVal}` } } else { nextAttrs[key] = nextVal } } - + return nextAttrs } -function generateMarks( doc, markType, attrs, $from, $to ) { +export function generateMarks(doc, markType, attrs, $from, $to) { const parentNode = $from.parent let from = $from.pos const parentStartPos = from - $from.parentOffset @@ -71,26 +71,26 @@ function generateMarks( doc, markType, attrs, $from, $to ) { let to = ($to.pos > parentEndPos) ? parentEndPos : $to.pos const markParams = [] - for( let i=from; i < to; i++ ) { + for (let i = from; i < to; i++) { let markParam - if( doc.rangeHasMark(i,i+1,markType) ) { - const $m = doc.resolve(i+1) + if (doc.rangeHasMark(i, i + 1, markType)) { + const $m = doc.resolve(i + 1) const marks = $m.marks() - const mark = marks.find((m) => m.type === markType ) - const nextAttrs = combineAttrs( attrs, mark.attrs ) - markParam = [ i, i+1, markType.create(nextAttrs) ] + const mark = marks.find((m) => m.type === markType) + const nextAttrs = combineAttrs(attrs, mark.attrs) + markParam = [i, i + 1, markType.create(nextAttrs)] } else { - markParam = [i,i+1,markType.create(attrs)] + markParam = [i, i + 1, markType.create(attrs)] } markParams.push(markParam) - } + } return markParams } export function removeMark(markType) { - return function(state, dispatch) { - let {empty, $cursor, ranges} = state.selection + return function (state, dispatch) { + let { empty, $cursor, ranges } = state.selection if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType)) return false if (dispatch) { if ($cursor) { @@ -98,11 +98,11 @@ export function removeMark(markType) { } else { let has = false, tr = state.tr for (let i = 0; !has && i < ranges.length; i++) { - let {$from, $to} = ranges[i] + let { $from, $to } = ranges[i] has = state.doc.rangeHasMark($from.pos, $to.pos, markType) } for (let i = 0; i < ranges.length; i++) { - let {$from, $to} = ranges[i] + let { $from, $to } = ranges[i] if (has) tr.removeMark($from.pos, $to.pos, markType) } dispatch(tr.scrollIntoView()) @@ -113,67 +113,67 @@ export function removeMark(markType) { } -export function insertAtomNodeAt(elementID, attrs, pos, schema, elements, insertType, tr, createSubDocument ) { +export function insertAtomNodeAt(elementID, attrs, pos, schema, elements, insertType, tr, createSubDocument) { const { doc } = tr const { fcType } = elements[elementID] const nodeType = schema.nodes[elementID] - const offset = insertType === 'below' ? 1 : (fcType === 'asides' ) ? (insertType === 'inside') ? -1 : 1 : -1 + const offset = insertType === 'below' ? 1 : (fcType === 'asides') ? (insertType === 'inside') ? -1 : 1 : -1 let node - if( fcType === 'asides' ) { - const subDocID = createSubDocument(document,elementID,attrs) + if (fcType === 'asides') { + const subDocID = createSubDocument(document, elementID, attrs) node = nodeType.create({ id: '', __id__: subDocID, ...attrs }) } else { node = nodeType.create(attrs) } // keep the final position in bounds at edge of document - let finalPos = pos+offset - if( finalPos >= doc.nodeSize-1 ) finalPos = doc.nodeSize - 2 - if( finalPos < 0 ) finalPos = 0 + let finalPos = pos + offset + if (finalPos >= doc.nodeSize - 1) finalPos = doc.nodeSize - 2 + if (finalPos < 0) finalPos = 0 const $prev = doc.resolve(finalPos) // if there's already a globalNode, insert within it. - if( $prev && $prev.parent && $prev.parent.type.name.includes('globalNode') ) { - tr.insert(finalPos,node) + if ($prev && $prev.parent && $prev.parent.type.name.includes('globalNode')) { + tr.insert(finalPos, node) } else { - tr.insert(pos,node) + tr.insert(pos, node) } return tr } -export function insertNodeAt( elementID, attrs, insertPos, schema, elements, tr, createSubDocument ) { - const validNode = createValidNode(elementID,attrs,Fragment.empty,schema,elements, null, createSubDocument ) - tr.insert( insertPos, validNode ) - return tr +export function insertNodeAt(elementID, attrs, insertPos, schema, elements, tr, createSubDocument) { + const validNode = createValidNode(elementID, attrs, Fragment.empty, schema, elements, null, createSubDocument) + tr.insert(insertPos, validNode) + return tr } -export function changeAttributes( element, newAttrs, $anchor, tr ) { - if( element instanceof Node ) { - const {pos} = $anchor +export function changeAttributes(element, newAttrs, $anchor, tr) { + if (element instanceof Node) { + const { pos } = $anchor const selection = tr.selection - tr.setNodeMarkup(pos,element.type,newAttrs) - if( selection.from === pos ) { + tr.setNodeMarkup(pos, element.type, newAttrs) + if (selection.from === pos) { const nextSelection = new NodeSelection(tr.doc.resolve(pos)) - tr.setSelection( nextSelection ) + tr.setSelection(nextSelection) } return element } else { - const { from, to } = markExtent($anchor,element,tr.doc) - const nextMark = element.type.create( newAttrs ) - tr.removeMark(from,to,element) - tr.addMark(from,to,nextMark) + const { from, to } = markExtent($anchor, element, tr.doc) + const nextMark = element.type.create(newAttrs) + tr.removeMark(from, to, element) + tr.addMark(from, to, nextMark) return nextMark } } function isBlank(node) { - if( node.childCount === 0 ) return true - if( node.childCount > 1 ) return false + if (node.childCount === 0) return true + if (node.childCount > 1) return false const child = node.child(0) - if( child.type.name.includes('textNode') ) { - if( child.textContent.match(/\S+/) ) return false + if (child.type.name.includes('textNode')) { + if (child.textContent.match(/\S+/)) return false return true } return false @@ -185,19 +185,19 @@ export function deleteParentNode(pos, tr) { const grandParentNode = $pos.node() // need to have a grand parent to adopt children - if(!grandParentNode) return tr + if (!grandParentNode) return tr const greatNodes = [], children = [] - for( let i=0; i < grandParentNode.childCount; i++ ) { + for (let i = 0; i < grandParentNode.childCount; i++) { const child = grandParentNode.child(i) - if( child === node ) { + if (child === node) { // always have at least one child so we can have a textnode below it - if( grandParentNode.childCount === 1 || !isBlank(node) ) { - for( let i=0; i < node.childCount; i++ ) { + if (grandParentNode.childCount === 1 || !isBlank(node)) { + for (let i = 0; i < node.childCount; i++) { const grandChild = node.child(i) greatNodes.push(grandChild) children.push(grandChild) - } + } } } else { greatNodes.push(child) @@ -205,18 +205,18 @@ export function deleteParentNode(pos, tr) { } // first, try to promote the children to replace the node - if( grandParentNode.type.validContent(Fragment.fromArray(greatNodes)) ) { - tr.replaceWith(pos,pos+node.nodeSize,children) - } + if (grandParentNode.type.validContent(Fragment.fromArray(greatNodes))) { + tr.replaceWith(pos, pos + node.nodeSize, children) + } else { const greatNodesWithoutNode = [] - for( let i=0; i < grandParentNode.childCount; i++ ) { + for (let i = 0; i < grandParentNode.childCount; i++) { const child = grandParentNode.child(i) - if( child !== node ) greatNodesWithoutNode.push(child) + if (child !== node) greatNodesWithoutNode.push(child) } // delete the node if it is valid to do so, don't leave parent node empty - if( greatNodesWithoutNode.length > 0 && grandParentNode.type.validContent(Fragment.fromArray(greatNodesWithoutNode)) ) { - tr.delete(pos,pos+node.nodeSize) + if (greatNodesWithoutNode.length > 0 && grandParentNode.type.validContent(Fragment.fromArray(greatNodesWithoutNode))) { + tr.delete(pos, pos + node.nodeSize) } else { tr.setMeta('alertMessage', "You must delete this element's parent to delete it.") } @@ -224,19 +224,19 @@ export function deleteParentNode(pos, tr) { return tr } -function findMarkExtent(doc,mark,startPos,dir,endPos) { +function findMarkExtent(doc, mark, startPos, dir, endPos) { let result = -1 let testFn - if( dir === -1 ) { + if (dir === -1) { testFn = (i) => (i >= endPos) } else { testFn = (i) => (i <= endPos) } - for( let i=startPos; testFn(i); i+=dir ) { - if( doc.rangeHasMark( i, i+1, mark ) ) { - result = (dir === -1) ? i : i+1 + for (let i = startPos; testFn(i); i += dir) { + if (doc.rangeHasMark(i, i + 1, mark)) { + result = (dir === -1) ? i : i + 1 } else break } return result @@ -249,12 +249,12 @@ export function markExtent($anchor, mark, doc) { const parentEndPos = parentStartPos + parentNode.nodeSize // walk from index in parent node backwards until we encounter text wo/this mark - const backwards = findMarkExtent(doc,mark,pos-1,-1,parentStartPos) - const from = (backwards !== -1 ) ? backwards : pos + const backwards = findMarkExtent(doc, mark, pos - 1, -1, parentStartPos) + const from = (backwards !== -1) ? backwards : pos // now walk forwards, doing the same thing - const forwards = findMarkExtent(doc,mark,pos,1,parentEndPos-1) - const to = (forwards !== -1 ) ? forwards : pos + const forwards = findMarkExtent(doc, mark, pos, 1, parentEndPos - 1) + const to = (forwards !== -1) ? forwards : pos return { from, to } } @@ -262,28 +262,28 @@ export function markExtent($anchor, mark, doc) { export function gatherMarkSets(softNode) { const markSets = [] - softNode.descendants((node) => { - if( node.type.name === 'text' ) { - markSets.push( { marks: node.marks, text: node.text } ) - } + softNode.descendants((node) => { + if (node.type.name === 'text') { + markSets.push({ marks: node.marks, text: node.text }) + } }) return markSets } -export function depthToLast(node,type,depth=0) { - if( node.type.name === type ) return { node, depth } - if( node.childCount === 0 ) return { node: null, depth: 0 } - return depthToLast(node.lastChild,type,depth+1) +export function depthToLast(node, type, depth = 0) { + if (node.type.name === type) return { node, depth } + if (node.childCount === 0) return { node: null, depth: 0 } + return depthToLast(node.lastChild, type, depth + 1) } export function fragmentWithout(fragment, node) { const childNodes = [] - for( let i=0; i -1 && findCount === idCount)) { index++ - // console.log('Found!') if (index < path.length) { - // console.log('Finding -> Node Size: ', node.nodeSize, ', Current Offset: ', curOffset - node.nodeSize) - return findNode(node, path, index, 1, curOffset - node.nodeSize) + // the + 1 is to account for this node's token + return findNode(node, path, index, 1, curOffset - node.nodeSize + 1) } else { - // console.log('Found! -> Node Size: ', node.nodeSize, ', Current Offset: ', curOffset - node.nodeSize) - // Account for the TextNode by adding 2 - return { node, offset: curOffset - node.nodeSize + 2 } + let textStartPos = 0; + node.descendants(function (n, pos) { + if (n.type.name === 'text') { + textStartPos = pos + // console.log('textStartPos: ', textStartPos) + return false + } + }) + // the + 1 is to account for this node's token + return { node, offset: curOffset - node.nodeSize + textStartPos + 1 } } } @@ -291,9 +296,9 @@ export function addAnnotations(state, dispatch = null) { } } - const { doc } = state + const { doc, tr } = state - data.forEach(d => { + annotationData.forEach(d => { const paths = getNodeMap(d.path) // We want to start at the children of text node as that @@ -306,8 +311,18 @@ export function addAnnotations(state, dispatch = null) { doc.nodesBetween(startNode.offset + parseInt(paths.start[paths.start.length - 1].offset), endNode.offset + parseInt(paths.end[paths.end.length - 1].offset), function (node, pos, parent, index) { position = pos }) - console.log('Text Annotated: ', doc.textBetween(startNode.offset + parseInt(paths.start[paths.start.length - 1].offset), position + parseInt(paths.end[paths.end.length - 1].offset))) + const markStart = startNode.offset + parseInt(paths.start[paths.start.length - 1].offset) + const markEnd = position + parseInt(paths.end[paths.end.length - 1].offset) + + // console.log('Text Annotated: ', doc.textBetween(markStart, markEnd)) + + let mark = doc.type.schema.marks['__annoMark__'].create({ id: d.id }) + tr.addMark(markStart, markEnd, mark) }) + + if (dispatch) { + dispatch(tr) + } } // take content fragment and replace any text nodes in there with text node type diff --git a/src/render/workers/parse-standoff.js b/src/render/workers/parse-standoff.js new file mode 100644 index 00000000..d658d11f --- /dev/null +++ b/src/render/workers/parse-standoff.js @@ -0,0 +1,39 @@ +export function parseStandoff(xml) { + let ret = []; + const parser = new DOMParser() + + const xmlDoc = parser.parseFromString(xml, "text/xml") + + const standOff = xmlDoc.querySelector('standOff') + + if (standOff) { + const type = standOff.getAttribute('type') + + // if (type && type === 'recogito_studio_annotations') { + const listAnnotation = standOff.querySelector('listAnnotation') + + if (listAnnotation) { + const annotations = listAnnotation.querySelectorAll('annotation') + + annotations.forEach(a => { + const id = a.getAttribute('xml:id') + const path = a.getAttribute('target') + const note = a.querySelector('note') + const annotation = note ? note.textContent : '' + const resp = a.querySelector('respStmt') + let author = '' + if (resp) { + const name = resp.querySelector('name') + author = name ? name.textContent : '' + } + + ret.push({ + id, path, annotation, author + }) + }) + } + //} + } + + return ret; +} \ No newline at end of file diff --git a/src/render/workers/project-archive-worker.js b/src/render/workers/project-archive-worker.js index 9f2ad0e0..cebef7bb 100644 --- a/src/render/workers/project-archive-worker.js +++ b/src/render/workers/project-archive-worker.js @@ -3,6 +3,7 @@ import { checkInResources, checkOutResources } from '../model/cloud-api/resource import { getResourceAsync, getResourcesAsync } from "../model/cloud-api/resources" import { serializeResource } from "../model/serialize-xml" import { renderTEIDocument } from "../model/editioncrafter/render" +const { parseStandoff } = require('./parse-standoff') const fs = window.fairCopy.getFs() const os = window.fairCopy.getOs() @@ -18,7 +19,7 @@ let projectArchiveState = { open: false, jobQueue: [] } function setupTempFolder() { const cacheFolder = `${os.tmpdir()}/faircopy/` - if( !fs.existsSync(cacheFolder) ) fs.mkdirSync(cacheFolder) + if (!fs.existsSync(cacheFolder)) fs.mkdirSync(cacheFolder) const zipPath = fs.mkdtempSync(cacheFolder) return { cacheFolder, zipPath } } @@ -27,84 +28,84 @@ function readUTF8(targetFilePath, zip) { const file = zip.file(targetFilePath) return file ? file.async("string") : new Promise((resolve) => resolve(null)) } - -function writeUTF8( targetFilePath, data, zip ) { + +function writeUTF8(targetFilePath, data, zip) { zip.file(targetFilePath, data) } -function addFile( localFilePath, resourceID, zip ) { +function addFile(localFilePath, resourceID, zip) { const buffer = fs.readFileSync(localFilePath) zip.file(resourceID, buffer) } -async function checkIn( userID, serverURL, projectID, committedResources, message, zip, postMessage ) { - const authToken = getAuthToken( userID, serverURL ) +async function checkIn(userID, serverURL, projectID, committedResources, message, zip, postMessage) { + const authToken = getAuthToken(userID, serverURL) - if( authToken ) { + if (authToken) { const onSuccess = (results) => { const resourceStatus = {} - for( const result of results ) { - resourceStatus[result.resource_guid] = 'ok' + for (const result of results) { + resourceStatus[result.resource_guid] = 'ok' } postMessage({ messageType: 'check-in-results', resourceStatus, error: null }) console.log(`Check in successful.`) } - - const onFail = (error,results) => { + + const onFail = (error, results) => { const resourceStatus = {} - if( results ) { - for( const result of results ) { + if (results) { + for (const result of results) { resourceStatus[result.resource_guid] = result.error - } + } } postMessage({ messageType: 'check-in-results', resourceStatus, error }) console.log(error) } - + // add the content for each resource being added or updated - for( const committedResource of committedResources ) { - if( committedResource.action !== 'destroy' && committedResource.type !== 'teidoc' ) { + for (const committedResource of committedResources) { + if (committedResource.action !== 'destroy' && committedResource.type !== 'teidoc') { committedResource.content = await readUTF8(committedResource.id, zip) } else { committedResource.content = null } } - - checkInResources(userID, serverURL, authToken, projectID, committedResources, message, onSuccess, onFail) + + checkInResources(userID, serverURL, authToken, projectID, committedResources, message, onSuccess, onFail) } else { postMessage({ messageType: 'check-in-results', resourceIDs: [], error: "User not logged in." }) } } -async function checkOut( userID, serverURL, projectID, resourceEntries, zip, postMessage ) { - const authToken = getAuthToken( userID, serverURL ) +async function checkOut(userID, serverURL, projectID, resourceEntries, zip, postMessage) { + const authToken = getAuthToken(userID, serverURL) const resources = {} // create a list of resource IDs include child resources const resourceIDs = [] - for( const resourceEntry of resourceEntries ) { + for (const resourceEntry of resourceEntries) { const { id: resourceID, type } = resourceEntry - if( type === 'teidoc' ) { - const resourceData = await getResourcesAsync( userID, serverURL, authToken, projectID, resourceID, 1) - for( const resource of resourceData.remoteResources ) { - if( resource.type !== 'header' ) resourceIDs.push(resource.id) + if (type === 'teidoc') { + const resourceData = await getResourcesAsync(userID, serverURL, authToken, projectID, resourceID, 1) + for (const resource of resourceData.remoteResources) { + if (resource.type !== 'header') resourceIDs.push(resource.id) } } resourceIDs.push(resourceID) } - if( authToken ) { + if (authToken) { try { - const resourceStates = await checkOutResources( serverURL, authToken, projectID, resourceIDs ) - + const resourceStates = await checkOutResources(serverURL, authToken, projectID, resourceIDs) + // get the contents for each resource and add them to the project - for( const resourceState of resourceStates ) { + for (const resourceState of resourceStates) { const { resource_guid: resourceID, state } = resourceState - const { resourceEntry, parentEntry, content } = await getResourceAsync( userID, serverURL, authToken, resourceID ) + const { resourceEntry, parentEntry, content } = await getResourceAsync(userID, serverURL, authToken, resourceID) - if( state === 'success') { + if (state === 'success') { resources[resourceEntry.id] = { state, resourceEntry, parentEntry, content } - writeUTF8( resourceEntry.id, content, zip ) + writeUTF8(resourceEntry.id, content, zip) } else { resources[resourceID] = { state, resourceEntry } } @@ -113,46 +114,46 @@ async function checkOut( userID, serverURL, projectID, resourceEntries, zip, pos } catch (e) { postMessage({ messageType: 'check-out-results', resources, error: e.errorMessage }) - } + } } else { postMessage({ messageType: 'check-out-results', resources, error: "User not logged in." }) } } -async function prepareResourceExport( resourceEntry, projectData, zip ) { +async function prepareResourceExport(resourceEntry, projectData, zip) { const { remote, localEntries } = projectData const childEntries = [], contents = {} - if( remote ) { + if (remote) { try { const { serverURL, userID, projectID } = projectData - const authToken = getAuthToken( userID, serverURL ) - if( !authToken ) { + const authToken = getAuthToken(userID, serverURL) + if (!authToken) { return { error: "User not logged in." } } - - if( resourceEntry.type === 'teidoc' ) { + + if (resourceEntry.type === 'teidoc') { // if a teidoc is local, then its children must also be local - if( resourceEntry.local ) { - for( const localEntry of Object.values(localEntries) ) { - if( localEntry.parentResource === resourceEntry.id ) { + if (resourceEntry.local) { + for (const localEntry of Object.values(localEntries)) { + if (localEntry.parentResource === resourceEntry.id) { childEntries.push(localEntry) - contents[localEntry.id] = await readUTF8( localEntry.id, zip ) + contents[localEntry.id] = await readUTF8(localEntry.id, zip) } } } else { - const resourceData = await getResourcesAsync( userID, serverURL, authToken, projectID, resourceEntry.id, 1) + const resourceData = await getResourcesAsync(userID, serverURL, authToken, projectID, resourceEntry.id, 1) const { remoteResources } = resourceData - for( const remoteEntry of remoteResources ) { + for (const remoteEntry of remoteResources) { const { id: resourceID } = remoteEntry const localEntry = localEntries[resourceID] - if( localEntry ) { + if (localEntry) { childEntries.push(localEntry) - contents[resourceID] = await readUTF8( resourceID, zip ) + contents[resourceID] = await readUTF8(resourceID, zip) } else { childEntries.push(remoteEntry) - const remoteResource = await getResourceAsync( userID, serverURL,authToken,resourceID) + const remoteResource = await getResourceAsync(userID, serverURL, authToken, resourceID) contents[resourceID] = remoteResource.content } } @@ -160,27 +161,27 @@ async function prepareResourceExport( resourceEntry, projectData, zip ) { } else { const { id: resourceID } = resourceEntry const localEntry = localEntries[resourceID] - if( localEntry ) { - contents[resourceID] = await readUTF8( resourceID, zip ) + if (localEntry) { + contents[resourceID] = await readUTF8(resourceID, zip) } else { - const remoteResource = await getResourceAsync( userID, serverURL,authToken,resourceID) + const remoteResource = await getResourceAsync(userID, serverURL, authToken, resourceID) contents[resourceID] = remoteResource.content } - } - } catch(e) { + } + } catch (e) { return { error: e.message } } } else { - if( resourceEntry.type === 'teidoc' ) { - for( const localEntry of Object.values(localEntries) ) { - if( localEntry.parentResource === resourceEntry.id ) { + if (resourceEntry.type === 'teidoc') { + for (const localEntry of Object.values(localEntries)) { + if (localEntry.parentResource === resourceEntry.id) { childEntries.push(localEntry) - contents[localEntry.id] = await readUTF8( localEntry.id, zip ) + contents[localEntry.id] = await readUTF8(localEntry.id, zip) } } } else { const { id: resourceID } = resourceEntry - contents[resourceID] = await readUTF8( resourceID, zip ) + contents[resourceID] = await readUTF8(resourceID, zip) } } @@ -189,13 +190,13 @@ async function prepareResourceExport( resourceEntry, projectData, zip ) { async function cacheResource(resourceID, fileName, cacheFolder, zip) { const cacheFile = `${cacheFolder}/${fileName}` - if( !fs.existsSync(cacheFile) ) { + if (!fs.existsSync(cacheFile)) { const imageBuffer = await zip.file(resourceID).async('nodebuffer') - if( !imageBuffer ) { + if (!imageBuffer) { console.error(`Unable to retrieve ${resourceID} from project archive.`) return null } - fs.writeFileSync(cacheFile,imageBuffer) + fs.writeFileSync(cacheFile, imageBuffer) } return cacheFile } @@ -208,56 +209,56 @@ function saveArchive(startTime, zipPath, zip, callback) { // corrupted if we terminate unexpectedly zip .generateNodeStream({ - type:'nodebuffer', + type: 'nodebuffer', compression: "DEFLATE", compressionOptions: { level: 1 }, - streamFiles:true + streamFiles: true }) .pipe(fs.createWriteStream(zipFile)) .on('finish', () => { - fs.copyFileSync( zipFile, projectFilePath ) - fs.unlinkSync( zipFile ) + fs.copyFileSync(zipFile, projectFilePath) + fs.unlinkSync(zipFile) const finishTime = Date.now() - console.info(`${zipFile} written in ${finishTime-startTime}ms`); + console.info(`${zipFile} written in ${finishTime - startTime}ms`); callback() }); } -function exportResource(resourceData, path) { +function exportResource(resourceData, path) { const { resourceEntry } = resourceData const { localID } = resourceEntry const filePath = `${path}/${localID}.xml` try { const xml = serializeResource(resourceData) - fs.writeFileSync(filePath,xml) - } catch(e) { + fs.writeFileSync(filePath, xml) + } catch (e) { // log.error(e) } } function previewResource(resourceData) { - const teiDocXML = serializeResource(resourceData,false) + const teiDocXML = serializeResource(resourceData, false) const teiDocumentID = resourceData.resourceEntry.localID - const renderOptions = { teiDocumentID, baseURL, thumbnailWidth, thumbnailHeight } + const renderOptions = { teiDocumentID, baseURL, thumbnailWidth, thumbnailHeight } const ecData = renderTEIDocument(teiDocXML, renderOptions) const layers = {} let layerID const { childEntries } = resourceData - for( const childEntry of childEntries ) { - if( childEntry.type === 'text' || childEntry.type === 'sourceDoc') { - const {name, localID} = childEntry + for (const childEntry of childEntries) { + if (childEntry.type === 'text' || childEntry.type === 'sourceDoc') { + const { name, localID } = childEntry const html = ecData.resources[localID].html layers[localID] = { name, html } // first layer found is default layerID - if( !layerID ) layerID = localID + if (!layerID) layerID = localID } } return { layers, layerID, ecData } } -async function openArchive(postMessage,workerData) { +async function openArchive(postMessage, workerData) { const { projectFilePath, manifestEntryName, configSettingsEntryName, idMapEntryName } = workerData // create the archive based on worker data @@ -265,9 +266,9 @@ async function openArchive(postMessage,workerData) { const zip = await JSZip.loadAsync(data) const { cacheFolder, zipPath } = setupTempFolder() - const fairCopyManifestJSON = await readUTF8(manifestEntryName,zip) - const fairCopyConfigJSON = await readUTF8(configSettingsEntryName,zip) - const idMap = await readUTF8(idMapEntryName,zip) + const fairCopyManifestJSON = await readUTF8(manifestEntryName, zip) + const fairCopyConfigJSON = await readUTF8(configSettingsEntryName, zip) + const idMap = await readUTF8(idMapEntryName, zip) const fairCopyManifest = fairCopyManifestJSON ? JSON.parse(fairCopyManifestJSON) : null const fairCopyConfig = fairCopyConfigJSON ? JSON.parse(fairCopyConfigJSON) : null @@ -282,18 +283,18 @@ async function openArchive(postMessage,workerData) { return { zip, cacheFolder, zipPath, open, jobQueue, projectFilePath } } -function postError(errorMessage,postMessage) { +function postError(errorMessage, postMessage) { postMessage({ messageType: 'error', errorMessage }) } const save = () => { // process write operations in order and one at a time. const processNextJob = (job) => { - const {startTime, zipPath, zip } = job - saveArchive(startTime, zipPath, zip, () => { + const { startTime, zipPath, zip } = job + saveArchive(startTime, zipPath, zip, () => { projectArchiveState.jobQueue.shift() const job = projectArchiveState.jobQueue[0] - if( job ) processNextJob(job) + if (job) processNextJob(job) }) } @@ -302,7 +303,7 @@ const save = () => { const job = { startTime, zipPath, zip } console.log(`job starting ${startTime}`) - if( projectArchiveState.jobQueue.length === 0 ) { + if (projectArchiveState.jobQueue.length === 0) { projectArchiveState.jobQueue.push(job) processNextJob(job) } else { @@ -311,12 +312,12 @@ const save = () => { } // terminate worker after all jobs are done -const closeSafely = (close) => { +const closeSafely = (close) => { const { jobQueue, cacheFolder } = projectArchiveState - if( jobQueue.length > 0 ) { + if (jobQueue.length > 0) { // write jobs still active, wait a moment and then try again - setTimeout( () => { closeSafely(close) }, 1000 ) + setTimeout(() => { closeSafely(close) }, 1000) } else { // when we are done with jobs, clear cache and exit fs.rmSync(cacheFolder, { recursive: true, force: true }) @@ -325,25 +326,25 @@ const closeSafely = (close) => { } } -export function projectArchive( msg, workerMethods, workerData ) { +export function projectArchive(msg, workerMethods, workerData) { const { open } = projectArchiveState const { messageType } = msg const { postMessage, close } = workerMethods - if( !open ) { - if( messageType === 'open' ) { - openArchive(postMessage,workerData).then((state) => { + if (!open) { + if (messageType === 'open') { + openArchive(postMessage, workerData).then((state) => { projectArchiveState = state }) } else { postError("Can't perform operations when archive is closed.", postMessage) } - return + return } const { zip } = projectArchiveState - - switch( messageType ) { + + switch (messageType) { case 'read-resource': { const { resourceID, xmlID } = msg @@ -352,6 +353,15 @@ export function projectArchive( msg, workerMethods, workerData ) { }) } break + case 'parse-standoff': + { + const { resourceID, parentResourceID } = msg + readUTF8(resourceID, zip).then(resource => { + const data = parseStandoff(resource) + postMessage({ messageType: 'annotation-data', data, parentResourceID }) + }) + } + break case 'request-index': { const { resourceID } = msg @@ -363,17 +373,17 @@ export function projectArchive( msg, workerMethods, workerData ) { case 'request-export': { const { resourceEntry, projectData, path } = msg - prepareResourceExport(resourceEntry,projectData,zip).then( resourceData => { + prepareResourceExport(resourceEntry, projectData, zip).then(resourceData => { exportResource(resourceData, path) postMessage({ messageType: 'exported-resource', path }) }) } - break + break case 'request-preview': { const { previewData, projectData } = msg - prepareResourceExport(previewData.resourceEntry,projectData,zip).then( resp => { - if( resp.error ) { + prepareResourceExport(previewData.resourceEntry, projectData, zip).then(resp => { + if (resp.error) { postMessage({ messageType: 'preview-resource', error: resp.error }) } else { const { ecData, layers, layerID } = previewResource(resp) @@ -384,24 +394,24 @@ export function projectArchive( msg, workerMethods, workerData ) { } }) } - break + break case 'write-resource': { const { resourceID, data } = msg - writeUTF8( resourceID, data, zip ) + writeUTF8(resourceID, data, zip) } - break + break case 'write-file': { const { fileID, data } = msg - writeUTF8( fileID, data, zip ) + writeUTF8(fileID, data, zip) } - break + break case 'cache-resource': { const { cacheFolder } = projectArchiveState const { resourceID, fileName } = msg - cacheResource(resourceID, fileName, cacheFolder, zip).then( (cacheFile) => { + cacheResource(resourceID, fileName, cacheFolder, zip).then((cacheFile) => { postMessage({ messageType: 'cache-file-name', cacheFile }) }) } @@ -409,34 +419,34 @@ export function projectArchive( msg, workerMethods, workerData ) { case 'add-local-file': { const { localFilePath, resourceID } = msg - addFile( localFilePath, resourceID, zip ) + addFile(localFilePath, resourceID, zip) } - break + break case 'remove-file': { const { fileID } = msg zip.remove(fileID) } - break - case 'check-in': + break + case 'check-in': { const { userID, serverURL, projectID, committedResources, message } = msg - checkIn( userID, serverURL, projectID, committedResources, message, zip, postMessage ) - } - break - case 'check-out': + checkIn(userID, serverURL, projectID, committedResources, message, zip, postMessage) + } + break + case 'check-out': { const { userID, serverURL, projectID, resourceEntries } = msg - checkOut( userID, serverURL, projectID, resourceEntries, zip, postMessage ) - } - break + checkOut(userID, serverURL, projectID, resourceEntries, zip, postMessage) + } + break case 'save': save() break case 'close': projectArchiveState.open = false closeSafely(close) - break + break default: throw new Error(`Unrecognized message type: ${messageType}`) } diff --git a/src/render/workers/remote-project-worker.js b/src/render/workers/remote-project-worker.js index 3fb8cb95..1960998e 100644 --- a/src/render/workers/remote-project-worker.js +++ b/src/render/workers/remote-project-worker.js @@ -5,59 +5,60 @@ import { getIDMap } from "../model/cloud-api/id-map" import { connectCable } from "../model/cloud-api/activity-cable" import { getConfig, initConfig, checkInConfig, checkOutConfig } from "../model/cloud-api/config" -function updateIDMap( userID, serverURL, authToken, projectID, postMessage) { - getIDMap( userID, serverURL, authToken, projectID, (idMapData) => { +function updateIDMap(userID, serverURL, authToken, projectID, postMessage) { + getIDMap(userID, serverURL, authToken, projectID, (idMapData) => { postMessage({ messageType: 'id-map-update', idMapData }) }, (error) => { console.log(error) }) } -function updateResourceView( userID, serverURL, projectID, resourceView, authToken, postMessage ) { - if( authToken ) { +function updateResourceView(userID, serverURL, projectID, resourceView, authToken, postMessage) { + if (authToken) { const { currentPage, rowsPerPage, nameFilter, order, orderBy, indexParentID } = resourceView - getResources( userID, serverURL, authToken, projectID, indexParentID, currentPage, rowsPerPage, nameFilter, order, orderBy, (resourceData) => { + getResources(userID, serverURL, authToken, projectID, indexParentID, currentPage, rowsPerPage, nameFilter, order, orderBy, (resourceData) => { const { parentEntry, remoteResources, totalRows } = resourceData resourceView.parentEntry = parentEntry resourceView.totalRows = totalRows resourceView.loading = false postMessage({ messageType: 'resource-view-update', resourceView, remoteResources }) - }, - (error) => { - console.log(error) - }) + }, + (error) => { + console.log(error) + }) } else { // user is not logged in, remote list is empty - const emptyView = { indexParentID: null, + const emptyView = { + indexParentID: null, parentEntry: null, - currentPage: 1, + currentPage: 1, rowsPerPage: resourceView.rowsPerPage, totalRows: 0, - loading: false - } + loading: false + } postMessage({ messageType: 'resource-view-update', resourceView: emptyView, remoteResources: [] }) } } -function updateProjectInfo( userID, serverURL, authToken, projectID, postMessage) { +function updateProjectInfo(userID, serverURL, authToken, projectID, postMessage) { getProject(userID, projectID, serverURL, authToken, (projectInfo) => { postMessage({ messageType: 'project-info-update', projectInfo }) }, - (error) => { - console.log(error) - }) + (error) => { + console.log(error) + }) } -function updateConfig( userID, serverURL, authToken, projectID, postMessage ) { +function updateConfig(userID, serverURL, authToken, projectID, postMessage) { getConfig(userID, projectID, serverURL, authToken, (config, configLastAction) => { postMessage({ messageType: 'config-update', config, configLastAction }) }, - (error) => { - console.log(error) - }) + (error) => { + console.log(error) + }) } -function checkInFairCopyConfig( userID, serverURL, projectID, fairCopyConfig, firstAction, authToken, postMessage ) { +function checkInFairCopyConfig(userID, serverURL, projectID, fairCopyConfig, firstAction, authToken, postMessage) { const onSuccess = (config, configLastAction) => { postMessage({ messageType: 'config-update', config, configLastAction }) } @@ -67,14 +68,14 @@ function checkInFairCopyConfig( userID, serverURL, projectID, fairCopyConfig, fi console.log(error) } - if( firstAction ) { - initConfig( fairCopyConfig, userID, projectID, serverURL, authToken, onSuccess, onFail ) + if (firstAction) { + initConfig(fairCopyConfig, userID, projectID, serverURL, authToken, onSuccess, onFail) } else { - checkInConfig( fairCopyConfig, userID, projectID, serverURL, authToken, onSuccess, onFail ) + checkInConfig(fairCopyConfig, userID, projectID, serverURL, authToken, onSuccess, onFail) } } -function checkOutFairCopyConfig( userID, serverURL, projectID, authToken, postMessage ) { +function checkOutFairCopyConfig(userID, serverURL, projectID, authToken, postMessage) { const onSuccess = (status) => { postMessage({ messageType: 'config-check-out-result', status }) } @@ -84,25 +85,25 @@ function checkOutFairCopyConfig( userID, serverURL, projectID, authToken, postMe console.log(error) } - checkOutConfig( projectID, userID, serverURL, authToken, onSuccess, onFail ) + checkOutConfig(projectID, userID, serverURL, authToken, onSuccess, onFail) } -function getParentResource( userID, serverURL, authToken, resourceEntry, content, xmlID, postMessage) { - getResource( userID, serverURL, authToken, resourceEntry.parentResource, (response) => { +function getParentResource(userID, serverURL, authToken, resourceEntry, content, xmlID, postMessage) { + getResource(userID, serverURL, authToken, resourceEntry.parentResource, (response) => { const { resourceEntry: parentEntry } = response postMessage({ messageType: 'got-parent', resourceEntry, parentEntry, content, xmlID }) }, (errorMessage) => { const parentEntry = { id: resourceEntry.parentResource, localID: '___offline___', - name: '*OFFLINE*', + name: '*OFFLINE*', type: 'teidoc', remote: true, parentResource: null, deleted: false, gitHeadRevision: null, lastAction: null - } + } postMessage({ messageType: 'got-parent', resourceEntry, parentEntry, content, xmlID }) console.log(errorMessage) }) @@ -113,9 +114,9 @@ const onNotification = (data, workerData, postMessage) => { const authToken = getAuthToken(userID, serverURL) const { notification_type: notification } = data - if( notification === "resources_checked_in" ) { + if (notification === "resources_checked_in") { const { resources } = data - updateIDMap( userID, serverURL, authToken, projectID, postMessage ) + updateIDMap(userID, serverURL, authToken, projectID, postMessage) postMessage({ messageType: 'resources-updated', resources }) } // other possible notifications: @@ -125,50 +126,50 @@ const onNotification = (data, workerData, postMessage) => { // config_checked_in } -export function remoteProject( msg, workerMethods, workerData ) { +export function remoteProject(msg, workerMethods, workerData) { const { messageType } = msg const { postMessage, close } = workerMethods const { userID, serverURL, projectID } = workerData const authToken = getAuthToken(userID, serverURL) - - switch( messageType ) { + + switch (messageType) { case 'open': updateProjectInfo(userID, serverURL, authToken, projectID, postMessage) - updateConfig( userID, serverURL, authToken, projectID, postMessage ) - updateIDMap( userID, serverURL, authToken, projectID, postMessage ) - connectCable(projectID, serverURL, authToken, (data) => onNotification( data, workerData, postMessage ) ) + updateConfig(userID, serverURL, authToken, projectID, postMessage) + updateIDMap(userID, serverURL, authToken, projectID, postMessage) + connectCable(projectID, serverURL, authToken, (data) => onNotification(data, workerData, postMessage)) break case 'get-resource': { - const { resourceID, xmlID } = msg - getResource( userID, serverURL, authToken, resourceID, (response) => { + const { resourceID, xmlID } = msg + getResource(userID, serverURL, authToken, resourceID, (response) => { const { resourceEntry, parentEntry, content } = response postMessage({ messageType: 'resource-data', resourceEntry, parentEntry, content, xmlID }) }, (errorMessage) => { console.log(errorMessage) }) - } + } break - case 'get-parent': + case 'get-parent': { const { resourceEntry, content, xmlID } = msg - getParentResource( userID, serverURL, authToken, resourceEntry, content, xmlID, postMessage) + getParentResource(userID, serverURL, authToken, resourceEntry, content, xmlID, postMessage) } break case 'checkin-config': - const { config:fairCopyConfig, firstAction } = msg - checkInFairCopyConfig( userID, serverURL, projectID, fairCopyConfig, firstAction, authToken, postMessage ) + const { config: fairCopyConfig, firstAction } = msg + checkInFairCopyConfig(userID, serverURL, projectID, fairCopyConfig, firstAction, authToken, postMessage) break case 'checkout-config': - checkOutFairCopyConfig( userID, serverURL, projectID, authToken, postMessage ) + checkOutFairCopyConfig(userID, serverURL, projectID, authToken, postMessage) break case 'request-view': - const { resourceView } = msg - updateResourceView( userID, serverURL, projectID, resourceView, authToken, postMessage ) + const { resourceView } = msg + updateResourceView(userID, serverURL, projectID, resourceView, authToken, postMessage) break case 'close': close() - break + break default: throw new Error(`Unrecognized message type: ${messageType}`) } From 572a9e0f03fc964ceaa263f3befb57fc001178af Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Mon, 30 Dec 2024 14:25:13 -0500 Subject: [PATCH 4/6] Working parsing with remote projects --- src/main-process/FairCopySession.js | 7 +- src/main-process/RemoteProject.js | 88 +++++++++++---------- src/main-process/WorkerWindow.js | 22 +++--- src/render/workers/remote-project-worker.js | 25 +++++- 4 files changed, 83 insertions(+), 59 deletions(-) diff --git a/src/main-process/FairCopySession.js b/src/main-process/FairCopySession.js index 428f421a..a3f881e0 100644 --- a/src/main-process/FairCopySession.js +++ b/src/main-process/FairCopySession.js @@ -434,12 +434,7 @@ class FairCopySession { annotationDataReceived(data, parentResourceID) { - // For now this shouldn't work for remote projects - if (this.remote) { - // Do nothing - } else { - this.fairCopyApplication.sendToAllWindows('annotationDataReceived', { annotationData: data, parentResourceID }) - } + this.fairCopyApplication.sendToAllWindows('annotationDataReceived', { annotationData: data, parentResourceID }) } // parent found, send complete resourceOpened message diff --git a/src/main-process/RemoteProject.js b/src/main-process/RemoteProject.js index 448ce051..1fff8f75 100644 --- a/src/main-process/RemoteProject.js +++ b/src/main-process/RemoteProject.js @@ -4,83 +4,89 @@ const { app } = require('electron') class RemoteProject { - constructor( fairCopySession, userID, serverURL, projectID ) { + constructor(fairCopySession, userID, serverURL, projectID) { const { fairCopyApplication } = fairCopySession - const {baseDir} = fairCopyApplication + const { baseDir } = fairCopyApplication this.fairCopySession = fairCopySession - this.initRemoteProjectWorker( baseDir, !app.isPackaged, userID, serverURL, projectID ).then(() => { + this.initRemoteProjectWorker(baseDir, !app.isPackaged, userID, serverURL, projectID).then(() => { this.open() }) } - initRemoteProjectWorker( baseDir, debug, userID, serverURL, projectID ) { - this.remoteProjectWorker = new WorkerWindow( baseDir, debug, 'remote-project', (msg) => { + initRemoteProjectWorker(baseDir, debug, userID, serverURL, projectID) { + this.remoteProjectWorker = new WorkerWindow(baseDir, debug, 'remote-project', (msg) => { const { messageType } = msg - switch( messageType ) { + switch (messageType) { case 'resource-data': { const { resourceEntry, parentEntry, content, xmlID } = msg - this.fairCopySession.resourceOpened( resourceEntry, parentEntry, content, xmlID ) + this.fairCopySession.resourceOpened(resourceEntry, parentEntry, content, xmlID) } break case 'got-parent': - { + { const { resourceEntry, parentEntry, content, xmlID } = msg - this.fairCopySession.parentFound( resourceEntry, parentEntry, content, xmlID ) + this.fairCopySession.parentFound(resourceEntry, parentEntry, content, xmlID) } break case 'project-info-update': { const { projectInfo } = msg const { fairCopyApplication } = this.fairCopySession - fairCopyApplication.sendToAllWindows('updateProjectInfo', projectInfo ) + fairCopyApplication.sendToAllWindows('updateProjectInfo', projectInfo) } break case 'resource-view-update': { const { resourceView, remoteResources } = msg - this.fairCopySession.sendResourceViewUpdate(resourceView,remoteResources) + this.fairCopySession.sendResourceViewUpdate(resourceView, remoteResources) + } + break + case 'parsed-annotation-data': + { + const { data, parentResourceID } = msg + this.fairCopySession.annotationDataReceived(data, parentResourceID) } - break - case 'id-map-update': - { - const { idMapAuthority } = this.fairCopySession - const { idMapData } = msg - idMapAuthority.setBaseMap(idMapData) - } - break + break + case 'id-map-update': + { + const { idMapAuthority } = this.fairCopySession + const { idMapData } = msg + idMapAuthority.setBaseMap(idMapData) + } + break case 'config-update': - { - const { config, configLastAction } = msg - const { fairCopyApplication, projectStore } = this.fairCopySession - const { baseConfig } = projectStore - const { config: appConfig } = fairCopyApplication - const { generatedWith } = projectStore.manifestData - // make sure that the incoming config is migrated to the latest schema - migrateConfig(generatedWith, baseConfig, config, appConfig ) - fairCopyApplication.sendToAllWindows('updateFairCopyConfig', {config, configLastAction} ) - } - break + { + const { config, configLastAction } = msg + const { fairCopyApplication, projectStore } = this.fairCopySession + const { baseConfig } = projectStore + const { config: appConfig } = fairCopyApplication + const { generatedWith } = projectStore.manifestData + // make sure that the incoming config is migrated to the latest schema + migrateConfig(generatedWith, baseConfig, config, appConfig) + fairCopyApplication.sendToAllWindows('updateFairCopyConfig', { config, configLastAction }) + } + break case 'config-check-out-result': const { status } = msg const { fairCopyApplication } = this.fairCopySession - fairCopyApplication.sendToAllWindows('fairCopyConfigCheckedOut', {status} ) - break + fairCopyApplication.sendToAllWindows('fairCopyConfigCheckedOut', { status }) + break case 'resources-updated': - { - const { resources } = msg - // TODO determine which resources need to be updated - // TODO what does this have to do with delete again? - this.fairCopySession.requestResourceView() - } - break + { + const { resources } = msg + // TODO determine which resources need to be updated + // TODO what does this have to do with delete again? + this.fairCopySession.requestResourceView() + } + break default: throw new Error(`Unrecognized message type ${messageType} received from remote project: ${JSON.stringify(msg)}`) } }) - - return this.remoteProjectWorker.start({userID, serverURL, projectID}) + + return this.remoteProjectWorker.start({ userID, serverURL, projectID }) } open() { diff --git a/src/main-process/WorkerWindow.js b/src/main-process/WorkerWindow.js index c4e121bb..dfd7e8dd 100644 --- a/src/main-process/WorkerWindow.js +++ b/src/main-process/WorkerWindow.js @@ -9,16 +9,16 @@ class WorkerWindow { this.messageHandler = messageHandler } - messageForwarder = (e,wid,msg) => { + messageForwarder = (e, wid, msg) => { // if this message is for this worker, send to callback - if( wid === this.workerID ) this.messageHandler(msg) + if (wid === this.workerID) this.messageHandler(msg) } - closeMessageHandler = (e,wid) => { - if( wid === this.workerID ) this.close() + closeMessageHandler = (e, wid) => { + if (wid === this.workerID) this.close() } - async start( workerData ) { + async start(workerData) { ipcMain.on('worker-window-message', this.messageForwarder) ipcMain.on('close-worker-window', this.closeMessageHandler) const webpackPreloadPath = getWebpackPreload('worker_window') @@ -31,28 +31,28 @@ class WorkerWindow { contextIsolation: false, } }) - if( this.debug ) - this.workerWindow.webContents.openDevTools({ mode: 'bottom'} ) + if (this.debug) + this.workerWindow.webContents.openDevTools({ mode: 'bottom' }) this.workerWindow.on('closed', () => { this.workerWindow = null ipcMain.removeListener('worker-window-message', this.messageForwarder) ipcMain.removeListener('close-worker-window', this.closeMessageHandler) }) - + const webpackEntryURL = getWebpackEntry('worker_window') await this.workerWindow.loadURL(webpackEntryURL); - this.workerWindow.webContents.send('init', { workerID: this.workerID, workerData }) + this.workerWindow.webContents.send('init', { workerID: this.workerID, workerData }) } postMessage(messageData) { - if( this.workerWindow && !this.workerWindow.isDestroyed() ) { + if (this.workerWindow && !this.workerWindow.isDestroyed()) { this.workerWindow.webContents.send('message', messageData) } } close() { - if( this.workerWindow && !this.workerWindow.isDestroyed() ) { + if (this.workerWindow && !this.workerWindow.isDestroyed()) { this.workerWindow.destroy() } } diff --git a/src/render/workers/remote-project-worker.js b/src/render/workers/remote-project-worker.js index 1960998e..ebf62a02 100644 --- a/src/render/workers/remote-project-worker.js +++ b/src/render/workers/remote-project-worker.js @@ -1,9 +1,12 @@ import { getResource, getResources } from "../model/cloud-api/resources" import { getProject } from "../model/cloud-api/projects" -import { getAuthToken } from '../model/cloud-api/auth' +import { getAuthToken, authConfig } from '../model/cloud-api/auth'; import { getIDMap } from "../model/cloud-api/id-map" import { connectCable } from "../model/cloud-api/activity-cable" import { getConfig, initConfig, checkInConfig, checkOutConfig } from "../model/cloud-api/config" +import axios from 'axios'; +import { standardErrorHandler } from '../model/cloud-api/error-handler'; +const { parseStandoff } = require('./parse-standoff') function updateIDMap(userID, serverURL, authToken, projectID, postMessage) { getIDMap(userID, serverURL, authToken, projectID, (idMapData) => { @@ -21,6 +24,7 @@ function updateResourceView(userID, serverURL, projectID, resourceView, authToke resourceView.parentEntry = parentEntry resourceView.totalRows = totalRows resourceView.loading = false + checkForAnnotationData(serverURL, userID, authToken, remoteResources, postMessage) postMessage({ messageType: 'resource-view-update', resourceView, remoteResources }) }, (error) => { @@ -40,6 +44,25 @@ function updateResourceView(userID, serverURL, projectID, resourceView, authToke } } +function checkForAnnotationData(serverURL, userID, authToken, remoteResources, postMessage) { + remoteResources.forEach(resource => { + if (resource.type === 'standOff') { + const getResourceURL = `${serverURL}/api/resources/${resource.id}` + + axios.get(getResourceURL, authConfig(authToken)).then( + (okResponse) => { + const { resource } = okResponse.data + const { parent_resource, resource_content: content } = resource + const data = parseStandoff(content) + console.log('parent_resource: ', parent_resource) + postMessage({ messageType: 'parsed-annotation-data', data, parentResourceID: parent_resource.resource_guid }) + }, + standardErrorHandler(userID, serverURL, (error) => console.log(error)) + ) + } + }) +} + function updateProjectInfo(userID, serverURL, authToken, projectID, postMessage) { getProject(userID, projectID, serverURL, authToken, (projectInfo) => { postMessage({ messageType: 'project-info-update', projectInfo }) From 79cdb60bc2f495530c8c1a07c007180a6f761767 Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Thu, 2 Jan 2025 08:58:22 -0500 Subject: [PATCH 5/6] Working with 'mark' nodes --- .../tei-editor/ParameterDrawer.jsx | 951 ++++++++++-------- src/render/css/TEIElementStyles.css | 41 +- src/render/model/TEISchema.js | 11 +- src/render/model/editor-navigation.js | 202 ++-- src/render/model/xml.js | 102 +- 5 files changed, 734 insertions(+), 573 deletions(-) diff --git a/src/render/components/main-window/tei-editor/ParameterDrawer.jsx b/src/render/components/main-window/tei-editor/ParameterDrawer.jsx index ab14188f..95cd1f1b 100644 --- a/src/render/components/main-window/tei-editor/ParameterDrawer.jsx +++ b/src/render/components/main-window/tei-editor/ParameterDrawer.jsx @@ -1,9 +1,21 @@ -import React, { Component } from 'react'; -import { Button, Popper, Paper, ClickAwayListener, IconButton } from '@material-ui/core' -import { Card, CardContent, CardActions, CardHeader, Tooltip } from '@material-ui/core' +import React, { Component } from 'react' +import { + Button, + Popper, + Paper, + ClickAwayListener, + IconButton, +} from '@material-ui/core' +import { + Card, + CardContent, + CardActions, + CardHeader, + Tooltip, +} from '@material-ui/core' import Typography from '@material-ui/core/Typography' -import {Node} from 'prosemirror-model' +import { Node } from 'prosemirror-model' import AttributeDialog from '../dialogs/AttributeDialog' import VocabDialog from '../dialogs/VocabDialog' @@ -15,465 +27,546 @@ import TEIDataWordLikeField from './attribute-fields/TEIDataWordLikeField' import IDField from './attribute-fields/IDField' import ReadOnlyField from './attribute-fields/ReadOnlyField' -import { changeAttributes } from "../../../model/commands" -import { getHighlightColor } from "../../../model/highlighter" +import { changeAttributes } from '../../../model/commands' +import { getHighlightColor } from '../../../model/highlighter' import { checkID } from '../../../model/attribute-validators' import { saveConfig, addElementToSchema } from '../../../model/faircopy-config' -import { teiDataWordValidator, teiDataCountValidator, teiDataNumericValidator, teiDataProbability, teiDataTruthValue } from '../../../model/attribute-validators' -import { findNoteNode } from '../../../model/xml'; +import { + teiDataWordValidator, + teiDataCountValidator, + teiDataNumericValidator, + teiDataProbability, + teiDataTruthValue, +} from '../../../model/attribute-validators' +import { findNoteNode } from '../../../model/xml' export default class ParameterDrawer extends Component { - - constructor() { - super() - this.state = { - attributeDialogOpen: false, - openElementName: null, - vocabDialogOpen: false, - openAttrName: null, - anchorEl: null, - selectedAttr: null, - selectedElement: null, - errorStates: {} - } + constructor() { + super() + this.state = { + attributeDialogOpen: false, + openElementName: null, + vocabDialogOpen: false, + openAttrName: null, + anchorEl: null, + selectedAttr: null, + selectedElement: null, + errorStates: {}, + } + } + + getNextErrorStates(elementName, attrName, error) { + const { errorStates } = this.state + const nextErrorStates = { ...errorStates } + + if (error) { + if (nextErrorStates[elementName]) { + if (!nextErrorStates[elementName].includes(attrName)) + nextErrorStates[elementName].push(attrName) + } else { + nextErrorStates[elementName] = [attrName] + } + } else { + if (nextErrorStates[elementName]) { + nextErrorStates[elementName] = nextErrorStates[elementName].filter( + (a) => a !== attrName + ) + } } - getNextErrorStates(elementName, attrName, error ) { - const { errorStates } = this.state - const nextErrorStates = { ...errorStates } - - if( error ) { - if( nextErrorStates[elementName] ) { - if( !nextErrorStates[elementName].includes(attrName) ) nextErrorStates[elementName].push(attrName) - } else { - nextErrorStates[elementName] = [ attrName ] - } + return nextErrorStates + } + + changeAttributeHandler = (element, attributeKey) => { + return (value, error) => { + const { teiDocument, noteID } = this.props + const { editorGutterPos } = teiDocument.currentTreeNode + const elementName = element.type.name + + const nextErrorStates = this.getNextErrorStates( + elementName, + attributeKey, + error + ) + this.setState({ ...this.state, errorStates: nextErrorStates }) + + const newAttrs = { ...element.attrs } + newAttrs[attributeKey] = value + + const editorView = teiDocument.getActiveView() + const { state } = editorView + const { doc, tr } = state + + if (editorGutterPos !== null) { + const $pos = doc.resolve(editorGutterPos) + const node = $pos.node().child($pos.index()) + // if this is pointing to an aside content node, we want to transform the node in the main doc + if (node.type.name.endsWith('X')) { + const { doc: mainDoc, tr: mainTr } = teiDocument.editorView.state + const { notePos } = findNoteNode(mainDoc, noteID) + const $notePos = mainDoc.resolve(notePos) + changeAttributes(element, newAttrs, $notePos, mainTr) + teiDocument.editorView.dispatch(mainTr) } else { - if( nextErrorStates[elementName] ) { - nextErrorStates[elementName] = nextErrorStates[elementName].filter( a => a !== attrName ) - } + // pointing at a node + changeAttributes(element, newAttrs, $pos, tr) + editorView.dispatch(tr) } - - return nextErrorStates + } else { + // pointing at a mark or inline element + const { $anchor } = state.selection + changeAttributes(element, newAttrs, $anchor, tr) + editorView.dispatch(tr) + } } + } - changeAttributeHandler = ( element, attributeKey ) => { - return (value,error) => { - const { teiDocument, noteID } = this.props - const { editorGutterPos } = teiDocument.currentTreeNode - const elementName = element.type.name - - const nextErrorStates = this.getNextErrorStates(elementName,attributeKey,error) - this.setState({...this.state, errorStates: nextErrorStates }) - - const newAttrs = { ...element.attrs } - newAttrs[attributeKey] = value - - const editorView = teiDocument.getActiveView() - const { state } = editorView - const { doc, tr } = state - - if( editorGutterPos !== null ) { - const $pos = doc.resolve(editorGutterPos) - const node = $pos.node().child($pos.index()) - // if this is pointing to an aside content node, we want to transform the node in the main doc - if( node.type.name.endsWith('X') ) { - const { doc: mainDoc, tr: mainTr } = teiDocument.editorView.state - const { notePos } = findNoteNode( mainDoc, noteID ) - const $notePos = mainDoc.resolve(notePos) - changeAttributes( element, newAttrs, $notePos, mainTr ) - teiDocument.editorView.dispatch(mainTr) - } else { - // pointing at a node - changeAttributes( element, newAttrs, $pos, tr ) - editorView.dispatch(tr) - } - } else { - // pointing at a mark or inline element - const { $anchor } = state.selection - changeAttributes( element, newAttrs, $anchor, tr ) - editorView.dispatch(tr) - } - } - } + renderAttributeInfoPopper() { + const { teiSchema } = this.props.teiDocument.fairCopyProject + const { selectedAttr, selectedElement, anchorEl } = this.state - renderAttributeInfoPopper() { - const {teiSchema} = this.props.teiDocument.fairCopyProject - const {selectedAttr, selectedElement, anchorEl} = this.state - - if( !anchorEl ) return null - const attrSpec = teiSchema.getAttrSpec( selectedAttr, selectedElement ) - const onClickAway = () => { this.setState({...this.state, anchorEl: null})} - - let { minOccurs, maxOccurs } = attrSpec - minOccurs = ( minOccurs === null ) ? 1 : Number(minOccurs) - maxOccurs = ( maxOccurs === null ) ? 1 : (maxOccurs === "unbounded" ) ? '∞' : Number(maxOccurs) - const s = minOccurs === 1 ? '' : 's' - let occurrance = (minOccurs === maxOccurs) ? `${minOccurs} time${s}.` : `(${minOccurs}-${maxOccurs})` - occurrance = ( minOccurs === maxOccurs && maxOccurs === 1 ) ? '' : occurrance - - return( - - - - {selectedAttr} - {attrSpec.description} - {attrSpec.dataType} {occurrance} - - - - ) + if (!anchorEl) return null + const attrSpec = teiSchema.getAttrSpec(selectedAttr, selectedElement) + const onClickAway = () => { + this.setState({ ...this.state, anchorEl: null }) } - renderAttributeField(elementName,attrName,value,attrSpec,onChange) { - const { readOnly, canEditConfig } = this.props - const { dataType, minOccurs, maxOccurs, valListType } = attrSpec - - if( readOnly ) { - return ( - - ) - } - - if( dataType === 'token') { - return ( - - ) - } - if( dataType === 'teidata.word' ) { - return ( - - ) - } - if( dataType === 'teidata.enumerated' ) { - const { vocab } = this.props.teiDocument.getVocab(elementName,attrName) - return ( - - ) - } - if( dataType === 'teidata.pointer' ) { - const { teiDocument } = this.props - return ( - - ) - } - if( dataType === 'teidata.numeric' ) { - return ( - - ) - } - if( dataType === 'teidata.count' ) { - return ( - - ) - } - if( dataType === 'teidata.probability' ) { - return ( - - ) - } - if( dataType === 'teidata.truthValue' ) { - return ( - - ) - } - - // fallback to treating it as text if data type isn't supported - return ( - - ) - + let { minOccurs, maxOccurs } = attrSpec + minOccurs = minOccurs === null ? 1 : Number(minOccurs) + maxOccurs = + maxOccurs === null + ? 1 + : maxOccurs === 'unbounded' + ? '∞' + : Number(maxOccurs) + const s = minOccurs === 1 ? '' : 's' + let occurrance = + minOccurs === maxOccurs + ? `${minOccurs} time${s}.` + : `(${minOccurs}-${maxOccurs})` + occurrance = minOccurs === maxOccurs && maxOccurs === 1 ? '' : occurrance + + return ( + + + + + {selectedAttr} + + + {attrSpec.description} + + + {attrSpec.dataType} {occurrance} + + + + + ) + } + + renderAttributeField(elementName, attrName, value, attrSpec, onChange) { + const { readOnly, canEditConfig } = this.props + const { dataType, minOccurs, maxOccurs, valListType } = attrSpec + + if (readOnly) { + return } - openVocabEditor = (elementName,attrName) => { - this.setState({ - ...this.state, - vocabDialogOpen: true, - openElementName: elementName, - openAttrName: attrName - }) + if (dataType === 'token') { + return ( + + ) } - - renderAttributes(element,elementID,attrState) { - const {teiSchema} = this.props.teiDocument.fairCopyProject - - let attrFields = [], inactiveErrors = [] - - for( const key of Object.keys(attrState) ) { - const attrSpec = teiSchema.getAttrSpec( key, elementID ) - const {attrs} = element - const value = attrs[key] ? attrs[key] : "" - const active = attrState[key].active - - if( !attrSpec.hidden && (active || value !== "")) { - const fieldKey = `attr-${key}` - const onChange = this.changeAttributeHandler(element,key) - - const handleClick = (e) => { - this.setState({ ...this.state, selectedAttr: key, selectedElement: elementID, anchorEl: e.currentTarget }) - } - attrFields.push( -
- { this.renderAttributeField(elementID,key,value,attrSpec,onChange) } - - - -
- ) - if( !active ) { - inactiveErrors.push(this.renderInactiveError(key)) - } - } - } - - return ( attrFields.length > 0 ? -
-
- {attrFields} -
- {inactiveErrors} -
- : This element has no attributes. - ) + if (dataType === 'teidata.word') { + return ( + + ) } - - renderLegendBox(heatmapLevel) { - const style = { - background: getHighlightColor(heatmapLevel) - } - - return ( -
- ) + if (dataType === 'teidata.enumerated') { + const { vocab } = this.props.teiDocument.getVocab(elementName, attrName) + return ( + + ) } - - renderIDField(element) { - const { teiDocument, readOnly } = this.props - const onChange = this.changeAttributeHandler(element,'xml:id') - - let xmlID, preExistingCondition - if( element.attrs['xml:id'] ) { - xmlID = element.attrs['xml:id'] - const relativeParentID = teiDocument.getRelativeParentID() - // flags if the field already has an error - preExistingCondition = checkID(xmlID,relativeParentID,teiDocument.fairCopyProject.idMap)?.errorMessage - } else { - xmlID = "" - preExistingCondition = null - } - - return ( - - ) + if (dataType === 'teidata.pointer') { + const { teiDocument } = this.props + return ( + + ) } - - renderInactiveError(missing) { - return ( - {missing} is not in the project schema. - ) + if (dataType === 'teidata.numeric') { + return ( + + ) + } + if (dataType === 'teidata.count') { + return ( + + ) + } + if (dataType === 'teidata.probability') { + return ( + + ) + } + if (dataType === 'teidata.truthValue') { + return ( + + ) } - renderElement(element,count,key) { - const { teiDocument, readOnly, canEditConfig } = this.props - const { teiSchema, fairCopyConfig } = teiDocument.fairCopyProject - const { elements } = teiSchema - const configElements = fairCopyConfig.elements - const name = element.type.name - const elementID = name.startsWith('mark') ? name.slice('mark'.length) : name.endsWith('X') ? name.slice(0,name.length-1) : name - const {attrState} = configElements[elementID] - const elementSpec = elements[elementID] - - const openAttributeDialog = () => { - this.setState({...this.state, openElementName: elementID, attributeDialogOpen: true }) + // fallback to treating it as text if data type isn't supported + return ( + + ) + } + + openVocabEditor = (elementName, attrName) => { + this.setState({ + ...this.state, + vocabDialogOpen: true, + openElementName: elementName, + openAttrName: attrName, + }) + } + + renderAttributes(element, elementID, attrState) { + const { teiSchema } = this.props.teiDocument.fairCopyProject + + let attrFields = [], + inactiveErrors = [] + + for (const key of Object.keys(attrState)) { + const attrSpec = teiSchema.getAttrSpec(key, elementID) + const { attrs } = element + const value = attrs[key] ? attrs[key] : '' + const active = attrState[key].active + + if (!attrSpec.hidden && (active || value !== '')) { + const fieldKey = `attr-${key}` + const onChange = this.changeAttributeHandler(element, key) + + const handleClick = (e) => { + this.setState({ + ...this.state, + selectedAttr: key, + selectedElement: elementID, + anchorEl: e.currentTarget, + }) } - - const onAddToSchema = () => { - const elementMenu = teiSchema.getElementMenu(elementSpec.pmType)[0] - addElementToSchema(elementID,elementMenu,fairCopyConfig) - saveConfig(fairCopyConfig) - teiDocument.refreshView() + attrFields.push( +
+ {this.renderAttributeField( + elementID, + key, + value, + attrSpec, + onChange + )} + + + +
+ ) + if (!active) { + inactiveErrors.push(this.renderInactiveError(key)) } - - const headerAction = (element instanceof Node) ? this.renderIDField(element) : null - const inactiveElement = fairCopyConfig.elements[elementID] && fairCopyConfig.elements[elementID].active === false - - return ( - - - - { this.renderAttributes(element,elementID,attrState) } - { inactiveElement && this.renderInactiveError(elementID) } - - { !readOnly && canEditConfig && - - - { inactiveElement && } - - } - - ) + } } - renderDialogs() { - const { teiDocument } = this.props - const { fairCopyConfig, teiSchema } = teiDocument.fairCopyProject - const { attributeDialogOpen, openElementName, vocabDialogOpen, openAttrName } = this.state - - const onCloseAttributeDialog = () => { - this.setState({...this.state, openElementName: null, attributeDialogOpen: false }) - } - - const onUpdateConfig = (nextConfig) => { - saveConfig(nextConfig) - teiDocument.refreshView() - } + return attrFields.length > 0 ? ( +
+
{attrFields}
+ {inactiveErrors} +
+ ) : ( + This element has no attributes. + ) + } + + renderLegendBox(heatmapLevel) { + const style = { + background: getHighlightColor(heatmapLevel), + } - const onCloseVocabDialog = () => { - this.setState({...this.state, openAttributeName: null, vocabDialogOpen: false }) - } + return
+ } + + renderIDField(element) { + const { teiDocument, readOnly } = this.props + const onChange = this.changeAttributeHandler(element, 'xml:id') + + let xmlID, preExistingCondition + if (element.attrs['xml:id']) { + xmlID = element.attrs['xml:id'] + const relativeParentID = teiDocument.getRelativeParentID() + // flags if the field already has an error + preExistingCondition = checkID( + xmlID, + relativeParentID, + teiDocument.fairCopyProject.idMap + )?.errorMessage + } else { + xmlID = '' + preExistingCondition = null + } - return ( -
- { attributeDialogOpen && } - { vocabDialogOpen && } - { this.renderAttributeInfoPopper() } -
- ) + return ( + + ) + } + + renderInactiveError(missing) { + return ( + + + {missing} is not in the project schema. + + ) + } + + renderElement(element, count, key) { + const { teiDocument, readOnly, canEditConfig } = this.props + const { teiSchema, fairCopyConfig } = teiDocument.fairCopyProject + const { elements } = teiSchema + const configElements = fairCopyConfig.elements + const name = element.type.name + const elementID = name.startsWith('mark') + ? name.slice('mark'.length) + : name.endsWith('X') + ? name.slice(0, name.length - 1) + : name + if (elementID === '__annoMark__') { + console.log('Why?') + } + const { attrState } = configElements[elementID] + const elementSpec = elements[elementID] + + const openAttributeDialog = () => { + this.setState({ + ...this.state, + openElementName: elementID, + attributeDialogOpen: true, + }) } - renderDrawerPin() { - const { drawerPinned, onDrawerPinToggle } = this.props - const pinIcon = drawerPinned ? 'fas fa-thumbtack' : 'fas fa-thumbtack' - - return ( - - - - - - ) + const onAddToSchema = () => { + const elementMenu = teiSchema.getElementMenu(elementSpec.pmType)[0] + addElementToSchema(elementID, elementMenu, fairCopyConfig) + saveConfig(fairCopyConfig) + teiDocument.refreshView() } - render() { - const { teiDocument, width, height, drawerPinned, onRef } = this.props + const headerAction = + element instanceof Node ? this.renderIDField(element) : null + const inactiveElement = + fairCopyConfig.elements[elementID] && + fairCopyConfig.elements[elementID].active === false + + return ( + + + + {this.renderAttributes(element, elementID, attrState)} + {inactiveElement && this.renderInactiveError(elementID)} + + {!readOnly && canEditConfig && ( + + + {inactiveElement && ( + + )} + + )} + + ) + } + + renderDialogs() { + const { teiDocument } = this.props + const { fairCopyConfig, teiSchema } = teiDocument.fairCopyProject + const { + attributeDialogOpen, + openElementName, + vocabDialogOpen, + openAttrName, + } = this.state + + const onCloseAttributeDialog = () => { + this.setState({ + ...this.state, + openElementName: null, + attributeDialogOpen: false, + }) + } - const elements = teiDocument.selectedElements - if( !drawerPinned && elements.length === 0 ) return null + const onUpdateConfig = (nextConfig) => { + saveConfig(nextConfig) + teiDocument.refreshView() + } - const elementEls = [] - let count = 0 - for( const element of elements ) { - elementEls.push( this.renderElement(element,count,`attr-panel-${count++}`) ) - } + const onCloseVocabDialog = () => { + this.setState({ + ...this.state, + openAttributeName: null, + vocabDialogOpen: false, + }) + } - const s = (elements.length !== 1) ? 's' : '' - const headerMessage = `Element Inspector (${elements.length} element${s})` - - return ( -
-
- { this.renderDrawerPin() } - {headerMessage} -
-
- { (drawerPinned || elementEls.length > 0) && elementEls } -
- { this.renderDialogs() } -
- ) + return ( +
+ {attributeDialogOpen && ( + + )} + {vocabDialogOpen && ( + + )} + {this.renderAttributeInfoPopper()} +
+ ) + } + + renderDrawerPin() { + const { drawerPinned, onDrawerPinToggle } = this.props + const pinIcon = drawerPinned ? 'fas fa-thumbtack' : 'fas fa-thumbtack' + + return ( + + + + + + ) + } + + render() { + const { teiDocument, width, height, drawerPinned, onRef } = this.props + + const elements = teiDocument.selectedElements + if (!drawerPinned && elements.length === 0) return null + + const elementEls = [] + let count = 0 + for (const element of elements) { + elementEls.push( + this.renderElement(element, count, `attr-panel-${count++}`) + ) } + const s = elements.length !== 1 ? 's' : '' + const headerMessage = `Element Inspector (${elements.length} element${s})` + + return ( +
+
+ {this.renderDrawerPin()} + {headerMessage} +
+
+ {(drawerPinned || elementEls.length > 0) && elementEls} +
+ {this.renderDialogs()} +
+ ) + } } diff --git a/src/render/css/TEIElementStyles.css b/src/render/css/TEIElementStyles.css index 16e220c7..334716e4 100644 --- a/src/render/css/TEIElementStyles.css +++ b/src/render/css/TEIElementStyles.css @@ -1,30 +1,31 @@ -[__rtl__="true"] { +[__rtl__='true'] { direction: rtl; text-align: right; } -[__ltr__="true"] { +[__ltr__='true'] { direction: ltr; text-align: left; } -[__error__="true"] { +[__error__='true'] { border-bottom: dotted red; } -[phraselvl="false"] { +[phraselvl='false'] { margin: 0px; display: block; } /* Leave blank space for structure elements that have no content */ -[phraselvl="false"]:empty { +[phraselvl='false']:empty { display: inline-block; min-height: 30px; } __annomark__ { background-color: rgba(0, 0, 255, 0.1); + mix-blend-mode: multiply; } tei-textnode0, @@ -64,51 +65,51 @@ tei-del { text-decoration: line-through; } -*[rend~="uppercase"] { +*[rend~='uppercase'] { text-transform: uppercase; } -*[rend~="italic"] { +*[rend~='italic'] { font-style: italic; } -*[rend~="bold"] { +*[rend~='bold'] { font-weight: bold; } -*[rend~="red"] { +*[rend~='red'] { color: red; } -*[rend~="green"] { +*[rend~='green'] { color: green; } -*[rend~="blue"] { +*[rend~='blue'] { color: blue; } -*[rend~="purple"] { +*[rend~='purple'] { color: purple; } -*[rend~="orange"] { +*[rend~='orange'] { color: orange; } -*[rend~="brown"] { +*[rend~='brown'] { color: brown; } -*[rend~="gray"] { +*[rend~='gray'] { color: gray; } -*[rend~="small"] { +*[rend~='small'] { font-size: small; } -*[rend~="medium"] { +*[rend~='medium'] { font-size: large; } -*[rend~="large"] { +*[rend~='large'] { font-size: x-large; } -*[rend~="underline"] { +*[rend~='underline'] { text-decoration: underline; } -*[rend~="line-through"] { +*[rend~='line-through'] { text-decoration: line-through; } diff --git a/src/render/model/TEISchema.js b/src/render/model/TEISchema.js index b91a4828..c38d32ef 100644 --- a/src/render/model/TEISchema.js +++ b/src/render/model/TEISchema.js @@ -124,8 +124,12 @@ export default class TEISchema { // This is a test of adding the annotations to the document marks['__annoMark__'] = { - toDOM() { return ["__annoMark__", 0] }, - parseDOM: [{ tag: "__annoMark__" }], + toDOM(node) { let { id } = node.attrs; return ["__annoMark__", { id }, 0] }, + parseDOM: [{ + tag: "__annoMark__", getAttrs(el) { + return { id: el.getAttribute('id') } + } + }], attrs: { 'id': { hidden: true } }, group: "model_addrPart model_paraPart model_phrase model_limitedPhrase model_pPart_data model_correspActionPart model_nameLike", } @@ -267,6 +271,9 @@ export default class TEISchema { const elementName = synthNameToElementName(elementID) if (!elementName) return this.attrs[attrID] const elementSpec = this.elements[elementName] + if (!elementSpec) { + console.log('Whoops!') + } return elementSpec.derivedAttrs.includes(attrID) ? this.attrs[`${attrID}-${elementName}`] : this.attrs[attrID] } diff --git a/src/render/model/editor-navigation.js b/src/render/model/editor-navigation.js index eb56c04e..d7fc7b0e 100644 --- a/src/render/model/editor-navigation.js +++ b/src/render/model/editor-navigation.js @@ -1,57 +1,57 @@ -import {NodeSelection, TextSelection} from "prosemirror-state" +import { NodeSelection, TextSelection } from "prosemirror-state" import { getHighlightRanges } from "./highlighter" import { synthNameToElementName, findNoteNode } from "./xml" -import {undo, redo} from "prosemirror-history" +import { undo, redo } from "prosemirror-history" import { cutSelectedNode, copySelectedNode } from "./cut-and-paste" import { eraseSelection } from "./editor-actions" const fairCopy = window.fairCopy -export function navigateTree( direction, editorView, pos ) { +export function navigateTree(direction, editorView, pos) { const { doc } = editorView.state const $pos = doc.resolve(pos) const nodeIndex = $pos.index() const parentNode = $pos.node() - + let nextPos = pos, nextNode = parentNode - if( direction === 'up' ) { + if (direction === 'up') { // move the selection to the previous sibling. - if( nodeIndex > 0 ) { - const node = parentNode.child(nodeIndex-1) + if (nodeIndex > 0) { + const node = parentNode.child(nodeIndex - 1) const nodeType = node.type.name // don't move into textNodes or globalNodes - if( !nodeType.includes('textNode') && !nodeType.includes('globalNode') ) { + if (!nodeType.includes('textNode') && !nodeType.includes('globalNode')) { nextNode = node - nextPos = $pos.pos - $pos.nodeBefore.nodeSize + nextPos = $pos.pos - $pos.nodeBefore.nodeSize } } - } else if( direction === 'down' ) { + } else if (direction === 'down') { // select the next sibling - if( nodeIndex < parentNode.childCount-1 ) { - const node = parentNode.child(nodeIndex+1) + if (nodeIndex < parentNode.childCount - 1) { + const node = parentNode.child(nodeIndex + 1) const nodeType = node.type.name - if( !nodeType.includes('textNode') && !nodeType.includes('globalNode') ) { + if (!nodeType.includes('textNode') && !nodeType.includes('globalNode')) { nextNode = node nextPos = $pos.pos + $pos.nodeAfter.nodeSize } } - } else if( direction === 'left' ) { + } else if (direction === 'left') { // select parent const parentPos = $pos.start() - 1 - if( parentPos >= 0 ) { + if (parentPos >= 0) { nextNode = parentNode nextPos = parentPos } - } else if( direction === 'right' ) { + } else if (direction === 'right') { // select first child const node = parentNode.child(nodeIndex) - if( node.childCount > 0 ) { + if (node.childCount > 0) { const firstChild = node.child(0) const childNodeType = firstChild.type.name - if( !childNodeType.includes('textNode') && !childNodeType.includes('globalNode') ) { + if (!childNodeType.includes('textNode') && !childNodeType.includes('globalNode')) { nextNode = firstChild nextPos = $pos.pos + 1 } else { @@ -62,49 +62,49 @@ export function navigateTree( direction, editorView, pos ) { } } - const nextPath = nextNode ? synthNameToElementName( nextNode.type.name ) : null + const nextPath = nextNode ? synthNameToElementName(nextNode.type.name) : null return { nextPos, nextPath } } // Find the structure tree leaf closest to the editor cursor -export function navigateFromEditorToTree( editorView ) { +export function navigateFromEditorToTree(editorView) { const { doc, selection } = editorView.state - + function findLeaf($pos) { const nextNode = $pos.node() const nodeType = nextNode.type.name const nextPos = $pos.start() - 1 >= 0 ? $pos.start() - 1 : 0 - if( nodeType.startsWith('textNode') || nodeType.startsWith('globalNode') ) { - if( nextPos >= 0 ) { + if (nodeType.startsWith('textNode') || nodeType.startsWith('globalNode')) { + if (nextPos >= 0) { return findLeaf(doc.resolve(nextPos)) } else { - return { nextPos:null, nextPath:null } + return { nextPos: null, nextPath: null } } } else { - const nextPath = nextNode ? synthNameToElementName( nextNode.type.name ) : null + const nextPath = nextNode ? synthNameToElementName(nextNode.type.name) : null return { nextPos, nextPath } } } - + const { $anchor } = selection return findLeaf($anchor) } -export function navigateFromTreeToEditor( editorView, editorGutterPos ) { +export function navigateFromTreeToEditor(editorView, editorGutterPos) { const { tr, doc } = editorView.state - tr.setSelection( TextSelection.create(doc,editorGutterPos+1) ) + tr.setSelection(TextSelection.create(doc, editorGutterPos + 1)) tr.scrollIntoView() - tr.setMeta( 'highlightEnabled', true ) + tr.setMeta('highlightEnabled', true) editorView.dispatch(tr) } -export function selectElement( editorView, pos, nodeSelection ) { +export function selectElement(editorView, pos, nodeSelection) { const { tr, doc } = editorView.state - if( nodeSelection ) { - tr.setSelection( NodeSelection.create(doc,pos) ) + if (nodeSelection) { + tr.setSelection(NodeSelection.create(doc, pos)) } else { - tr.setSelection( TextSelection.create(doc,pos+1) ) + tr.setSelection(TextSelection.create(doc, pos + 1)) } tr.scrollIntoView() editorView.dispatch(tr) @@ -112,17 +112,17 @@ export function selectElement( editorView, pos, nodeSelection ) { export function getEnabledMenus(teiDocument) { const editorView = teiDocument.getActiveView() - const {editorGutterPos} = teiDocument.currentTreeNode + const { editorGutterPos } = teiDocument.currentTreeNode - if( editorView ) { + if (editorView) { const { selection } = editorView.state - if( editorGutterPos !== null ) { + if (editorGutterPos !== null) { return { marks: false, inline: false, eraser: true } - } else if( selection.$cursor) { + } else if (selection.$cursor) { return { marks: false, inline: true, @@ -135,7 +135,7 @@ export function getEnabledMenus(teiDocument) { eraser: true } } - } + } return { marks: false, inline: false, @@ -143,136 +143,138 @@ export function getEnabledMenus(teiDocument) { } } -export function arrowNavToNote( openNotePopup, teiDocument, direction ) { +export function arrowNavToNote(openNotePopup, teiDocument, direction) { const { editorView } = teiDocument const { selection } = editorView.state - - if( selection && selection.node ) { + + if (selection && selection.node) { const { node } = selection const nodeName = node.type.name - const {teiSchema} = teiDocument.fairCopyProject - - if( teiSchema.elementGroups.asides.includes(nodeName) ) { + const { teiSchema } = teiDocument.fairCopyProject + + if (teiSchema.elementGroups.asides.includes(nodeName)) { const noteID = node.attrs['__id__'] const { $anchor } = selection const anchorEl = editorView.nodeDOM($anchor.pos) openNotePopup(noteID, anchorEl) - } + } else { - const {tr, selection} = editorView.state - const {$anchor} = selection + const { tr, selection } = editorView.state + const { $anchor } = selection tr.setSelection(TextSelection.create(tr.doc, $anchor.pos + direction)) editorView.dispatch(tr) - } + } } } -export function getEditorCommands( teiDocument, onTogglePalette, onOpenElementMenu, clipboardSerializer, onToggleSearchBar ) { +export function getEditorCommands(teiDocument, onTogglePalette, onOpenElementMenu, clipboardSerializer, onToggleSearchBar) { const editorView = teiDocument.getActiveView() const enabledMenus = getEnabledMenus(teiDocument) - + return { - onTogglePalette: (e) => { + onTogglePalette: (e) => { // for some reason, just hitting meta key will activate this handler after it // has been activated once.. check that we also hit the 1 key. - if( e.key === '1' ) { - onTogglePalette() + if (e.key === '1') { + onTogglePalette() } }, - onOpenMarkMenu: () => { - if(enabledMenus.marks) onOpenElementMenu({ menuGroup: 'mark' }) - }, - onOpenInineMenu: () => { - if(enabledMenus.inline) onOpenElementMenu({ menuGroup: 'inline' }) - }, - eraseSelection: () => { - if(enabledMenus.eraser) eraseSelection(teiDocument) + onOpenMarkMenu: () => { + if (enabledMenus.marks) onOpenElementMenu({ menuGroup: 'mark' }) + }, + onOpenInineMenu: () => { + if (enabledMenus.inline) onOpenElementMenu({ menuGroup: 'inline' }) }, - undo: () => { undo(editorView.state,editorView.dispatch) }, - redo: () => { redo(editorView.state,editorView.dispatch) }, - cutSelectedNode: () => { cutSelectedNode( teiDocument, clipboardSerializer ) }, - copySelectedNode: () => { copySelectedNode( teiDocument, clipboardSerializer ) }, - openSearchBar: () => { + eraseSelection: () => { + if (enabledMenus.eraser) eraseSelection(teiDocument) + }, + undo: () => { undo(editorView.state, editorView.dispatch) }, + redo: () => { redo(editorView.state, editorView.dispatch) }, + cutSelectedNode: () => { cutSelectedNode(teiDocument, clipboardSerializer) }, + copySelectedNode: () => { copySelectedNode(teiDocument, clipboardSerializer) }, + openSearchBar: () => { console.log('open search bar') onToggleSearchBar(true) }, - closeSearchBar: () => { + closeSearchBar: () => { onToggleSearchBar(false) } } } -export function getSelectedElements( teiDocument, noteID ) { +export function getSelectedElements(teiDocument, noteID) { const { asides } = teiDocument.fairCopyProject.teiSchema.elementGroups const { editorGutterPos } = teiDocument.currentTreeNode const editorView = teiDocument.getActiveView() - const selection = (editorView) ? editorView.state.selection : null - + const selection = (editorView) ? editorView.state.selection : null + // create a list of the selected phrase level elements let elements = [] - if( editorGutterPos !== null ) { + if (editorGutterPos !== null) { const { doc } = editorView.state const $pos = doc.resolve(editorGutterPos) const node = $pos.node().child($pos.index()) - if( node.type.name.endsWith('X') ) { + if (node.type.name.endsWith('X')) { // find the node in the main document const mainDoc = teiDocument.editorView.state.doc - const { noteNode } = findNoteNode( mainDoc, noteID ) - if( noteNode ) { - elements.push( noteNode ) + const { noteNode } = findNoteNode(mainDoc, noteID) + if (noteNode) { + elements.push(noteNode) } } else { - elements.push( node ) + elements.push(node) } - } else if( selection ) { - if( selection.node ) { + } else if (selection) { + if (selection.node) { const name = selection.node.type.name - if( !asides.includes(name) && !name.includes('globalNode') ) { - elements.push( selection.node ) - } + if (!asides.includes(name) && !name.includes('globalNode') && !name.includes('annoMark')) { + elements.push(selection.node) + } } else { // highlight ranges are not active when there's a browser selection const browserSelection = window.getSelection() - if( browserSelection.isCollapsed ) { + if (browserSelection.isCollapsed) { const { doc } = editorView.state const { $anchor } = selection - const highlightRanges = getHighlightRanges(doc,$anchor) - for( const highlightRange of highlightRanges ) { - elements.push( highlightRange.mark ) - } - } + const highlightRanges = getHighlightRanges(doc, $anchor) + for (const highlightRange of highlightRanges) { + if (!highlightRange.mark.type.name.includes('annoMark')) { + elements.push(highlightRange.mark) + } + } + } } - } + } return elements } -export function broadcastZoneLinks( teiDocument ) { +export function broadcastZoneLinks(teiDocument) { const { selectedElements, parentEntry } = teiDocument - const {teiSchema, idMap} = teiDocument.fairCopyProject + const { teiSchema, idMap } = teiDocument.fairCopyProject const selectedZones = [] - for( const element of selectedElements ) { - for( const attr of Object.keys(element.attrs) ) { - const attrSpec = teiSchema.getAttrSpec( attr, element.type.name ) + for (const element of selectedElements) { + for (const attr of Object.keys(element.attrs)) { + const attrSpec = teiSchema.getAttrSpec(attr, element.type.name) const dataType = attrSpec?.dataType // is the attribute a tei data pointer? - if( dataType === 'teidata.pointer' ) { + if (dataType === 'teidata.pointer') { const uris = element.attrs[attr]?.split(" ") - if( uris ) { - for( const uri of uris ) { + if (uris) { + for (const uri of uris) { // gather any zone uris - const entry = idMap.get( uri, parentEntry?.localID ) - if( entry && entry.type === 'zone' ) { + const entry = idMap.get(uri, parentEntry?.localID) + if (entry && entry.type === 'zone') { selectedZones.push(uri) } - } + } } } } } - fairCopy.ipcSend('selectedZones', selectedZones ) + fairCopy.ipcSend('selectedZones', selectedZones) } \ No newline at end of file diff --git a/src/render/model/xml.js b/src/render/model/xml.js index c8e3e2a8..e5d82804 100644 --- a/src/render/model/xml.js +++ b/src/render/model/xml.js @@ -89,7 +89,7 @@ export function findNoteNode(doc, noteID) { } export function synthNameToElementName(nodeName) { - if (nodeName.includes('textNode') || nodeName.includes('globalNode')) return null + if (nodeName.includes('textNode') || nodeName.includes('globalNode') || nodeName.includes('__annoMark__')) return null return nodeName.endsWith('X') ? nodeName.slice(0, -1) : nodeName.startsWith('mark') ? nodeName.slice('mark'.length) : nodeName } @@ -221,6 +221,9 @@ export function addTextNodes(state, dispatch = null) { } export function addAnnotations(state, annotationData = [], dispatch = null) { + + const { doc, tr } = state + function getNodeMap(path) { const arr = path.split(' ') const start = arr[0] @@ -264,39 +267,74 @@ export function addAnnotations(state, annotationData = [], dispatch = null) { return ret; } - function findNode(startNode, path, index, findCount, curOffset) { + function findMarkPositionInDescendant(markType, parent) { + let markPos = { start: -1, end: -1 }; + parent.descendants((node, pos) => { + // stop recursing if result is found + if (markPos.start > -1) { + return false; + } + if (markPos.start === -1 && node.marks.find(m => m.type.name === markType)) { + // expect to see something like `markhi('my text')` + console.log(node.toString()) + markPos = { + start: pos, + end: pos + Math.max(node.textContent.length, 1), + }; + } + }); + + return markPos; + } + + function findNode(startNode, path, index, findCount) { const idCount = parseInt(path[index].id) === NaN ? -1 : parseInt(path[index].id); const idMatch = idCount < 0 ? path[index].id : '' for (let i = 0; i < startNode.childCount; i++) { const node = startNode.child(i); - curOffset += node.nodeSize if (node.type.name === path[index].node) { if ((idCount < 0 && node.attrs['@xml:id'] === idMatch) || (idCount > -1 && findCount === idCount)) { index++ if (index < path.length) { - // the + 1 is to account for this node's token - return findNode(node, path, index, 1, curOffset - node.nodeSize + 1) + return findNode(node, path, index, 1) } else { let textStartPos = 0; node.descendants(function (n, pos) { + //console.log('Descendant Pos: ', pos, ', Node: ', n.type.name) if (n.type.name === 'text') { - textStartPos = pos - // console.log('textStartPos: ', textStartPos) + // We only want the starting text node + // This may have multiple nodes broken up by marks + // Since this node matched the path, we want the offset to + // begin from the begining of the fragments + textStartPos = (textStartPos === 0 ? pos : textStartPos) + //console.log('textStartPos: ', textStartPos) return false } }) // the + 1 is to account for this node's token - return { node, offset: curOffset - node.nodeSize + textStartPos + 1 } + return { node, offset: textStartPos + 1 } } } findCount++ } } - } - const { doc, tr } = state + // If you made it here, it did not find the node name...Maybe a mark instead? + // Looking at the startNode, see if it has 'mark${name} in its descendants' + const markPos = findMarkPositionInDescendant(`mark${path[index].node}`, startNode) + if (markPos.start > -1) { + //console.log('Child mark found: ', markPos) + index++ + if (index < path.length) { + return findNode(startNode, path, index, 1) + } else { + // The +1 accounts for the mark token + return { node: startNode, offset: markPos.start + 1 } + } + } + } annotationData.forEach(d => { const paths = getNodeMap(d.path) @@ -306,18 +344,38 @@ export function addAnnotations(state, annotationData = [], dispatch = null) { const startIndex = paths.start.findIndex(p => p.node === 'text') + 1; const endIndex = paths.end.findIndex(p => p.node === 'text') + 1; const startNode = findNode(doc, paths.start, startIndex, 1, 0); - const endNode = findNode(doc, paths.end, endIndex, 1, 0); - let position = 0 - doc.nodesBetween(startNode.offset + parseInt(paths.start[paths.start.length - 1].offset), endNode.offset + parseInt(paths.end[paths.end.length - 1].offset), function (node, pos, parent, index) { - position = pos - }) - const markStart = startNode.offset + parseInt(paths.start[paths.start.length - 1].offset) - const markEnd = position + parseInt(paths.end[paths.end.length - 1].offset) - - // console.log('Text Annotated: ', doc.textBetween(markStart, markEnd)) - - let mark = doc.type.schema.marks['__annoMark__'].create({ id: d.id }) - tr.addMark(markStart, markEnd, mark) + const endNode = findNode(doc, paths.end, endIndex, 1, 0) + + if (startNode && endNode) { + console.log('startNode: ', startNode) + console.log('endNode', endNode) + let startPosition = 0 + doc.descendants((node, pos) => { + if (node === startNode.node) { + startPosition = pos; + } + }) + + //console.log('Alt Start Position: ', startPosition) + + let endPosition = 0 + doc.descendants((node, pos) => { + if (node === endNode.node) { + endPosition = pos; + } + }) + + //console.log('Alt End Position: ', endPosition) + + const markStart = startPosition + startNode.offset + parseInt(paths.start[paths.start.length - 1].offset) + const markEnd = endPosition + endNode.offset + parseInt(paths.end[paths.end.length - 1].offset) + + //console.log('markStart: ', markStart, ', markEnd: ', markEnd) + console.log('Text Annotated: ', doc.textBetween(markStart, markEnd)) + + let mark = doc.type.schema.marks['__annoMark__'].create({ id: d.id }) + tr.addMark(markStart, markEnd, mark) + } }) if (dispatch) { From de88456aaba2e661598deff4efb003b2b2e0bcf6 Mon Sep 17 00:00:00 2001 From: lorinjameson Date: Fri, 16 May 2025 14:56:42 -0400 Subject: [PATCH 6/6] WIP --- src/main-process/FairCopyApplication.js | 241 ++++++------ src/main-process/FairCopySession.js | 25 +- src/main-process/ProjectStore.js | 7 + .../components/main-window/MainWindow.jsx | 6 +- .../tei-editor/ParameterDrawer.jsx | 2 +- .../main-window/tei-editor/TEIEditor.jsx | 359 +++++++++--------- src/render/css/TEIElementStyles.css | 4 +- src/render/model/FairCopyProject.js | 190 ++++----- src/render/model/TEIDocument.js | 66 +++- src/render/model/TEISchema.js | 6 +- src/render/model/editor-navigation.js | 4 +- src/render/model/xml.js | 246 +++++++++--- src/render/workers/project-archive-worker.js | 13 + src/render/workers/serialize-standoff.js | 37 ++ 14 files changed, 746 insertions(+), 460 deletions(-) create mode 100644 src/render/workers/serialize-standoff.js diff --git a/src/main-process/FairCopyApplication.js b/src/main-process/FairCopyApplication.js index d52fd055..7b06d02e 100644 --- a/src/main-process/FairCopyApplication.js +++ b/src/main-process/FairCopyApplication.js @@ -9,7 +9,7 @@ const log = require('electron-log') const { FairCopySession } = require('./FairCopySession') const debugBaseDir = `${process.cwd()}/src` -const distBaseDir = process.resourcesPath +const distBaseDir = process.resourcesPath class FairCopyApplication { @@ -24,7 +24,7 @@ class FairCopyApplication { this.baseDir = !app.isPackaged ? debugBaseDir : distBaseDir this.config = this.getConfig() - + this.mainMenu = new MainMenu(this) this.initFileProtocol() this.initIPC() @@ -44,228 +44,231 @@ class FairCopyApplication { const url = request.url console.log(`handling request: ${url}`) // handle editioncrafter asset requests - if( url.startsWith('ec://ec')) { + if (url.startsWith('ec://ec')) { const content = this.fairCopySession.requestEditionCrafterData(url) return new Response(content, { headers: { 'content-type': 'text/html' } - }) - // handle local image resource requests - // } else if( url.startsWith('file://images') ) { + }) + // handle local image resource requests + // } else if( url.startsWith('file://images') ) { // TODO ... need localPath // this.fairCopySession.openImageResource(request.url) // return net.fetch(pathToFileURL(localPath).toString()) - // } else { + // } else { // TODO ignore any other reqs } }) } initIPC() { - - ipcMain.on('closeProject', (event) => { + + ipcMain.on('closeProject', (event) => { this.closeProject() this.exitApp() }) - ipcMain.on('openWebpage', (event, url ) => { + ipcMain.on('openWebpage', (event, url) => { shell.openExternal(url) - }) - - ipcMain.on('exitApp', (event) => { - if( this.projectWindow ) { - this.projectWindow.close() + }) + + ipcMain.on('exitApp', (event) => { + if (this.projectWindow) { + this.projectWindow.close() } else { - this.exitApp() + this.exitApp() } }) ipcMain.on('reopenProject', (event) => { - this.fairCopySession.reopenProject() + this.fairCopySession.reopenProject() }) - - ipcMain.on('addResource', (event, resourceEntry, resourceData, resourceMap) => { this.fairCopySession.addResource(resourceEntry,resourceData,resourceMap) }) + + ipcMain.on('addResource', (event, resourceEntry, resourceData, resourceMap) => { this.fairCopySession.addResource(resourceEntry, resourceData, resourceMap) }) ipcMain.on('replaceTEIDocument', (event, resources) => { this.fairCopySession.replaceTEIDocument(resources) }) - ipcMain.on('replaceResource', (event, resource, parentEntry) => { this.fairCopySession.replaceResource(resource,parentEntry) }) + ipcMain.on('replaceResource', (event, resource, parentEntry) => { this.fairCopySession.replaceResource(resource, parentEntry) }) + + ipcMain.on('removeResources', (event, resourceIDs) => { + this.fairCopySession.removeResources(resourceIDs) - ipcMain.on('removeResources', (event, resourceIDs) => { - this.fairCopySession.removeResources(resourceIDs) - // close any open image windows - for( const resourceID of resourceIDs ) { + for (const resourceID of resourceIDs) { const imageView = this.imageViews[resourceID] - if( imageView ) { + if (imageView) { imageView.close() } - delete this.imageViews[resourceID] + delete this.imageViews[resourceID] } }) - ipcMain.on('recoverResources', (event, resourceID) => { - this.fairCopySession.recoverResources(resourceID) + ipcMain.on('recoverResources', (event, resourceID) => { + this.fairCopySession.recoverResources(resourceID) }) - ipcMain.on('requestResourceView', (event, resourceViewRequest) => { - this.fairCopySession.updateResourceView(resourceViewRequest) + ipcMain.on('requestResourceView', (event, resourceViewRequest) => { + this.fairCopySession.updateResourceView(resourceViewRequest) }) - - ipcMain.on('searchProject', (event, searchQuery) => { - this.fairCopySession.searchProject(searchQuery) + + ipcMain.on('searchProject', (event, searchQuery) => { + this.fairCopySession.searchProject(searchQuery) }) - ipcMain.on('requestSave', (event, msgID, resourceID, resourceData) => { - const ok = this.fairCopySession.saveResource(resourceID, resourceData, !!this.previewView) - if( ok ) { - const update = { resourceID, messageID: msgID, resourceContent: resourceData } - this.sendToAllWindows('resourceContentUpdated', update ) + ipcMain.on('requestSave', (event, msgID, resourceID, resourceData, annotationData) => { + const ok = this.fairCopySession.saveResource(resourceID, resourceData, !!this.previewView, annotationData) + if (ok) { + const update = { resourceID, messageID: msgID, resourceContent: resourceData } + this.sendToAllWindows('resourceContentUpdated', update) } }) - ipcMain.on('abandonResourceMap', (event, resourceID) => { + ipcMain.on('updateStandoffAnnotations', (event, resourceID, annotationData) => { + const ok = this.fairCopySession.saveAnnotations(resourceID, annotationData) + }) + ipcMain.on('abandonResourceMap', (event, resourceID) => { this.fairCopySession.abandonResourceMap(resourceID) }) - ipcMain.on('updateResource', (event, resourceEntry) => { - this.fairCopySession.updateResource(resourceEntry) + ipcMain.on('updateResource', (event, resourceEntry) => { + this.fairCopySession.updateResource(resourceEntry) }) ipcMain.on('requestImageData', (event) => { const paths = this.mainMenu.openAddImage() - if( paths ) { + if (paths) { this.processImageData(paths).then((imageData) => { - this.sendToAllWindows('imagesOpened', imageData ) - }) + this.sendToAllWindows('imagesOpened', imageData) + }) } else { this.sendToAllWindows('imagesOpened', []) } }) - + // Main window events ////// - ipcMain.on('openResource', (event,resourceID) => { + ipcMain.on('openResource', (event, resourceID) => { this.fairCopySession.openResource(resourceID) }) - ipcMain.on('setResourceMap', (event, resourceMap, localID, parentID) => { + ipcMain.on('setResourceMap', (event, resourceMap, localID, parentID) => { this.fairCopySession.setResourceMap(resourceMap, localID, parentID) }) - ipcMain.on('importContinue', (event) => { + ipcMain.on('importContinue', (event) => { this.fairCopySession.importContinue() }) - ipcMain.on('importEnd', (event) => { + ipcMain.on('importEnd', (event) => { this.fairCopySession.importEnd() }) - ipcMain.on('checkIn', (event, userID, serverURL, projectID, checkInResources, message ) => { + ipcMain.on('checkIn', (event, userID, serverURL, projectID, checkInResources, message) => { this.fairCopySession.checkIn(userID, serverURL, projectID, checkInResources, message) }) - ipcMain.on('checkOut', (event, userID, serverURL, projectID, resourceIDs ) => { + ipcMain.on('checkOut', (event, userID, serverURL, projectID, resourceIDs) => { this.fairCopySession.checkOut(userID, serverURL, projectID, resourceIDs) }) - ipcMain.on('requestSaveConfig', (event,fairCopyConfig,lastAction) => { this.fairCopySession.saveFairCopyConfig(fairCopyConfig,lastAction) }) - ipcMain.on('checkInConfig', (event,fairCopyConfig,firstAction) => { this.fairCopySession.checkInConfig(fairCopyConfig,firstAction) }) - ipcMain.on('checkOutConfig', (event) => { this.fairCopySession.checkOutConfig() }) - ipcMain.on('requestExportConfig', (event,exportPath,fairCopyConfig) => { this.fairCopySession.exportFairCopyConfig(exportPath,fairCopyConfig) }) - ipcMain.on('updateProjectInfo', (event,projectInfo) => { this.fairCopySession.updateProjectInfo(projectInfo) }) - - ipcMain.on('requestPaste', (event) => { - if( this.mainWindow ) { - this.mainWindow.webContents.paste() + ipcMain.on('requestSaveConfig', (event, fairCopyConfig, lastAction) => { this.fairCopySession.saveFairCopyConfig(fairCopyConfig, lastAction) }) + ipcMain.on('checkInConfig', (event, fairCopyConfig, firstAction) => { this.fairCopySession.checkInConfig(fairCopyConfig, firstAction) }) + ipcMain.on('checkOutConfig', (event) => { this.fairCopySession.checkOutConfig() }) + ipcMain.on('requestExportConfig', (event, exportPath, fairCopyConfig) => { this.fairCopySession.exportFairCopyConfig(exportPath, fairCopyConfig) }) + ipcMain.on('updateProjectInfo', (event, projectInfo) => { this.fairCopySession.updateProjectInfo(projectInfo) }) + + ipcMain.on('requestPaste', (event) => { + if (this.mainWindow) { + this.mainWindow.webContents.paste() } }) - ipcMain.on('requestImport', (event,options) => { + ipcMain.on('requestImport', (event, options) => { const paths = this.mainMenu.openImport() - if( paths ) { - this.fairCopySession.importStart(paths,options) + if (paths) { + this.fairCopySession.importStart(paths, options) } }) - ipcMain.on('requestIIIFImport', (event, importList) => { - this.fairCopySession.importIIIFStart(importList) + ipcMain.on('requestIIIFImport', (event, importList) => { + this.fairCopySession.importIIIFStart(importList) }) - ipcMain.on('requestExport', (event, resourceEntries) => { + ipcMain.on('requestExport', (event, resourceEntries) => { const paths = this.mainMenu.openExport() const path = paths ? paths[0] : null - if( path ) { - this.fairCopySession.requestExport(resourceEntries,path) + if (path) { + this.fairCopySession.requestExport(resourceEntries, path) } }) - ipcMain.on('requestImageView', (event, imageViewInfo) => { - this.createImageWindow(imageViewInfo).then( () => { + ipcMain.on('requestImageView', (event, imageViewInfo) => { + this.createImageWindow(imageViewInfo).then(() => { log.info(`Opened image view.`) }) }) - ipcMain.on('requestPreviewView', (event, previewData) => { + ipcMain.on('requestPreviewView', (event, previewData) => { // if the preview window already exists, move it to the front - if( this.previewView ) { + if (this.previewView) { this.previewView.focus() } this.fairCopySession.requestPreviewView(previewData) }) - ipcMain.on('selectedZones', (event, selectedZones) => { - this.sendToAllWindows('selectedZones', selectedZones ) + ipcMain.on('selectedZones', (event, selectedZones) => { + this.sendToAllWindows('selectedZones', selectedZones) }) // Project Window events /////// - ipcMain.on('requestNewPath', (event) => { + ipcMain.on('requestNewPath', (event) => { const targetPath = this.mainMenu.selectPath() this.projectWindow.webContents.send('pathSelected', targetPath) }) - ipcMain.on('requestNewProject', (event, projectInfo) => { - createProjectArchive({ ...projectInfo, defaultProjectCSS: this.config.defaultProjectCSS, generatedWith: this.config.version}, this.baseDir, () => { + ipcMain.on('requestNewProject', (event, projectInfo) => { + createProjectArchive({ ...projectInfo, defaultProjectCSS: this.config.defaultProjectCSS, generatedWith: this.config.version }, this.baseDir, () => { this.openProject(projectInfo.filePath) }) }) - ipcMain.on('requestFileOpen', (event) => { - this.mainMenu.openFileMenu() + ipcMain.on('requestFileOpen', (event) => { + this.mainMenu.openFileMenu() }) - ipcMain.on('requestProject', (event,targetFile) => { - if( fs.existsSync(targetFile) ) { + ipcMain.on('requestProject', (event, targetFile) => { + if (fs.existsSync(targetFile)) { this.openProject(targetFile) } }) - + } async createMainWindow() { - if( this.mainWindow ) { - if( !this.mainWindow.isDestroyed() ) { + if (this.mainWindow) { + if (!this.mainWindow.isDestroyed()) { this.mainWindow.destroy() } this.mainWindow = null } - const windowSize = this.config.devMode ? [1440,1200] : [1440,900] - this.mainWindow = await this.createWindow('main_window', ...windowSize, true, '#fff', true, true ) + const windowSize = this.config.devMode ? [1440, 1200] : [1440, 900] + this.mainWindow = await this.createWindow('main_window', ...windowSize, true, '#fff', true, true) this.mainWindow.webContents.send('appConfig', this.config) // let render window handle on close (without browser restrictions) this.mainWindow.on('close', (event) => { - if( !this.exiting ) { + if (!this.exiting) { this.sendToMainWindow('requestExitApp') event.preventDefault() - } + } }) } async createProjectWindow() { - this.projectWindow = await this.createWindow('project_window', 740, 570, true, '#E6DEF9', false ) + this.projectWindow = await this.createWindow('project_window', 740, 570, true, '#E6DEF9', false) this.projectWindow.webContents.send('appConfig', this.config) - } + } async createPreviewWindow(previewData) { - if( !this.previewView ) { - const windowSize = this.config.devMode ? [1440,1200] : [1440,900] - this.previewView = await this.createWindow('preview_window', ...windowSize, true, '#fff', false, true ) - this.previewView.on('close', e => delete this.previewView ) + if (!this.previewView) { + const windowSize = this.config.devMode ? [1440, 1200] : [1440, 900] + this.previewView = await this.createWindow('preview_window', ...windowSize, true, '#fff', false, true) + this.previewView.on('close', e => delete this.previewView) this.previewView.webContents.send('previewViewOpened', previewData) } else { this.previewView.webContents.send('updatePreview', previewData) @@ -273,42 +276,42 @@ class FairCopyApplication { } async createImageWindow(imageViewInfo) { - const imageView = await this.createWindow('image_window', 1024, 768, true, '#fff', false ) - const {resourceID, xmlID} = imageViewInfo - + const imageView = await this.createWindow('image_window', 1024, 768, true, '#fff', false) + const { resourceID, xmlID } = imageViewInfo + this.imageViews[resourceID] = imageView imageView.on('close', (event) => { delete this.imageViews[resourceID] }) - this.fairCopySession.openResource( resourceID, xmlID ) + this.fairCopySession.openResource(resourceID, xmlID) } exitApp() { - if( !this.exiting ) { + if (!this.exiting) { this.exiting = true - for( const imageView of Object.values(this.imageViews) ) { - if( !imageView.isDestroyed() ) { + for (const imageView of Object.values(this.imageViews)) { + if (!imageView.isDestroyed()) { imageView.close() } } this.imageViews = {} - if( this.previewView ) { + if (this.previewView) { this.previewView.close() this.previewView = null } - if( this.returnToProjectWindow ) { + if (this.returnToProjectWindow) { this.createProjectWindow().then(() => { this.returnToProjectWindow = false this.exiting = false }) - } + } - if( this.mainWindow ) { + if (this.mainWindow) { this.mainWindow.close() this.fairCopySession.closeProject() } @@ -317,9 +320,9 @@ class FairCopyApplication { openProject(targetFile) { this.createMainWindow().then(() => { - if( this.projectWindow ) { + if (this.projectWindow) { this.projectWindow.close() - this.projectWindow = null + this.projectWindow = null } this.fairCopySession = new FairCopySession(this, targetFile) this.mainMenu.updateMenu() @@ -328,13 +331,13 @@ class FairCopyApplication { } closeProject() { - if( this.mainWindow && !this.mainWindow.isDestroyed() ) { + if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.returnToProjectWindow = true this.mainWindow.close() this.fairCopySession.closeProject() this.mainMenu.updateMenu() } - if( this.projectWindow && !this.projectWindow.isDestroyed()) { + if (this.projectWindow && !this.projectWindow.isDestroyed()) { this.projectWindow.close() } } @@ -349,14 +352,14 @@ class FairCopyApplication { sendToAllWindows(message, params) { this.sendToMainWindow(message, params) - for( const imageView of Object.values(this.imageViews) ) { + for (const imageView of Object.values(this.imageViews)) { imageView.webContents.send(message, params) } } async processImageData(paths) { const imageData = [] - for( const path of paths ) { + for (const path of paths) { const image = await Jimp.read(path) const mimeType = image.getMIME() const width = image.bitmap.width @@ -371,7 +374,7 @@ class FairCopyApplication { return imageData } - async createWindow(windowName, width, height, resizable, backgroundColor, menuBar, devTools ) { + async createWindow(windowName, width, height, resizable, backgroundColor, menuBar, devTools) { const webpackPreloadPath = getWebpackPreload(windowName) @@ -382,9 +385,9 @@ class FairCopyApplication { minWidth: 1024, minHeight: 768, webPreferences: { - sandbox: false, // so we can use system clipboard - preload: webpackPreloadPath, - spellcheck: false + sandbox: false, // so we can use system clipboard + preload: webpackPreloadPath, + spellcheck: false }, autoHideMenuBar: !menuBar, @@ -393,13 +396,13 @@ class FairCopyApplication { }) const webpackEntryURL = getWebpackEntry(windowName) - await browserWindow.loadURL(webpackEntryURL) + await browserWindow.loadURL(webpackEntryURL) log.info(`Opened window.`) // Open the DevTools. - if( !app.isPackaged && devTools ) - browserWindow.webContents.openDevTools({ mode: 'bottom'}) - + if (!app.isPackaged && devTools) + browserWindow.webContents.openDevTools({ mode: 'bottom' }) + return browserWindow } } diff --git a/src/main-process/FairCopySession.js b/src/main-process/FairCopySession.js index a3f881e0..a5a1c8eb 100644 --- a/src/main-process/FairCopySession.js +++ b/src/main-process/FairCopySession.js @@ -4,6 +4,7 @@ const log = require('electron-log') const { RemoteProject } = require('./RemoteProject') const { ProjectStore } = require('./ProjectStore') const { createIDMapAuthority } = require('./IDMapAuthority') +const { serializeStandoff } = require('../render/workers/serialize-standoff') const initialRowsPerPage = 50 @@ -355,7 +356,7 @@ class FairCopySession { this.projectStore.searchIndex.searchProject(searchQuery) } - saveResource(resourceID, resourceData, updatePreview) { + saveResource(resourceID, resourceData, updatePreview, annotationData) { const { resources } = this.projectStore.manifestData const resourceEntry = resources[resourceID] if (resourceEntry) { @@ -370,6 +371,28 @@ class FairCopySession { return false } + saveAnnotations(resourceID, annotationData) { + const { resources } = this.projectStore.manifestData + const resourceEntry = resources[resourceID] + if (resourceEntry) { + // Find the standoff for this entry + const parent = resources[resourceEntry.parentResource] + let standoff + Object.keys(resources).forEach(key => { + const res = resources[key] + if (res.parentResource === parent.id && res.type === 'standOff') { + standoff = res + } + }) + + if (standoff) { + this.projectStore.saveAnnotationData(standoff, annotationData) + } + return true + } + return false + } + updateResource(resourceEntry) { const { resources } = this.projectStore.manifestData if (resources[resourceEntry.id]) { diff --git a/src/main-process/ProjectStore.js b/src/main-process/ProjectStore.js index 4f64f460..054b022f 100644 --- a/src/main-process/ProjectStore.js +++ b/src/main-process/ProjectStore.js @@ -250,6 +250,13 @@ class ProjectStore { this.save() } + saveAnnotationData(standoffEntry, data) { + if (standoffEntry.local) { + this.projectArchiveWorker.postMessage({ messageType: 'save-annotation-data', resourceID: standoffEntry.id, data: data }) + this.save() + } + } + requestEditionCrafterData(url) { return this.editionCrafterServer.processRequest(url) } diff --git a/src/render/components/main-window/MainWindow.jsx b/src/render/components/main-window/MainWindow.jsx index 70c3b01c..b5783ba2 100644 --- a/src/render/components/main-window/MainWindow.jsx +++ b/src/render/components/main-window/MainWindow.jsx @@ -125,7 +125,7 @@ export default class MainWindow extends Component { onResourceOpened = (event, resourceData) => { const { fairCopyProject } = this.props - const { openResources, requestedResources } = this.state + const { openResources, requestedResources, annotationData } = this.state const { resourceEntry, parentEntry, resource } = resourceData // if this is a resource we asked for, then open it. @@ -137,7 +137,8 @@ export default class MainWindow extends Component { const doc = fairCopyProject.onResourceOpened( resourceEntry, parentEntry, - resource + resource, + annotationData[parentEntry.id] ) if (doc) { const nextOpenResources = { ...openResources } @@ -1021,7 +1022,6 @@ export default class MainWindow extends Component { key={key} hidden={hidden} teiDocument={resource} - annotationData={annotationData[resource.parentEntry.id]} onOpenElementMenu={this.onOpenElementMenu} onProjectSettings={onProjectSettings} onDragElement={this.onDragElement} diff --git a/src/render/components/main-window/tei-editor/ParameterDrawer.jsx b/src/render/components/main-window/tei-editor/ParameterDrawer.jsx index 95cd1f1b..702c3164 100644 --- a/src/render/components/main-window/tei-editor/ParameterDrawer.jsx +++ b/src/render/components/main-window/tei-editor/ParameterDrawer.jsx @@ -410,7 +410,7 @@ export default class ParameterDrawer extends Component { : name.endsWith('X') ? name.slice(0, name.length - 1) : name - if (elementID === '__annoMark__') { + if (elementID === '__ANNOMARK__') { console.log('Why?') } const { attrState } = configElements[elementID] diff --git a/src/render/components/main-window/tei-editor/TEIEditor.jsx b/src/render/components/main-window/tei-editor/TEIEditor.jsx index cf04ca2a..41e3cdd8 100644 --- a/src/render/components/main-window/tei-editor/TEIEditor.jsx +++ b/src/render/components/main-window/tei-editor/TEIEditor.jsx @@ -1,24 +1,24 @@ -import React, { Component } from "react"; -import { EditorView } from "prosemirror-view"; -import { debounce } from "debounce"; -import { HotKeys } from "react-hotkeys"; - -import applyDevTools from "prosemirror-dev-tools"; - -import ProseMirrorComponent from "../../common/ProseMirrorComponent"; -import EditorGutter from "./EditorGutter"; -import StructurePalette from "./StructurePalette"; -import ParameterDrawer from "./ParameterDrawer"; -import EditorToolbar from "./EditorToolbar"; -import ThumbnailMargin from "./ThumbnailMargin"; -import ReadOnlyToolbar from "./ReadOnlyToolbar"; -import TitleBar from "../TitleBar"; -import NotePopup from "./NotePopup"; +import React, { Component } from 'react' +import { EditorView } from 'prosemirror-view' +import { debounce } from 'debounce' +import { HotKeys } from 'react-hotkeys' + +import applyDevTools from 'prosemirror-dev-tools' + +import ProseMirrorComponent from '../../common/ProseMirrorComponent' +import EditorGutter from './EditorGutter' +import StructurePalette from './StructurePalette' +import ParameterDrawer from './ParameterDrawer' +import EditorToolbar from './EditorToolbar' +import ThumbnailMargin from './ThumbnailMargin' +import ReadOnlyToolbar from './ReadOnlyToolbar' +import TitleBar from '../TitleBar' +import NotePopup from './NotePopup' import { transformPastedHTMLHandler, transformPastedHandler, createClipboardSerializer, -} from "../../../model/cut-and-paste"; +} from '../../../model/cut-and-paste' import { navigateFromTreeToEditor, getSelectedElements, @@ -26,17 +26,17 @@ import { navigateFromEditorToTree, getEditorCommands, arrowNavToNote, -} from "../../../model/editor-navigation"; -import { findNoteNode } from "../../../model/xml"; -import { canConfigAdmin } from "../../../model/permissions"; -import { getConfigStatus } from "../../../model/faircopy-config"; -import { getHotKeyConfig } from "../../../model/editor-keybindings"; +} from '../../../model/editor-navigation' +import { findNoteNode } from '../../../model/xml' +import { canConfigAdmin } from '../../../model/permissions' +import { getConfigStatus } from '../../../model/faircopy-config' +import { getHotKeyConfig } from '../../../model/editor-keybindings' -const resizeRefreshRate = 100; +const resizeRefreshRate = 100 export default class TEIEditor extends Component { constructor() { - super(); + super() this.state = { noteID: null, notePopupAnchorEl: null, @@ -44,70 +44,67 @@ export default class TEIEditor extends Component { paletteWindowOpen: false, drawerPinned: false, currentSubmenuID: 0, - }; - this.drawerRef = null; + } + this.drawerRef = null } componentDidMount() { - this.resizeListener = debounce(this.onResize, resizeRefreshRate); - window.addEventListener("resize", this.resizeListener); - window.onbeforeunload = this.onBeforeUnload; + this.resizeListener = debounce(this.onResize, resizeRefreshRate) + window.addEventListener('resize', this.resizeListener) + window.onbeforeunload = this.onBeforeUnload // bit of a hack to make the gutter settle into the right shape on initial load - setTimeout(this.onResize, 1000); + setTimeout(this.onResize, 1000) } componentWillUnmount() { - window.removeEventListener("resize", this.resizeListener); + window.removeEventListener('resize', this.resizeListener) } onResize = () => { - const { teiDocument } = this.props; - teiDocument.gutterMarkCacheDirty = true; - teiDocument.refreshView(); - }; + const { teiDocument } = this.props + teiDocument.gutterMarkCacheDirty = true + teiDocument.refreshView() + } onBeforeUnload = (e) => { - const { teiDocument } = this.props; - const { changedSinceLastSave } = teiDocument; - const { exitAnyway } = this.state; + const { teiDocument } = this.props + const { changedSinceLastSave } = teiDocument + const { exitAnyway } = this.state if (!exitAnyway && changedSinceLastSave) { // TODO send callback // this.setState({ ...this.state, alertDialogMode: 'close'}) - e.returnValue = false; + e.returnValue = false } - }; + } // Since the editor isn't a react component, some hotkeys are handled by editor directly onEditorKeyDown = (editorView, e) => { - const { teiDocument } = this.props; - const selection = editorView ? editorView.state.selection : null; - const key = e.key; + const { teiDocument } = this.props + const selection = editorView ? editorView.state.selection : null + const key = e.key - if (key === "ArrowLeft") { - arrowNavToNote(this.openNotePopup, teiDocument, -1); - return; + if (key === 'ArrowLeft') { + arrowNavToNote(this.openNotePopup, teiDocument, -1) + return } - if (key === "ArrowRight") { - arrowNavToNote(this.openNotePopup, teiDocument, 1); - return; + if (key === 'ArrowRight') { + arrowNavToNote(this.openNotePopup, teiDocument, 1) + return } if (selection && selection.node) { - return key === "Backspace" || key === "Delete" ? false : true; + return key === 'Backspace' || key === 'Delete' ? false : true } - }; + } createEditorView = (element) => { - const { teiDocument, annotationData } = this.props; - const { teiSchema } = teiDocument.fairCopyProject; + const { teiDocument } = this.props + const { teiSchema } = teiDocument.fairCopyProject - if (teiDocument.editorView) return; - this.clipboardSerializer = createClipboardSerializer( - teiSchema, - teiDocument - ); + if (teiDocument.editorView) return + this.clipboardSerializer = createClipboardSerializer(teiSchema, teiDocument) const editorView = new EditorView(element, { dispatchTransaction: this.dispatchTransaction, @@ -117,41 +114,41 @@ export default class TEIEditor extends Component { transformPastedHTML: transformPastedHTMLHandler(teiSchema, teiDocument), transformPasted: transformPastedHandler(teiSchema, teiDocument), editable: () => { - return teiDocument.isEditable(); + return teiDocument.isEditable() }, clipboardSerializer: this.clipboardSerializer, - }); + }) // uncomment to use ProseMirror dev tools - if (process.env["NODE_ENV"] === "development") applyDevTools(editorView); - teiDocument.finalizeEditorView(editorView, annotationData); - }; + if (process.env['NODE_ENV'] === 'development') applyDevTools(editorView) + teiDocument.finalizeEditorView(editorView) + } dispatchTransaction = (transaction) => { - const { noteID } = this.state; + const { noteID } = this.state const { teiDocument, onAlertMessage, onErrorCountChange, onResetSearch } = - this.props; - const { editorView } = teiDocument; + this.props + const { editorView } = teiDocument if (editorView) { // display error messages generated by this transaction - const alertMessage = transaction.getMeta("alertMessage"); + const alertMessage = transaction.getMeta('alertMessage') if (alertMessage) { - onAlertMessage(alertMessage); + onAlertMessage(alertMessage) } // clear search if doc changed if (transaction.docChanged) { - transaction.setMeta("searchResults", -1); - transaction.setMeta("searchQuery", null); - transaction.setMeta("selectionIndex", 0); - onResetSearch(); + transaction.setMeta('searchResults', -1) + transaction.setMeta('searchQuery', null) + transaction.setMeta('selectionIndex', 0) + onResetSearch() } // update document state - teiDocument.onUpdate(transaction); + teiDocument.onUpdate(transaction) // get next selection state - const selectedElements = getSelectedElements(teiDocument, noteID); + const selectedElements = getSelectedElements(teiDocument, noteID) // opening parameter drawer if ( @@ -159,10 +156,10 @@ export default class TEIEditor extends Component { selectedElements.length > 0 ) { setTimeout(() => { - const { tr } = editorView.state; - tr.scrollIntoView(); - editorView.dispatch(tr); - }, 100); + const { tr } = editorView.state + tr.scrollIntoView() + editorView.dispatch(tr) + }, 100) } // closing parameter drawer, update system flags @@ -172,67 +169,67 @@ export default class TEIEditor extends Component { ) { // TODO can I debounce this? setTimeout(() => { - const { tr } = editorView.state; - teiDocument.updateSystemFlags(tr, onErrorCountChange); - editorView.dispatch(tr); - }, 100); + const { tr } = editorView.state + teiDocument.updateSystemFlags(tr, onErrorCountChange) + editorView.dispatch(tr) + }, 100) } // update selection state - teiDocument.selectedElements = selectedElements; + teiDocument.selectedElements = selectedElements // maintain note position - const nextNotePopupAnchorEl = this.maintainNoteAnchor(); - const nextNoteID = nextNotePopupAnchorEl ? noteID : null; + const nextNotePopupAnchorEl = this.maintainNoteAnchor() + const nextNoteID = nextNotePopupAnchorEl ? noteID : null - broadcastZoneLinks(teiDocument); + broadcastZoneLinks(teiDocument) this.setState({ ...this.state, noteID: nextNoteID, notePopupAnchorEl: nextNotePopupAnchorEl, - }); + }) } - }; + } // editing a note's attributes changes the DOM, invalidating notePopupAnchorEl // this function keeps it attached to the right part of the DOM maintainNoteAnchor() { - const { teiDocument } = this.props; - const { editorView } = teiDocument; - const { noteID, notePopupAnchorEl } = this.state; + const { teiDocument } = this.props + const { editorView } = teiDocument + const { noteID, notePopupAnchorEl } = this.state if (notePopupAnchorEl) { - const { doc } = editorView.state; - const { notePos } = findNoteNode(doc, noteID); + const { doc } = editorView.state + const { notePos } = findNoteNode(doc, noteID) if (notePos !== null) { - const domPos = editorView.domAtPos(notePos); - return domPos.node.childNodes[domPos.offset]; + const domPos = editorView.domAtPos(notePos) + return domPos.node.childNodes[domPos.offset] } else { // anchor element was probably deleted - return null; + return null } } - return notePopupAnchorEl; + return notePopupAnchorEl } onClickOn = (editorView, pos, node, nodePos, event, direct) => { - if (!direct) return; + if (!direct) return - const { teiDocument } = this.props; - const { asides } = teiDocument.fairCopyProject.teiSchema.elementGroups; - const { currentTreeNode } = teiDocument; + const { teiDocument } = this.props + const { asides } = teiDocument.fairCopyProject.teiSchema.elementGroups + const { currentTreeNode } = teiDocument if (asides.includes(node.type.name)) { - const { noteID } = this.state; - const nextID = node.attrs["__id__"]; + const { noteID } = this.state + const nextID = node.attrs['__id__'] if (noteID !== nextID) { if (noteID !== null) { - this.closeNotePopup(); + this.closeNotePopup() } else { - this.openNotePopup(nextID, event.target); + this.openNotePopup(nextID, event.target) } } } else { - this.closeNotePopup(); + this.closeNotePopup() } // if the document is read only, clicking on the body deselects the editor gutter @@ -240,35 +237,35 @@ export default class TEIEditor extends Component { teiDocument.currentTreeNode = { editorGutterPos: null, editorGutterPath: null, - treeID: "main", - }; - teiDocument.refreshView(); + treeID: 'main', + } + teiDocument.refreshView() } - }; + } onTogglePalette = () => { - const { paletteWindowOpen } = this.state; - this.setState({ ...this.state, paletteWindowOpen: !paletteWindowOpen }); - }; + const { paletteWindowOpen } = this.state + this.setState({ ...this.state, paletteWindowOpen: !paletteWindowOpen }) + } onDrawerPinToggle = () => { - const { drawerPinned } = this.state; - this.setState({ ...this.state, drawerPinned: !drawerPinned }); - }; + const { drawerPinned } = this.state + this.setState({ ...this.state, drawerPinned: !drawerPinned }) + } openNotePopup = (noteID, notePopupAnchorEl) => { - this.setState({ ...this.state, noteID, notePopupAnchorEl }); - }; + this.setState({ ...this.state, noteID, notePopupAnchorEl }) + } closeNotePopup = () => { - const { noteID, notePopupAnchorEl } = this.state; + const { noteID, notePopupAnchorEl } = this.state if (noteID !== null || notePopupAnchorEl !== null) { - this.setState({ ...this.state, noteID: null, notePopupAnchorEl: null }); + this.setState({ ...this.state, noteID: null, notePopupAnchorEl: null }) } - }; + } getMainEditorHotKeyConfig() { - const { teiDocument, onToggleSearchBar } = this.props; + const { teiDocument, onToggleSearchBar } = this.props // get the base hotkey config const { keyMap, handlers } = getHotKeyConfig( @@ -280,49 +277,49 @@ export default class TEIEditor extends Component { this.clipboardSerializer, onToggleSearchBar ) - ); + ) - keyMap.hopToTree = "shift+tab"; + keyMap.hopToTree = 'shift+tab' handlers.hopToTree = () => { - const { editorView } = teiDocument; - const { treeID } = teiDocument.currentTreeNode; - const { editorGutterPos } = teiDocument.currentTreeNode; + const { editorView } = teiDocument + const { treeID } = teiDocument.currentTreeNode + const { editorGutterPos } = teiDocument.currentTreeNode if (editorGutterPos === null) { - const { nextPos, nextPath } = navigateFromEditorToTree(editorView); - this.onChangePos(nextPos, nextPath, treeID); + const { nextPos, nextPath } = navigateFromEditorToTree(editorView) + this.onChangePos(nextPos, nextPath, treeID) } - }; + } - return { keyMap, handlers }; + return { keyMap, handlers } } onOpenElementMenu = (elementMenuOptions) => { - this.setState({ ...this.state, elementMenuOptions }); - }; + this.setState({ ...this.state, elementMenuOptions }) + } onCloseElementMenu = () => { - this.setState({ ...this.state, elementMenuOptions: null }); - }; + this.setState({ ...this.state, elementMenuOptions: null }) + } onChangePos = (editorGutterPos, editorGutterPath, treeID) => { - const { noteID } = this.state; - const { teiDocument } = this.props; - const editorView = teiDocument.getActiveView(); - const { tr } = editorView.state; - if (treeID === "main" && noteID) { + const { noteID } = this.state + const { teiDocument } = this.props + const editorView = teiDocument.getActiveView() + const { tr } = editorView.state + if (treeID === 'main' && noteID) { // close the note window - this.closeNotePopup(); + this.closeNotePopup() } else { teiDocument.currentTreeNode = { editorGutterPos, editorGutterPath, treeID, - }; - tr.setMeta("highlightEnabled", editorGutterPos === null); - editorView.dispatch(tr); + } + tr.setMeta('highlightEnabled', editorGutterPos === null) + editorView.dispatch(tr) } - }; + } render() { const { @@ -337,7 +334,7 @@ export default class TEIEditor extends Component { onResourceAction, leftPaneWidth, currentView, - } = this.props; + } = this.props const { noteID, notePopupAnchorEl, @@ -345,65 +342,63 @@ export default class TEIEditor extends Component { currentSubmenuID, drawerPinned, paletteWindowOpen, - } = this.state; - const { fairCopyProject, parentEntry, resourceEntry } = teiDocument; + } = this.state + const { fairCopyProject, parentEntry, resourceEntry } = teiDocument const { isLoggedIn, configLastAction, userID, permissions, remote } = - fairCopyProject; - const readOnly = !teiDocument.isEditable(); + fairCopyProject + const readOnly = !teiDocument.isEditable() const onClickBody = () => { - const { editorView, currentTreeNode } = teiDocument; + const { editorView, currentTreeNode } = teiDocument if ( editorView && !editorView.hasFocus() && currentTreeNode.editorGutterPos === null ) { - editorView.focus(); + editorView.focus() } - }; + } const onFocus = () => { - const { currentTreeNode, editorView } = teiDocument; - const { editorGutterPos } = currentTreeNode; + const { currentTreeNode, editorView } = teiDocument + const { editorGutterPos } = currentTreeNode if (editorGutterPos !== null) { teiDocument.currentTreeNode = { editorGutterPos: null, editorGutterPath: null, - treeID: "main", - }; - navigateFromTreeToEditor(editorView, editorGutterPos); + treeID: 'main', + } + navigateFromTreeToEditor(editorView, editorGutterPos) } - }; + } const onJumpToDrawer = () => { if (this.drawerRef) { - this.drawerRef.focus(); + this.drawerRef.focus() } - }; + } - const { selectedElements } = teiDocument; - const drawerHeight = drawerPinned || selectedElements.length > 0 ? 300 : 50; - const drawerWidthCSS = `calc(100vw - 30px - ${leftPaneWidth}px)`; - const editorHeight = - drawerPinned || selectedElements.length > 0 ? 530 : 180; + const { selectedElements } = teiDocument + const drawerHeight = drawerPinned || selectedElements.length > 0 ? 300 : 50 + const drawerWidthCSS = `calc(100vw - 30px - ${leftPaneWidth}px)` + const editorHeight = drawerPinned || selectedElements.length > 0 ? 530 : 180 - const editorHeightCSS = `calc(100% - ${editorHeight}px)`; - const editorWidthCSS = `calc(100vw - 10px - ${leftPaneWidth}px)`; + const editorHeightCSS = `calc(100% - ${editorHeight}px)` + const editorWidthCSS = `calc(100vw - 10px - ${leftPaneWidth}px)` const editorStyle = { minWidth: editorWidthCSS, maxHeight: editorHeightCSS, - }; - const style = hidden ? { display: "none" } : {}; + } + const style = hidden ? { display: 'none' } : {} - const canConfig = canConfigAdmin(permissions); - const lockStatus = getConfigStatus(configLastAction, userID); - const canEditConfig = - !remote || (canConfig && lockStatus === "checked_out"); + const canConfig = canConfigAdmin(permissions) + const lockStatus = getConfigStatus(configLastAction, userID) + const canEditConfig = !remote || (canConfig && lockStatus === 'checked_out') - const { keyMap, handlers } = this.getMainEditorHotKeyConfig(); + const { keyMap, handlers } = this.getMainEditorHotKeyConfig() return ( -
+
{!hidden && ( @@ -440,11 +435,11 @@ export default class TEIEditor extends Component { onClick={onClickBody} style={editorStyle} onScroll={this.onScrollEditor} - className="body" + className='body' > {!hidden && ( { - this.drawerRef = el; + this.drawerRef = el }} drawerPinned={drawerPinned} onDrawerPinToggle={this.onDrawerPinToggle} @@ -499,14 +494,14 @@ export default class TEIEditor extends Component { onAlertMessage={onAlertMessage} onProjectSettings={onProjectSettings} onChangeMenu={(currentSubmenuID) => { - this.setState({ ...this.state, currentSubmenuID }); + this.setState({ ...this.state, currentSubmenuID }) }} onClose={() => { - this.setState({ ...this.state, paletteWindowOpen: false }); + this.setState({ ...this.state, paletteWindowOpen: false }) }} > )}
- ); + ) } } diff --git a/src/render/css/TEIElementStyles.css b/src/render/css/TEIElementStyles.css index 334716e4..43e1ae8d 100644 --- a/src/render/css/TEIElementStyles.css +++ b/src/render/css/TEIElementStyles.css @@ -23,9 +23,9 @@ min-height: 30px; } -__annomark__ { +__ANNOMARK__ { background-color: rgba(0, 0, 255, 0.1); - mix-blend-mode: multiply; + mix-blend-mode: mul; } tei-textnode0, diff --git a/src/render/model/FairCopyProject.js b/src/render/model/FairCopyProject.js index 305d791d..a875d17e 100644 --- a/src/render/model/FairCopyProject.js +++ b/src/render/model/FairCopyProject.js @@ -4,10 +4,10 @@ import TEIDocument from "./TEIDocument" import FacsDocument from "./FacsDocument" import TEISchema from "./TEISchema" import IDMap from "./IDMap" -import {teiHeaderTemplate, teiTextTemplate, teiStandOffTemplate, teiSourceDocTemplate } from "./tei-template" -import {saveConfig, updateColorCodingStyles} from "./faircopy-config" -import {facsTemplate} from "./tei-template" -import {importResource} from "./import-tei" +import { teiHeaderTemplate, teiTextTemplate, teiStandOffTemplate, teiSourceDocTemplate } from "./tei-template" +import { saveConfig, updateColorCodingStyles } from "./faircopy-config" +import { facsTemplate } from "./tei-template" +import { importResource } from "./import-tei" import { getBlankResourceMap, getUniqueResourceID } from './id-map' import { isLoggedIn } from './cloud-api/auth' @@ -27,34 +27,34 @@ export default class FairCopyProject { this.projectFilePath = projectData.projectFilePath this.loadManifest(projectData.fairCopyManifest) this.fairCopyConfig = projectData.fairCopyConfig - updateColorCodingStyles( this.fairCopyConfig.colorCodings ) + updateColorCodingStyles(this.fairCopyConfig.colorCodings) this.baseConfigJSON = projectData.baseConfig this.teiSchema = new TEISchema(projectData.teiSchema) - this.idMap = new IDMap(projectData.idMap) + this.idMap = new IDMap(projectData.idMap) this.updateListeners = [] } - notifyListeners(messageType,resource) { - for( const listener of this.updateListeners ) { - listener(messageType,resource) + notifyListeners(messageType, resource) { + for (const listener of this.updateListeners) { + listener(messageType, resource) } } - onResourceOpened(resourceEntry, parentEntry, resourceData) { - const{ type } = resourceEntry + onResourceOpened(resourceEntry, parentEntry, resourceData, annotationData) { + const { type } = resourceEntry // for teidocs, just record resource entry and return - if( type === 'teidoc' ) { - return null + if (type === 'teidoc') { + return null } - + let resource = null - if( type === 'text' || type === 'header' || type === 'standOff' || type === 'sourceDoc' ) { - resource = new TEIDocument( resourceEntry, parentEntry, this ) + if (type === 'text' || type === 'header' || type === 'standOff' || type === 'sourceDoc') { + resource = new TEIDocument(resourceEntry, parentEntry, this, null, annotationData) this.addUpdateListener(resource.onResourceUpdated) resource.load(resourceData) - } else if( type === 'facs' ) { - resource = new FacsDocument( resourceEntry, parentEntry, this, resourceData ) + } else if (type === 'facs') { + resource = new FacsDocument(resourceEntry, parentEntry, this, resourceData) this.addUpdateListener(resource.onResourceUpdated) } else { throw new Error("Tried to open unknown resource type.") @@ -72,7 +72,7 @@ export default class FairCopyProject { } removeUpdateListener(listener) { - this.updateListeners = this.updateListeners.filter( l => l !== listener ) + this.updateListeners = this.updateListeners.filter(l => l !== listener) } previewResource(resourceEntry) { @@ -90,161 +90,161 @@ export default class FairCopyProject { this.permissions = fairCopyManifest.permissions this.configLastAction = fairCopyManifest.configLastAction } - - updateResource( resourceEntry ) { - fairCopy.ipcSend('updateResource', resourceEntry ) + + updateResource(resourceEntry) { + fairCopy.ipcSend('updateResource', resourceEntry) } getNextSurfaceID(parentEntry) { - return parentEntry ? this.idMap.nextSurfaceID(parentEntry.localID) : 0 + return parentEntry ? this.idMap.nextSurfaceID(parentEntry.localID) : 0 } - - newResource( name, localID, type, parentResourceID ) { + + newResource(name, localID, type, parentResourceID) { const resourceEntry = { id: uuidv4(), localID, - name, + name, type, parentResource: parentResourceID, ...cloudInitialConfig } - if( type === 'teidoc' ) { + if (type === 'teidoc') { const headerEntry = { id: uuidv4(), localID: 'header', - name: 'TEI Header', + name: 'TEI Header', type: 'header', remote: false, parentResource: resourceEntry.id, ...cloudInitialConfig - } + } this.addResource(resourceEntry, "", getBlankResourceMap(resourceEntry.id, resourceEntry.type)) this.addResource(headerEntry, teiHeaderTemplate(name), getBlankResourceMap(headerEntry.id, headerEntry.type)) - } else if( type === 'text' ) { + } else if (type === 'text') { this.addResource(resourceEntry, teiTextTemplate, getBlankResourceMap(resourceEntry.id, resourceEntry.type)) - } else if( type === 'facs' ) { + } else if (type === 'facs') { // add a blank facs const facs = { surfaces: [] } const xml = facsTemplate(facs) - this.addResource(resourceEntry,xml,getBlankResourceMap(resourceEntry.id, resourceEntry.type)) - } else if( type === 'standOff') { + this.addResource(resourceEntry, xml, getBlankResourceMap(resourceEntry.id, resourceEntry.type)) + } else if (type === 'standOff') { this.addResource(resourceEntry, teiStandOffTemplate, getBlankResourceMap(resourceEntry.id, resourceEntry.type)) - } else if( type === 'sourceDoc') { - this.addResource(resourceEntry, teiSourceDocTemplate, getBlankResourceMap(resourceEntry.id, resourceEntry.type)) + } else if (type === 'sourceDoc') { + this.addResource(resourceEntry, teiSourceDocTemplate, getBlankResourceMap(resourceEntry.id, resourceEntry.type)) } else { throw new Error("Attempted to create unknown document type.") } } - removeResources( resourceIDs ) { - fairCopy.ipcSend('removeResources', resourceIDs ) + removeResources(resourceIDs) { + fairCopy.ipcSend('removeResources', resourceIDs) } recoverResources(resourceIDs) { - fairCopy.ipcSend('recoverResources', resourceIDs ) - } + fairCopy.ipcSend('recoverResources', resourceIDs) + } - openResource( resourceID ) { - fairCopy.ipcSend('openResource', resourceID ) + openResource(resourceID) { + fairCopy.ipcSend('openResource', resourceID) } - async importResource(importData,parentEntry) { + async importResource(importData, parentEntry) { try { - const resources = await importResource(importData,parentEntry,this) - if( importData.options?.replaceResource ) { - if( resources.length > 1 ) { - fairCopy.ipcSend( 'replaceTEIDocument', resources ) + const resources = await importResource(importData, parentEntry, this) + if (importData.options?.replaceResource) { + if (resources.length > 1) { + fairCopy.ipcSend('replaceTEIDocument', resources) } else { - fairCopy.ipcSend( 'replaceResource', resources[0], parentEntry ) + fairCopy.ipcSend('replaceResource', resources[0], parentEntry) } } else { - for( const resource of resources ) { + for (const resource of resources) { const { resourceEntry, content, resourceMap } = resource - this.addResource( resourceEntry, content, resourceMap ) - } + this.addResource(resourceEntry, content, resourceMap) + } } return { error: false, errorMessage: null, resourceCount: resources.length } - } catch(e) { + } catch (e) { return { error: true, errorMessage: e.message, resourceCount: 0 } - } + } } - saveFairCopyConfig( nextFairCopyConfig=null, lastAction=null ) { - if( nextFairCopyConfig ) { + saveFairCopyConfig(nextFairCopyConfig = null, lastAction = null) { + if (nextFairCopyConfig) { this.fairCopyConfig = nextFairCopyConfig - updateColorCodingStyles( this.fairCopyConfig.colorCodings ) + updateColorCodingStyles(this.fairCopyConfig.colorCodings) } - if( lastAction ) { + if (lastAction) { this.configLastAction = lastAction } saveConfig(this.fairCopyConfig, lastAction) } - checkInConfig( nextFairCopyConfig ) { + checkInConfig(nextFairCopyConfig) { this.fairCopyConfig = nextFairCopyConfig - updateColorCodingStyles( this.fairCopyConfig.colorCodings ) + updateColorCodingStyles(this.fairCopyConfig.colorCodings) const firstAction = !!this.configLastAction.firstAction fairCopy.ipcSend('checkInConfig', nextFairCopyConfig, firstAction) } checkOutConfig() { - if( !this.configLastAction ) { + if (!this.configLastAction) { // if there's no last action, that means server doesn't have config, consider it checked out const lastAction = { action_type: 'check_out', user: { id: this.userID }, firstAction: true } this.configLastAction = lastAction - saveConfig( this.fairCopyConfig, lastAction ) + saveConfig(this.fairCopyConfig, lastAction) return true } else { // note: state is updated once we hear that the check out was a success - fairCopy.ipcSend('checkOutConfig') + fairCopy.ipcSend('checkOutConfig') return false } } - + configCheckedOut() { // create a synthentic lastAction to keep until we get a real one from server. this.configLastAction = { action_type: 'check_out', user: { id: this.userID } } - saveConfig( this.fairCopyConfig, this.configLastAction ) + saveConfig(this.fairCopyConfig, this.configLastAction) } // take the resources and move them into the parent ID - moveResources( resourceEntries, targetParentEntry ) { - for( const resourceEntry of resourceEntries ) { - if( resourceEntry.parentResource === targetParentEntry?.id ) continue + moveResources(resourceEntries, targetParentEntry) { + for (const resourceEntry of resourceEntries) { + if (resourceEntry.parentResource === targetParentEntry?.id) continue - if( targetParentEntry ) { + if (targetParentEntry) { const childLocalIDs = Object.keys(this.idMap.idMap[targetParentEntry.localID].ids) - resourceEntry.localID = getUniqueResourceID( resourceEntry.type, childLocalIDs, resourceEntry.localID ) + resourceEntry.localID = getUniqueResourceID(resourceEntry.type, childLocalIDs, resourceEntry.localID) resourceEntry.parentResource = targetParentEntry.id } else { const childLocalIDs = Object.keys(this.idMap.idMap) - resourceEntry.localID = getUniqueResourceID( resourceEntry.type, childLocalIDs, resourceEntry.localID ) + resourceEntry.localID = getUniqueResourceID(resourceEntry.type, childLocalIDs, resourceEntry.localID) resourceEntry.parentResource = null } // update resource - this.updateResource( resourceEntry ) + this.updateResource(resourceEntry) } } - addResource( resourceEntry, content, resourceMap ) { - fairCopy.ipcSend('addResource', resourceEntry, content, resourceMap ) + addResource(resourceEntry, content, resourceMap) { + fairCopy.ipcSend('addResource', resourceEntry, content, resourceMap) } getProjectInfo() { - return { - name: this.projectName, - description: this.description, + return { + name: this.projectName, + description: this.description, projectFilePath: this.projectFilePath, userID: this.userID, serverURL: this.serverURL, remote: this.remote, - permissions: [ ...this.permissions ] - } + permissions: [...this.permissions] + } } - updateProjectInfo( projectInfo ) { + updateProjectInfo(projectInfo) { this.projectName = projectInfo.name this.description = projectInfo.description this.permissions = projectInfo.permissions @@ -252,41 +252,41 @@ export default class FairCopyProject { // if this project is in the recent projects list, update its info in localStorage let projects = localStorage.getItem('recentProjects'); projects = projects ? JSON.parse(projects) : [] - const recentProjectData = projects.find( (project) => project.projectFilePath === this.projectFilePath ) - if( recentProjectData ) { + const recentProjectData = projects.find((project) => project.projectFilePath === this.projectFilePath) + if (recentProjectData) { recentProjectData.projectName = this.projectName recentProjectData.description = this.description localStorage.setItem('recentProjects', JSON.stringify(projects)); } - fairCopy.ipcSend('updateProjectInfo', projectInfo ) + fairCopy.ipcSend('updateProjectInfo', projectInfo) } hasID(xmlID, localID) { return this.idMap.hasID(xmlID, localID) } - areEditable = ( resourceEntries ) => { - for( const resourceEntry of resourceEntries ) { - if( !this.isEditable(resourceEntry) ) return false + areEditable = (resourceEntries) => { + for (const resourceEntry of resourceEntries) { + if (!this.isEditable(resourceEntry)) return false } return true } - isEditable = ( resourceEntry ) => { + isEditable = (resourceEntry) => { // can always edit in a local project - if( !this.remote ) return true - return isEntryEditable(resourceEntry, this.userID ) + if (!this.remote) return true + return isEntryEditable(resourceEntry, this.userID) } isLoggedIn = () => { - if( !this.remote ) return false - return isLoggedIn( this.userID, this.serverURL ) + if (!this.remote) return false + return isLoggedIn(this.userID, this.serverURL) } } -export function isEntryEditable( resourceEntry, userID ) { - if( resourceEntry.local ) return true - if( resourceEntry.deleted ) return false +export function isEntryEditable(resourceEntry, userID) { + if (resourceEntry.local) return true + if (resourceEntry.deleted) return false // can only edit files checked out by me const { lastAction } = resourceEntry @@ -295,9 +295,9 @@ export function isEntryEditable( resourceEntry, userID ) { return actionType === 'check_out' && actor === userID } -export function isCheckedOutRemote( resourceEntry, userID ) { - if( resourceEntry.local ) return true - if( resourceEntry.deleted ) return false +export function isCheckedOutRemote(resourceEntry, userID) { + if (resourceEntry.local) return true + if (resourceEntry.deleted) return false // can only edit files checked out by me const { lastAction } = resourceEntry diff --git a/src/render/model/TEIDocument.js b/src/render/model/TEIDocument.js index bd4031a9..93695d61 100644 --- a/src/render/model/TEIDocument.js +++ b/src/render/model/TEIDocument.js @@ -9,7 +9,7 @@ import { gapCursor } from "prosemirror-gapcursor" import { v4 as uuidv4 } from 'uuid' import { teiHeaderTemplate, teiTextTemplate, teiStandOffTemplate, teiSourceDocTemplate } from "./tei-template" -import { parseText, proseMirrorToDOM, serializeText, addTextNodes, addAnnotations } from "./xml" +import { parseText, proseMirrorToDOM, serializeText, addTextNodes, addAnnotations, getNodeMap } from "./xml" import { applySystemFlags } from "./system-flags" import { mapResource } from "./id-map" import { generateGutterMarks } from './gutter-marks' @@ -21,7 +21,7 @@ const gutterTopReadOnly = 115 export default class TEIDocument { - constructor(resourceEntry, parentEntry, fairCopyProject, teiSchema = null) { + constructor(resourceEntry, parentEntry, fairCopyProject, teiSchema = null, annotationData = []) { this.subDocs = {} this.subDocCounter = 0 this.errors = [] @@ -47,6 +47,7 @@ export default class TEIDocument { ] this.initialState = this.editorInitialState() this.changedSinceLastSave = false + this.annotationData = annotationData } onResourceUpdated = (eventType, resource) => { @@ -175,7 +176,7 @@ export default class TEIDocument { return this.parentEntry ? this.parentEntry.localID : this.resourceEntry.localID } - finalizeEditorView(editorView, annotationData) { + finalizeEditorView(editorView) { this.editorView = editorView const { tr, doc } = editorView.state tr.setSelection(TextSelection.create(doc, 0)) @@ -183,7 +184,7 @@ export default class TEIDocument { editorView.dispatch(tr); addTextNodes(editorView.state, editorView.dispatch) if (this.resourceType === 'text') { - addAnnotations(editorView.state, annotationData, editorView.dispatch) + addAnnotations(editorView.state, this.annotationData, editorView.dispatch) } editorView.focus() this.changedSinceLastSave = false @@ -296,6 +297,59 @@ export default class TEIDocument { this.fairCopyProject.previewResource(previewEntry) } + updateAnnotationData(annoData) { + for (const annoId in annoData) { + const data = annoData[annoId] + + let curData = this.annotationData.find(a => a.id === annoId) + + if (curData) { + let newPath = '' + const annoPaths = curData.path.split(' ') + + for (let i = 0; i < 2; i++) { + // Find the part of the xpath which is the 'text' node + const findTextStart = annoPaths[i].indexOf('text') + let newData = i === 0 ? data.start : data.end + const newOffset = i === 0 ? data.startOffset : data.endOffset + + if (findTextStart > -1) { + // Search for the next '/'. This is the beginning of the section we want to update + const textStart = annoPaths[i].indexOf('/', findTextStart) + + if (textStart > -1) { + // Begin the path with the same xpath up to 'text', the +1 accounts for the '/' + newPath += annoPaths[i].slice(0, textStart + 1) + + // Now fill in the rest of the new path + newData.forEach((a, idx) => { + newPath += `${a.node}[${a.id}]` + if (idx < newData.length - 1) { + newPath += '/' + } else { + newPath += `::${newOffset}` + } + }) + + newPath += (i === 0 ? ' ' : '') + } else { + console.log('Failed annotation update') + return + } + } else { + console.error('Failed to find annotation data start') + return + } + } + + console.log('Writing new path!') + console.log('Old path: ', curData.path) + console.log('New path: ', newPath) + curData.path = newPath + } + } + } + save() { const editorState = this.editorView.state const teiSchema = this.getTEISchema() @@ -304,6 +358,10 @@ export default class TEIDocument { const messageID = uuidv4() fairCopy.ipcSend('requestSave', messageID, this.resourceID, fileContents) + if (this.resourceType === 'text' && this.annotationData.length > 0) { + const annoId = uuidv4() + fairCopy.ipcSend('updateStandoffAnnotations', this.resourceID, this.annotationData) + } this.lastMessageID = messageID this.changedSinceLastSave = false diff --git a/src/render/model/TEISchema.js b/src/render/model/TEISchema.js index c38d32ef..a6132f11 100644 --- a/src/render/model/TEISchema.js +++ b/src/render/model/TEISchema.js @@ -123,10 +123,10 @@ export default class TEISchema { } // This is a test of adding the annotations to the document - marks['__annoMark__'] = { - toDOM(node) { let { id } = node.attrs; return ["__annoMark__", { id }, 0] }, + marks['__ANNOMARK__'] = { + toDOM(node) { let { id } = node.attrs; return ["__ANNOMARK__", { id }, 0] }, parseDOM: [{ - tag: "__annoMark__", getAttrs(el) { + tag: "__ANNOMARK__", getAttrs(el) { return { id: el.getAttribute('id') } } }], diff --git a/src/render/model/editor-navigation.js b/src/render/model/editor-navigation.js index d7fc7b0e..3c8e2efd 100644 --- a/src/render/model/editor-navigation.js +++ b/src/render/model/editor-navigation.js @@ -228,7 +228,7 @@ export function getSelectedElements(teiDocument, noteID) { } else if (selection) { if (selection.node) { const name = selection.node.type.name - if (!asides.includes(name) && !name.includes('globalNode') && !name.includes('annoMark')) { + if (!asides.includes(name) && !name.includes('globalNode') && !name.includes('ANNOMARK')) { elements.push(selection.node) } } else { @@ -239,7 +239,7 @@ export function getSelectedElements(teiDocument, noteID) { const { $anchor } = selection const highlightRanges = getHighlightRanges(doc, $anchor) for (const highlightRange of highlightRanges) { - if (!highlightRange.mark.type.name.includes('annoMark')) { + if (!highlightRange.mark.type.name.includes('ANNOMARK')) { elements.push(highlightRange.mark) } } diff --git a/src/render/model/xml.js b/src/render/model/xml.js index e5d82804..4ce321f0 100644 --- a/src/render/model/xml.js +++ b/src/render/model/xml.js @@ -1,10 +1,4 @@ import { DOMSerializer, Fragment } from "prosemirror-model" -import { AddMarkStep, Transform } from "prosemirror-transform" -import xpath from 'xpath' -import serialize from "w3c-xmlserializer" -import { RELEASE_TYPES } from "semver" -import { find } from "../../../webpack.rules" -import { generateMarks } from './commands'; // These elements are processed in XSLT by the xsl:strip-space command. This list is from xml/tei/odd/stripspace.xsl.model, TEI v4.1.0 const xmlStripSpaceNames = "TEI abstract additional address adminInfo altGrp altIdentifier alternate analytic annotation annotationBlock app appInfo application arc argument att attDef attList availability back biblFull biblStruct bicond binding bindingDesc body broadcast cRefPattern calendar calendarDesc castGroup castList category certainty char charDecl charProp choice cit classDecl classSpec classes climate cond constraintSpec content correction correspAction correspContext correspDesc custodialHist dataRef dataSpec datatype decoDesc dimensions div div1 div2 div3 div4 div5 div6 div7 divGen docTitle eLeaf eTree editionStmt editorialDecl elementSpec encodingDesc entry epigraph epilogue equipment event exemplum fDecl fLib facsimile figure fileDesc floatingText forest front fs fsConstraints fsDecl fsdDecl fvLib gap gi glyph graph graphic group handDesc handNotes history hom hyphenation iNode if imprint incident index interpGrp interpretation join joinGrp keywords kinesic langKnowledge langUsage layoutDesc leaf lg linkGrp list listAnnotation listApp listBibl listChange listEvent listForest listNym listObject listOrg listPerson listPlace listPrefixDef listRef listRelation listTranspose listWit location locusGrp macroSpec media metDecl model modelGrp modelSequence moduleRef moduleSpec monogr msContents msDesc msFrag msIdentifier msItem msItemStruct msPart namespace node normalization notatedMusic notesStmt nym object objectDesc objectIdentifier org paramList paramSpec particDesc performance person personGrp persona physDesc place population postscript precision prefixDef profileDesc projectDesc prologue publicationStmt punctuation quotation rdgGrp recordHist recording recordingStmt refsDecl relatedItem relation remarks respStmt respons revisionDesc root row samplingDecl schemaRef schemaSpec scriptDesc scriptStmt seal sealDesc segmentation sequence seriesStmt set setting settingDesc sourceDesc sourceDoc sp spGrp space spanGrp specGrp specList standOff state stdVals styleDefDecl subst substJoin superEntry supportDesc surface surfaceGrp table tagsDecl taxonomy teiCorpus teiHeader terrain text textClass textDesc timeline titlePage titleStmt trait transcriptionDesc transpose tree triangle typeDesc unitDecl unitDef vAlt vColl vDefault vLabel vMerge vNot vRange valItem valList vocal".split(' ') @@ -65,6 +59,10 @@ export function proseMirrorToDOM(content, teiDocument, teiSchema, subDocName) { } renameInterMarks(inter, domFragment, teiDocument.xmlDom) + const annoUpdate = processAnnotations(domFragment, teiDocument.annotationData) + if (Object.keys(annoUpdate).length > 0) { + teiDocument.updateAnnotationData(annoUpdate) + } teiSchema.teiDocuments.pop() return domFragment } @@ -89,7 +87,7 @@ export function findNoteNode(doc, noteID) { } export function synthNameToElementName(nodeName) { - if (nodeName.includes('textNode') || nodeName.includes('globalNode') || nodeName.includes('__annoMark__')) return null + if (nodeName.includes('textNode') || nodeName.includes('globalNode') || nodeName.includes('__ANNOMARK__')) return null return nodeName.endsWith('X') ? nodeName.slice(0, -1) : nodeName.startsWith('mark') ? nodeName.slice('mark'.length) : nodeName } @@ -142,6 +140,160 @@ function parseInterNodes(textEl, teiSchema, xmlDom) { } } +// Find the annotation marks (__ANNOMARK__) and update the annotation data +// and remove from the xml DOM +function processAnnotations(textEl, annotationData) { + + function getXPath(element) { + let xpath = ''; + let currentElement = element; + + while (currentElement !== null && currentElement.localName) { + let tagName = currentElement.localName; + let index = 1; + + // Check for siblings with the same tag name + for (let sibling = currentElement.previousSibling; sibling; sibling = sibling.previousSibling) { + if (sibling.nodeType === 1 && sibling.localName === tagName) { + index++; + } + } + + // Add the tag name and index to the XPath + xpath = `/${tagName}[${index}]` + xpath; + currentElement = currentElement.parentNode; + } + + return xpath; + } + + function removeTags(str) { + if ((str === null) || (str === '')) + return ''; + else + str = str.toString(); + + // Regular expression to identify XML tags in + // the input string. Replacing the identified + // XML tag with a null string. + return str.replace(/(<([^>]+)>)/ig, ''); + } + + // Get all the annoMarks in the doc + let findMap = {} + + if (annotationData.length === 0) { + return findMap + } + + const annoMarks = textEl.querySelectorAll('__ANNOMARK__'); + for (let i = 0; i < annoMarks.length; i++) { + const annoMark = annoMarks[i] + + const parentEl = annoMark.parentElement + console.log('annoMark: ', annoMark) + // Get the current path + const xpath = getXPath(parentEl) + console.log('XPath: ', xpath) + + // Convert to a path map + let newPath = getNodeMap(`${xpath} ${xpath}`) + console.log('New Paths: ', newPath) + + // Find the matching annotation + const findMatch = annotationData.find(a => a.id === annoMark.getAttribute('id')) + + if (findMatch) { + console.log(`Found Anno: ${JSON.stringify(findMatch, null, 2)}`) + const parentText = parentEl.innerText + + if (!findMap[findMatch.id]) { + findMap[findMatch.id] = { + id: findMap.id, + start: newPath.start, + end: [], + mark: annoMark, + markEnd: undefined + } + } + + // Update the end path + findMap[findMatch.id].end = newPath.end + findMap[findMatch.id].markEnd = annoMark + + } else { + console.log('Matching annotation not found!!') + } + } + + console.log('Inner: ', textEl.innerHTML) + + // We now use the innerHTML string to find offsets and remove the annomarks + let innerString = textEl.firstChild.innerHTML + let curIndex = 0 + + if (!innerString) { + return + } + + console.log('Inner: ', innerString) + while (curIndex !== -1) { + // Find the next anno mark + curIndex = innerString.indexOf('<__annomark__', curIndex) + if (curIndex > -1) { + console.log('Anno Found!') + // get the id of the anno + const idLoc = innerString.indexOf('id="', curIndex) + if (idLoc > -1) { + // ID is a fixed size of 44, the +4 accounts for 'id="' + const idAnno = innerString.substring(idLoc + 4, idLoc + 44) + console.log('Anno ID: ', idAnno) + + // Find the tag that contains this tag + // It should be the current index minus the index of the previous node + // const annoOffsetTagStart = findPreviousStartTagIndex(innerString, curIndex) + const annoOffsetTagStart = innerString.lastIndexOf(`<${findMap[idAnno].mark.parentElement.localName}`, curIndex) + const annoOffsetStart = innerString.indexOf('>', annoOffsetTagStart) + if (annoOffsetStart > -1) { + // Remove this tag, all annoMarks opening tags are 60 chatacters long + // <__annomark__ id = "uid-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"> + innerString = innerString.slice(0, curIndex) + innerString.slice(curIndex + 60) + + // We may have already have the beginning of this one + if (!findMap[idAnno].startOffset) { + // Remove any tags, the +1 accounts for the '>' + const offset = removeTags(innerString.slice(annoOffsetStart + 1, curIndex)).length + findMap[idAnno].startOffset = offset + } + + // We now have to find the end of the annotation + // This will work for annotations within annotations since + // this is accomplished with multiple tag blocks, + const annoEndIndex = innerString.indexOf('') + const annoOffsetTagStart = innerString.lastIndexOf(`<${findMap[idAnno].markEnd.parentElement.localName}`, annoEndIndex) + const annoOffsetEnd = innerString.indexOf('>', annoOffsetTagStart) + + // The + 1 accounts for the '>' + // Remove any tags + const offset = removeTags(innerString.slice(annoOffsetEnd + 1, annoEndIndex)).length + findMap[idAnno].endOffset = offset + + // Remove the end tag, all annoMarks closing tags are 15 characters log + innerString = innerString.slice(0, annoEndIndex) + innerString.slice(annoEndIndex + 15) + } + + } + } + } + + console.log('Old Data: ', JSON.stringify(annotationData, null, 2)) + console.log('New Data: ', JSON.stringify(findMap, null, 2)) + + textEl.firstChild.innerHTML = innerString + + return findMap +} + // Remove whitespace from children of xmlStripSpaceNames elements, // as per TEI Guidelines section 1.3.1.1.6 function stripSpaces(textEl) { @@ -220,52 +372,52 @@ export function addTextNodes(state, dispatch = null) { } } -export function addAnnotations(state, annotationData = [], dispatch = null) { +export function getNodeMap(path) { + const arr = path.split(' ') + const start = arr[0] + const end = arr[1] + const ret = { start: [], end: [] } + + for (let i = 0; i < 2; i++) { + let str, input; + if (i === 0) { + str = start; + input = ret.start; + } else { + str = end; + input = ret.end; + } - const { doc, tr } = state + // Always assume that first character is / + let index = 1 + let next = str.substring(index).indexOf('/') + while (next !== -1) { + if (str.substring(index, index + 1) === '/') { + index += 1 + } - function getNodeMap(path) { - const arr = path.split(' ') - const start = arr[0] - const end = arr[1] - const ret = { start: [], end: [] } - - for (let i = 0; i < 2; i++) { - let str, input; - if (i === 0) { - str = start; - input = ret.start; + next = str.substring(index).indexOf('/') + if (next === -1) { + const parse = str.substring(index).split('['); + const id = parse[1].split(']') + const offset = id[1].split("::") + input.push({ node: parse[0], id: id[0], offset: offset[1] }) } else { - str = end; - input = ret.end; + const parse = str.substring(index, index + next).split('['); + const id = parse[1].split(']') + input.push({ node: parse[0], id: id[0] }) } - // Always assume that first character is / - let index = 1 - let next = str.substring(index).indexOf('/') - while (next !== -1) { - if (str.substring(index, index + 1) === '/') { - index += 1 - } + index = index + next + 1; + } + } - next = str.substring(index).indexOf('/') - if (next === -1) { - const parse = str.substring(index).split('['); - const id = parse[1].split(']') - const offset = id[1].split("::") - input.push({ node: parse[0], id: id[0], offset: offset[1] }) - } else { - const parse = str.substring(index, index + next).split('['); - const id = parse[1].split(']') - input.push({ node: parse[0], id: id[0] }) - } + return ret; +} - index = index + next + 1; - } - } +export function addAnnotations(state, annotationData = [], dispatch = null) { - return ret; - } + const { doc, tr } = state function findMarkPositionInDescendant(markType, parent) { let markPos = { start: -1, end: -1 }; @@ -347,8 +499,6 @@ export function addAnnotations(state, annotationData = [], dispatch = null) { const endNode = findNode(doc, paths.end, endIndex, 1, 0) if (startNode && endNode) { - console.log('startNode: ', startNode) - console.log('endNode', endNode) let startPosition = 0 doc.descendants((node, pos) => { if (node === startNode.node) { @@ -373,7 +523,7 @@ export function addAnnotations(state, annotationData = [], dispatch = null) { //console.log('markStart: ', markStart, ', markEnd: ', markEnd) console.log('Text Annotated: ', doc.textBetween(markStart, markEnd)) - let mark = doc.type.schema.marks['__annoMark__'].create({ id: d.id }) + let mark = doc.type.schema.marks['__ANNOMARK__'].create({ id: d.id }) tr.addMark(markStart, markEnd, mark) } }) diff --git a/src/render/workers/project-archive-worker.js b/src/render/workers/project-archive-worker.js index cebef7bb..36ac08ca 100644 --- a/src/render/workers/project-archive-worker.js +++ b/src/render/workers/project-archive-worker.js @@ -4,6 +4,7 @@ import { getResourceAsync, getResourcesAsync } from "../model/cloud-api/resource import { serializeResource } from "../model/serialize-xml" import { renderTEIDocument } from "../model/editioncrafter/render" const { parseStandoff } = require('./parse-standoff') +const { serializeStandoff } = require('./serialize-standoff') const fs = window.fairCopy.getFs() const os = window.fairCopy.getOs() @@ -362,6 +363,18 @@ export function projectArchive(msg, workerMethods, workerData) { }) } break + // { messageType: 'save-annotation-data', resourceID: standoffEntry.id, data: data } + case 'save-annotation-data': + { + const { resourceID, data } = msg + // Read in the current standoff and update with new paths + // TODO: in the future we may want to be able to update the annotations themselves + readUTF8(resourceID, zip).then(resource => { + const newStandoff = serializeStandoff(resource, data) + writeUTF8(resourceID, newStandoff, zip) + }) + } + break case 'request-index': { const { resourceID } = msg diff --git a/src/render/workers/serialize-standoff.js b/src/render/workers/serialize-standoff.js new file mode 100644 index 00000000..8086414d --- /dev/null +++ b/src/render/workers/serialize-standoff.js @@ -0,0 +1,37 @@ +export function serializeStandoff(xml, newData) { + const parser = new DOMParser() + + const xmlDoc = parser.parseFromString(xml, "text/xml") + + const standOff = xmlDoc.querySelector('standOff') + + if (standOff) { + const type = standOff.getAttribute('type') + + // if (type && type === 'recogito_studio_annotations') { + const listAnnotation = standOff.querySelector('listAnnotation') + + if (listAnnotation) { + const annotations = listAnnotation.querySelectorAll('annotation') + + annotations.forEach(a => { + const id = a.getAttribute('xml:id') + const path = a.getAttribute('target') + + // Fnd the annotation in the new data + const found = newData.find(d => d.id === id); + if (found) { + if (found.path !== path) { + console.log('Writing new path!') + console.log('Old path: ', path) + console.log('New path: ', found.path) + } + a.setAttribute('target', found.path) + } + }) + } + //} + } + + return '' + xmlDoc.documentElement.outerHTML +} \ No newline at end of file