diff --git a/.DS_Store b/.DS_Store index 62b60b80..a67cca33 100644 Binary files a/.DS_Store and b/.DS_Store differ 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-lock.json b/package-lock.json index ebbffd34..ff970314 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", @@ -478,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" } @@ -4088,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", @@ -4171,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" }, @@ -4198,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", @@ -5723,20 +5674,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 +5688,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 +6390,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", @@ -6900,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", @@ -7324,20 +7226,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 +7240,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 +7259,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", @@ -9463,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", @@ -9516,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", @@ -10119,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" @@ -11593,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", @@ -11934,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", @@ -12059,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", @@ -12113,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" }, @@ -12493,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", @@ -12783,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", @@ -14058,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" } @@ -17834,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", @@ -21005,15 +20734,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", @@ -21116,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", @@ -21142,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", @@ -21158,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", @@ -21183,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", @@ -22202,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" }, @@ -22263,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", @@ -24124,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", @@ -24170,7 +24050,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 +24153,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 +25799,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..2bac4d11 100644 --- a/package.json +++ b/package.json @@ -83,9 +83,10 @@ "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" } -} +} \ No newline at end of file diff --git a/src/data/test-milestone.js b/src/data/test-milestone.js new file mode 100644 index 00000000..b1ecf1c6 --- /dev/null +++ b/src/data/test-milestone.js @@ -0,0 +1,15 @@ +export const data = [ + { + 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" + }, + { + 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/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 6287d708..a5a1c8eb 100644 --- a/src/main-process/FairCopySession.js +++ b/src/main-process/FairCopySession.js @@ -4,23 +4,24 @@ 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 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 +32,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 +50,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 +67,7 @@ class FairCopySession { closeProject() { this.projectStore.close() - if( this.remoteProject ) this.remoteProject.close() + if (this.remoteProject) this.remoteProject.close() this.projectOpen = false } @@ -74,73 +75,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 +154,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 +195,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 +217,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 +225,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 +268,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 +298,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 +335,104 @@ 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) { + saveResource(resourceID, resourceData, updatePreview, annotationData) { 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 + } + + 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] ) { + 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 +440,37 @@ 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) { + 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 +478,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 +487,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 +512,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 +526,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 +559,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 +575,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..054b022f 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,99 @@ 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 ) { + + 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) } - 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 +347,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 +363,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 +384,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 +393,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/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/components/main-window/MainWindow.jsx b/src/render/components/main-window/MainWindow.jsx index 53c0a373..b5783ba2 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, annotationData } = 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, + annotationData[parentEntry.id] + ) + 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/ParameterDrawer.jsx b/src/render/components/main-window/tei-editor/ParameterDrawer.jsx index ab14188f..702c3164 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/components/main-window/tei-editor/TEIEditor.jsx b/src/render/components/main-window/tei-editor/TEIEditor.jsx index 7acd88ee..41e3cdd8 100644 --- a/src/render/components/main-window/tei-editor/TEIEditor.jsx +++ b/src/render/components/main-window/tei-editor/TEIEditor.jsx @@ -1,424 +1,507 @@ import React, { Component } from 'react' -import {EditorView} from "prosemirror-view" -import { debounce } from "debounce"; +import { EditorView } from 'prosemirror-view' +import { debounce } from 'debounce' import { HotKeys } from 'react-hotkeys' -// import applyDevTools from "prosemirror-dev-tools"; +import applyDevTools from 'prosemirror-dev-tools' -import ProseMirrorComponent from "../../common/ProseMirrorComponent" -import EditorGutter from "./EditorGutter" -import StructurePalette from "./StructurePalette" +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 { + 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" +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 + constructor() { + super() + this.state = { + noteID: null, + notePopupAnchorEl: null, + elementMenuOptions: null, + paletteWindowOpen: false, + drawerPinned: false, + currentSubmenuID: 0, } - - 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 ) + 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 - } + if (key === 'ArrowRight') { + arrowNavToNote(this.openNotePopup, teiDocument, 1) + return } - // 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 - } - } - - 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() + this.openNotePopup(nextID, event.target) } + } + } else { + this.closeNotePopup() } - onTogglePalette = () => { - const { paletteWindowOpen } = this.state - this.setState({...this.state, paletteWindowOpen: !paletteWindowOpen}) + // 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() } - - onDrawerPinToggle = () => { - const { drawerPinned } = this.state - this.setState({...this.state, drawerPinned: !drawerPinned}) + } + + 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 }) } - - openNotePopup = (noteID, notePopupAnchorEl) => { - this.setState({...this.state, noteID, notePopupAnchorEl }) + } + + 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) + } } - closeNotePopup = () => { - const { noteID, notePopupAnchorEl } = this.state - if( noteID !== null || notePopupAnchorEl !== null ) { - this.setState({...this.state, noteID: null, notePopupAnchorEl: null }) - } + 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) } - - 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 }) + } + + 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() + } } - 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) + const onFocus = () => { + const { currentTreeNode, editorView } = teiDocument + const { editorGutterPos } = currentTreeNode + if (editorGutterPos !== null) { + teiDocument.currentTreeNode = { + editorGutterPos: null, + editorGutterPath: null, + treeID: 'main', } + navigateFromTreeToEditor(editorView, editorGutterPos) + } } - 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 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 { 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} )}} - > - } -
- ) + 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/css/TEIElementStyles.css b/src/render/css/TEIElementStyles.css index 68bcd801..43e1ae8d 100644 --- a/src/render/css/TEIElementStyles.css +++ b/src/render/css/TEIElementStyles.css @@ -1,100 +1,120 @@ - -[__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); + mix-blend-mode: mul; } -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; +*[rend~='uppercase'] { + text-transform: uppercase; } -*[rend~="italic"] { - font-style: italic; +*[rend~='italic'] { + font-style: italic; } -*[rend~="bold"] { - font-weight: bold; +*[rend~='bold'] { + font-weight: bold; } -*[rend~="red"] { - color: red; +*[rend~='red'] { + color: red; } -*[rend~="green"] { - color: green; +*[rend~='green'] { + color: green; } -*[rend~="blue"] { - color: blue; +*[rend~='blue'] { + color: blue; } -*[rend~="purple"] { - color: purple; +*[rend~='purple'] { + color: purple; } -*[rend~="orange"] { - color: orange; +*[rend~='orange'] { + color: orange; } -*[rend~="brown"] { - color: brown; +*[rend~='brown'] { + color: brown; } -*[rend~="gray"] { - color: gray; +*[rend~='gray'] { + color: gray; } -*[rend~="small"] { - font-size: small; +*[rend~='small'] { + font-size: small; } -*[rend~="medium"] { - font-size: large; +*[rend~='medium'] { + font-size: large; } -*[rend~="large"] { - font-size: x-large; +*[rend~='large'] { + font-size: x-large; } - -*[rend~="underline"] { - text-decoration: underline; +*[rend~='underline'] { + text-decoration: underline; } -*[rend~="line-through"] { - text-decoration: line-through; - } - +*[rend~='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/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 77e4ac5f..93695d61 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, getNodeMap } 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, annotationData = []) { this.subDocs = {} this.subDocCounter = 0 this.errors = [] @@ -47,32 +47,33 @@ export default class TEIDocument { ] this.initialState = this.editorInitialState() this.changedSinceLastSave = false + this.annotationData = annotationData } - 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 +84,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 +103,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 +180,21 @@ 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) + if (this.resourceType === 'text') { + addAnnotations(editorView.state, this.annotationData, editorView.dispatch) + } editorView.focus() 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 +206,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 +217,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 +252,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 } @@ -293,16 +297,73 @@ 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() - + const fileContents = serializeText(editorState.doc, this, teiSchema) 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 } } \ No newline at end of file diff --git a/src/render/model/TEISchema.js b/src/render/model/TEISchema.js index 1e65c823..a6132f11 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,42 @@ 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(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", } 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 +174,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 +206,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 +242,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 +267,35 @@ 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] + if (!elementSpec) { + console.log('Whoops!') + } 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 +310,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 +344,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/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 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 8b71f187..4ce321f0 100644 --- a/src/render/model/xml.js +++ b/src/render/model/xml.js @@ -7,7 +7,7 @@ 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) + parseInterNodes(textEl, teiSchema, teiDocument.xmlDom) const domParser = subDocName !== 'text' ? teiSchema.docNodeParsers[subDocName] : teiSchema.domParser const doc = domParser.parse(textEl) teiSchema.teiDocuments.pop() @@ -18,47 +18,51 @@ 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}`) } 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 } @@ -70,98 +74,252 @@ 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') || nodeName.includes('__ANNOMARK__')) 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) } } } } +// 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) { - 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 +327,249 @@ 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 } } +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; + } + + // 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; +} + +export function addAnnotations(state, annotationData = [], dispatch = null) { + + const { doc, tr } = state + + 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); + if (node.type.name === path[index].node) { + if ((idCount < 0 && node.attrs['@xml:id'] === idMatch) || (idCount > -1 && findCount === idCount)) { + index++ + if (index < path.length) { + 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') { + // 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: textStartPos + 1 } + } + } + + findCount++ + } + } + + // 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) + + // 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) + + if (startNode && 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) { + dispatch(tr) + } +} + // 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 +577,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 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..36ac08ca 100644 --- a/src/render/workers/project-archive-worker.js +++ b/src/render/workers/project-archive-worker.js @@ -3,6 +3,8 @@ 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 { serializeStandoff } = require('./serialize-standoff') const fs = window.fairCopy.getFs() const os = window.fairCopy.getOs() @@ -18,7 +20,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 +29,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 +115,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 +162,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 +191,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 +210,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 +267,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 +284,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 +304,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 +313,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 +327,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 +354,27 @@ 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 + // { 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 @@ -363,17 +386,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 +407,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 +432,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..ebf62a02 100644 --- a/src/render/workers/remote-project-worker.js +++ b/src/render/workers/remote-project-worker.js @@ -1,63 +1,87 @@ 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) => { +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 + checkForAnnotationData(serverURL, userID, authToken, remoteResources, postMessage) 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 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 }) }, - (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 +91,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 +108,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 +137,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 +149,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}`) } 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